Setup eslint and enforce rules
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
82e7a1f299
commit
1d5056b753
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
lib
|
||||
dist
|
113
.eslintrc.json
Normal file
113
.eslintrc.json
Normal file
@ -0,0 +1,113 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-console": "error",
|
||||
"curly": "error",
|
||||
"eqeqeq": "error",
|
||||
"guard-for-in": "error",
|
||||
"no-alert": "error",
|
||||
"no-caller": "error",
|
||||
"no-constructor-return": "error",
|
||||
"no-eval": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-invalid-this": "error",
|
||||
"no-return-await": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-useless-call": "error",
|
||||
"radix": "error",
|
||||
"yoda": "error",
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
"brace-style": "error",
|
||||
"camelcase": "error",
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": false,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"comma-style": [
|
||||
"error",
|
||||
"last"
|
||||
],
|
||||
"computed-property-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"eol-last": "error",
|
||||
"func-call-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "error",
|
||||
"max-params": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"new-parens": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"newline-per-chained-call": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-underscore-dangle": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"object-property-newline": "error",
|
||||
"prefer-exponentiation-operator": "error",
|
||||
"prefer-object-spread": "error",
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
1338
package-lock.json
generated
1338
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -38,10 +38,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prebuild": "pnpm run lint",
|
||||
"build": "tsc",
|
||||
"app": "tsc && node lib/index.js",
|
||||
"prepare": "pnpm run build",
|
||||
"docs:build": "typedoc --options typedoc.json"
|
||||
"docs:build": "typedoc --options typedoc.json",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint --fix . --ext .ts"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
@ -53,5 +56,10 @@
|
||||
"url": "https://code.garrettmills.dev/extollo/lib"
|
||||
},
|
||||
"author": "garrettmills <shout@garrettmills.dev>",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"eslint": "^7.27.0"
|
||||
}
|
||||
}
|
||||
|
1025
pnpm-lock.yaml
1025
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
||||
import {Injectable, Inject} from "../di"
|
||||
import {infer, ErrorWithContext} from "../util"
|
||||
import {CLIOption} from "./directive/options/CLIOption"
|
||||
import {PositionalOption} from "./directive/options/PositionalOption";
|
||||
import {FlagOption} from "./directive/options/FlagOption";
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {Logging} from "../service/Logging";
|
||||
import {Injectable, Inject} from '../di'
|
||||
import {infer, ErrorWithContext} from '../util'
|
||||
import {CLIOption} from './directive/options/CLIOption'
|
||||
import {PositionalOption} from './directive/options/PositionalOption'
|
||||
import {FlagOption} from './directive/options/FlagOption'
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {Logging} from '../service/Logging'
|
||||
|
||||
/**
|
||||
* Type alias for a definition of a command-line option.
|
||||
@ -35,7 +35,7 @@ export abstract class Directive extends AppClass {
|
||||
protected readonly logging!: Logging
|
||||
|
||||
/** Parsed option values. */
|
||||
private _optionValues: any
|
||||
private optionValues: any
|
||||
|
||||
/**
|
||||
* Get the keyword or array of keywords that will specify this directive.
|
||||
@ -84,8 +84,8 @@ export abstract class Directive extends AppClass {
|
||||
* @param optionValues
|
||||
* @private
|
||||
*/
|
||||
private _setOptionValues(optionValues: any) {
|
||||
this._optionValues = optionValues;
|
||||
private setOptionValues(optionValues: any) {
|
||||
this.optionValues = optionValues
|
||||
}
|
||||
|
||||
/**
|
||||
@ -93,9 +93,9 @@ export abstract class Directive extends AppClass {
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
public option(name: string, defaultValue?: any) {
|
||||
if ( name in this._optionValues ) {
|
||||
return this._optionValues[name]
|
||||
public option(name: string, defaultValue?: unknown): any {
|
||||
if ( name in this.optionValues ) {
|
||||
return this.optionValues[name]
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
@ -110,20 +110,28 @@ export abstract class Directive extends AppClass {
|
||||
*
|
||||
* @param argv
|
||||
*/
|
||||
async invoke(argv: string[]) {
|
||||
async invoke(argv: string[]): Promise<void> {
|
||||
const options = this.getResolvedOptions()
|
||||
|
||||
if ( this.didRequestUsage(argv) ) {
|
||||
// @ts-ignore
|
||||
const positionalArguments: PositionalOption<any>[] = options.filter(opt => opt instanceof PositionalOption)
|
||||
const positionalArguments: PositionalOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof PositionalOption ) {
|
||||
positionalArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const flagArguments: FlagOption<any>[] = options.filter(opt => opt instanceof FlagOption)
|
||||
const flagArguments: FlagOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof FlagOption ) {
|
||||
flagArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
const positionalDisplay: string = positionalArguments.map(x => `<${x.getArgumentName()}>`).join(' ')
|
||||
const flagDisplay: string = flagArguments.length ? ' [...flags]' : ''
|
||||
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`DIRECTIVE: ${this.getMainKeyword()} - ${this.getDescription()}`,
|
||||
'',
|
||||
@ -131,7 +139,7 @@ export abstract class Directive extends AppClass {
|
||||
].join('\n'))
|
||||
|
||||
if ( positionalArguments.length ) {
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`POSITIONAL ARGUMENTS:`,
|
||||
...(positionalArguments.map(arg => {
|
||||
@ -141,7 +149,7 @@ export abstract class Directive extends AppClass {
|
||||
}
|
||||
|
||||
if ( flagArguments.length ) {
|
||||
console.log([
|
||||
this.nativeOutput([
|
||||
'',
|
||||
`FLAGS:`,
|
||||
...(flagArguments.map(arg => {
|
||||
@ -152,34 +160,34 @@ export abstract class Directive extends AppClass {
|
||||
|
||||
const help = this.getHelpText()
|
||||
if ( help ) {
|
||||
console.log('\n' + help)
|
||||
this.nativeOutput('\n' + help)
|
||||
}
|
||||
|
||||
console.log('\n')
|
||||
this.nativeOutput('\n')
|
||||
} else {
|
||||
try {
|
||||
const optionValues = this.parseOptions(options, argv)
|
||||
this._setOptionValues(optionValues)
|
||||
this.setOptionValues(optionValues)
|
||||
await this.handle(argv)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
this.nativeOutput(e.message)
|
||||
if ( e instanceof OptionValidationError ) {
|
||||
// expecting, value, requirements
|
||||
if ( e.context.expecting ) {
|
||||
console.error(` - Expecting: ${e.context.expecting}`)
|
||||
this.nativeOutput(` - Expecting: ${e.context.expecting}`)
|
||||
}
|
||||
|
||||
if ( e.context.requirements && Array.isArray(e.context.requirements) ) {
|
||||
for ( const req of e.context.requirements ) {
|
||||
console.error(` - ${req}`)
|
||||
this.nativeOutput(` - ${req}`)
|
||||
}
|
||||
}
|
||||
|
||||
if ( e.context.value ) {
|
||||
console.error(` - ${e.context.value}`)
|
||||
this.nativeOutput(` - ${e.context.value}`)
|
||||
}
|
||||
}
|
||||
console.error('\nUse --help for more info.')
|
||||
this.nativeOutput('\nUse --help for more info.')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,9 +225,11 @@ export abstract class Directive extends AppClass {
|
||||
* Returns true if the given keyword should invoke this directive.
|
||||
* @param name
|
||||
*/
|
||||
public matchesKeyword(name: string) {
|
||||
public matchesKeyword(name: string): boolean {
|
||||
let kws = this.getKeywords()
|
||||
if ( !Array.isArray(kws) ) kws = [kws]
|
||||
if ( !Array.isArray(kws) ) {
|
||||
kws = [kws]
|
||||
}
|
||||
return kws.includes(name)
|
||||
}
|
||||
|
||||
@ -227,7 +237,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as success text.
|
||||
* @param output
|
||||
*/
|
||||
success(output: any) {
|
||||
success(output: unknown): void {
|
||||
this.logging.success(output, true)
|
||||
}
|
||||
|
||||
@ -235,7 +245,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as error text.
|
||||
* @param output
|
||||
*/
|
||||
error(output: any) {
|
||||
error(output: unknown): void {
|
||||
this.logging.error(output, true)
|
||||
}
|
||||
|
||||
@ -243,7 +253,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as warning text.
|
||||
* @param output
|
||||
*/
|
||||
warn(output: any) {
|
||||
warn(output: unknown): void {
|
||||
this.logging.warn(output, true)
|
||||
}
|
||||
|
||||
@ -251,7 +261,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as info text.
|
||||
* @param output
|
||||
*/
|
||||
info(output: any) {
|
||||
info(output: unknown): void {
|
||||
this.logging.info(output, true)
|
||||
}
|
||||
|
||||
@ -259,7 +269,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as debugging text.
|
||||
* @param output
|
||||
*/
|
||||
debug(output: any) {
|
||||
debug(output: unknown): void {
|
||||
this.logging.debug(output, true)
|
||||
}
|
||||
|
||||
@ -267,7 +277,7 @@ export abstract class Directive extends AppClass {
|
||||
* Print the given output to the log as verbose text.
|
||||
* @param output
|
||||
*/
|
||||
verbose(output: any) {
|
||||
verbose(output: unknown): void {
|
||||
this.logging.verbose(output, true)
|
||||
}
|
||||
|
||||
@ -275,7 +285,7 @@ export abstract class Directive extends AppClass {
|
||||
* Get the flag option that signals help. Usually, this is named 'help'
|
||||
* and supports the flags '--help' and '-?'.
|
||||
*/
|
||||
getHelpOption() {
|
||||
getHelpOption(): FlagOption<any> {
|
||||
return new FlagOption('--help', '-?', 'usage information about this directive')
|
||||
}
|
||||
|
||||
@ -283,12 +293,21 @@ export abstract class Directive extends AppClass {
|
||||
* Process the raw CLI arguments using an array of option class instances to build
|
||||
* a mapping of option names to provided values.
|
||||
*/
|
||||
parseOptions(options: CLIOption<any>[], args: string[]) {
|
||||
// @ts-ignore
|
||||
let positionalArguments: PositionalOption<any>[] = options.filter(cls => cls instanceof PositionalOption)
|
||||
parseOptions(options: CLIOption<any>[], args: string[]): {[key: string]: any} {
|
||||
let positionalArguments: PositionalOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof PositionalOption ) {
|
||||
positionalArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
const flagArguments: FlagOption<any>[] = []
|
||||
options.forEach(opt => {
|
||||
if ( opt instanceof FlagOption ) {
|
||||
flagArguments.push(opt)
|
||||
}
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const flagArguments: FlagOption<any>[] = options.filter(cls => cls instanceof FlagOption)
|
||||
const optionValue: any = {}
|
||||
|
||||
flagArguments.push(this.getHelpOption())
|
||||
@ -325,7 +344,7 @@ export abstract class Directive extends AppClass {
|
||||
const flagArgument = flagArguments.filter(x => x.shortFlag === value)
|
||||
if ( flagArgument.length < 1 ) {
|
||||
throw new OptionValidationError(`Unknown flag argument: ${value}`, {
|
||||
value
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
if ( flagArgument[0].argumentDescription ) {
|
||||
@ -350,7 +369,7 @@ export abstract class Directive extends AppClass {
|
||||
} else {
|
||||
if ( positionalArguments.length < 1 ) {
|
||||
throw new OptionValidationError(`Unknown positional argument: ${value}`, {
|
||||
value
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
const inferredValue = infer(value)
|
||||
@ -368,13 +387,13 @@ export abstract class Directive extends AppClass {
|
||||
|
||||
if ( expectingFlagArgument ) {
|
||||
throw new OptionValidationError(`Missing argument for flag: ${positionalFlagName}`, {
|
||||
expecting: positionalFlagName
|
||||
expecting: positionalFlagName,
|
||||
})
|
||||
}
|
||||
|
||||
if ( positionalArguments.length > 0 ) {
|
||||
throw new OptionValidationError(`Missing required argument: ${positionalArguments[0].getArgumentName()}`, {
|
||||
expecting: positionalArguments[0].getArgumentName()
|
||||
expecting: positionalArguments[0].getArgumentName(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -430,14 +449,18 @@ export abstract class Directive extends AppClass {
|
||||
* Determines if, at any point in the arguments, the help option's short or long flag appears.
|
||||
* @returns {boolean} - true if the help flag appeared
|
||||
*/
|
||||
didRequestUsage(argv: string[]) {
|
||||
const help_option = this.getHelpOption()
|
||||
didRequestUsage(argv: string[]): boolean {
|
||||
const helpOption = this.getHelpOption()
|
||||
for ( const arg of argv ) {
|
||||
if ( arg.trim() === help_option.longFlag || arg.trim() === help_option.shortFlag ) {
|
||||
if ( arg.trim() === helpOption.longFlag || arg.trim() === helpOption.shortFlag ) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
protected nativeOutput(...outputs: any[]): void {
|
||||
console.log(...outputs) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Directive} from "../Directive"
|
||||
import {CommandLineApplication} from "../service"
|
||||
import {Injectable} from "../../di"
|
||||
import {ErrorWithContext} from "../../util"
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Directive} from '../Directive'
|
||||
import {CommandLineApplication} from '../service'
|
||||
import {Injectable} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
|
||||
/**
|
||||
* A directive that starts the framework's final target normally.
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Directive} from "../Directive"
|
||||
import * as colors from "colors/safe"
|
||||
import {Directive} from '../Directive'
|
||||
import * as colors from 'colors/safe'
|
||||
import * as repl from 'repl'
|
||||
import {DependencyKey} from "../../di";
|
||||
import {DependencyKey} from '../../di'
|
||||
|
||||
/**
|
||||
* Launch an interactive REPL shell from within the application.
|
||||
@ -9,7 +9,7 @@ import {DependencyKey} from "../../di";
|
||||
*/
|
||||
export class ShellDirective extends Directive {
|
||||
protected options: any = {
|
||||
welcome: `powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||
welcome: `powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills\nAccess your application using the "app" global.`,
|
||||
prompt: `${colors.blue('(')}extollo${colors.blue(') ➤ ')}`,
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export class ShellDirective extends Directive {
|
||||
}
|
||||
|
||||
await new Promise<void>(res => {
|
||||
console.log(this.options.welcome)
|
||||
this.nativeOutput(this.options.welcome)
|
||||
this.repl = repl.start(this.options.prompt)
|
||||
Object.assign(this.repl.context, state)
|
||||
this.repl.on('exit', () => res())
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Directive, OptionDefinition} from "../Directive"
|
||||
import {PositionalOption} from "./options/PositionalOption"
|
||||
import {CommandLine} from "../service"
|
||||
import {Inject, Injectable} from "../../di";
|
||||
import {ErrorWithContext} from "../../util";
|
||||
import {Directive, OptionDefinition} from '../Directive'
|
||||
import {PositionalOption} from './options/PositionalOption'
|
||||
import {CommandLine} from '../service'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
|
||||
/**
|
||||
* Create a new file based on a template registered with the CommandLine service.
|
||||
@ -46,11 +46,11 @@ export class TemplateDirective extends Directive {
|
||||
'',
|
||||
...(registeredTemplates.map(template => {
|
||||
return ` - ${template.name}: ${template.description}`
|
||||
}).all())
|
||||
}).all()),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async handle(argv: string[]) {
|
||||
async handle(): Promise<void> {
|
||||
const templateName: string = this.option('template_name')
|
||||
const destinationName: string = this.option('file_name')
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Directive} from "../Directive"
|
||||
import {Injectable, Inject} from "../../di"
|
||||
import {padRight} from "../../util"
|
||||
import {CommandLine} from "../service"
|
||||
import {Directive} from '../Directive'
|
||||
import {Injectable, Inject} from '../../di'
|
||||
import {padRight} from '../../util'
|
||||
import {CommandLine} from '../service'
|
||||
|
||||
/**
|
||||
* Directive that prints the help message and usage information about
|
||||
@ -20,7 +20,7 @@ export class UsageDirective extends Directive {
|
||||
return 'print information about available commands'
|
||||
}
|
||||
|
||||
public handle(argv: string[]): void | Promise<void> {
|
||||
public handle(): void | Promise<void> {
|
||||
const directiveStrings = this.cli.getDirectives()
|
||||
.map(cls => this.make<Directive>(cls))
|
||||
.map<[string, string]>(dir => {
|
||||
@ -30,15 +30,15 @@ export class UsageDirective extends Directive {
|
||||
const maxLen = directiveStrings.max<number>(x => x[0].length)
|
||||
|
||||
const printStrings = directiveStrings.map(grp => {
|
||||
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||
})
|
||||
return [padRight(grp[0], maxLen + 1), grp[1]]
|
||||
})
|
||||
.map(grp => {
|
||||
return ` ${grp[0]}: ${grp[1]}`
|
||||
})
|
||||
.toArray()
|
||||
|
||||
console.log(this.cli.getASCIILogo())
|
||||
console.log([
|
||||
this.nativeOutput(this.cli.getASCIILogo())
|
||||
this.nativeOutput([
|
||||
'',
|
||||
'Welcome to Extollo! Specify a command to get started.',
|
||||
'',
|
||||
@ -48,7 +48,7 @@ export class UsageDirective extends Directive {
|
||||
'',
|
||||
'For usage information about a particular command, pass the --help flag.',
|
||||
'-------------------------------------------',
|
||||
`powered by Extollo, © ${(new Date).getFullYear()} Garrett Mills`,
|
||||
`powered by Extollo, © ${(new Date()).getFullYear()} Garrett Mills`,
|
||||
].join('\n'))
|
||||
}
|
||||
}
|
||||
|
@ -9,200 +9,207 @@ export abstract class CLIOption<T> {
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useWhitelist: boolean = false
|
||||
protected useWhitelist = false
|
||||
|
||||
/**
|
||||
* Do we use the blacklist?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useBlacklist: boolean = false
|
||||
protected useBlacklist = false
|
||||
|
||||
/**
|
||||
* Do we use the less-than comparison?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useLessThan: boolean = false
|
||||
protected useLessThan = false
|
||||
|
||||
/**
|
||||
* Do we use the greater-than comparison?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useGreaterThan: boolean = false
|
||||
protected useGreaterThan = false
|
||||
|
||||
/**
|
||||
* Do we use the equality operator?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _useEquality: boolean = false
|
||||
protected useEquality = false
|
||||
|
||||
/**
|
||||
* Is this option optional?
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _optional: boolean = false
|
||||
protected isOptional = false
|
||||
|
||||
/**
|
||||
* Whitelisted values.
|
||||
* @type {Array<*>}
|
||||
* @private
|
||||
*/
|
||||
protected _whitelist: T[] = []
|
||||
protected whitelistItems: T[] = []
|
||||
|
||||
/**
|
||||
* Blacklisted values.
|
||||
* @type {Array<*>}
|
||||
* @private
|
||||
*/
|
||||
protected _blacklist: T[] = []
|
||||
protected blacklistItems: T[] = []
|
||||
|
||||
/**
|
||||
* Value to be compared in less than.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _lessThanValue?: T
|
||||
protected lessThanValue?: T
|
||||
|
||||
/**
|
||||
* If true, the less than will be less than or equal to.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _lessThanBit: boolean = false
|
||||
protected lessThanBit = false
|
||||
|
||||
/**
|
||||
* Value to be compared in greater than.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _greaterThanValue?: T
|
||||
protected greaterThanValue?: T
|
||||
|
||||
/**
|
||||
* If true, the greater than will be greater than or equal to.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
protected _greateerThanBit: boolean = false
|
||||
protected greaterThanBit = false
|
||||
|
||||
/**
|
||||
* The value to be used to check equality.
|
||||
* @type {*}
|
||||
* @private
|
||||
*/
|
||||
protected _equalityValue?: T
|
||||
protected equalityValue?: T
|
||||
|
||||
/**
|
||||
* Whitelist the specified item or items and enable the whitelist.
|
||||
* @param {...*} items - the items to whitelist
|
||||
*/
|
||||
whitelist(...items: T[]) {
|
||||
this._useWhitelist = true
|
||||
items.forEach(item => this._whitelist.push(item))
|
||||
whitelist(...items: T[]): this {
|
||||
this.useWhitelist = true
|
||||
items.forEach(item => this.whitelistItems.push(item))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Blacklist the specified item or items and enable the blacklist.
|
||||
* @param {...*} items - the items to blacklist
|
||||
*/
|
||||
blacklist(...items: T[]) {
|
||||
this._useBlacklist = true
|
||||
items.forEach(item => this._blacklist.push(item))
|
||||
blacklist(...items: T[]): this {
|
||||
this.useBlacklist = true
|
||||
items.forEach(item => this.blacklistItems.push(item))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in less-than comparison and enables less-than comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
lessThan(value: T) {
|
||||
this._useLessThan = true
|
||||
this._lessThanValue = value
|
||||
lessThan(value: T): this {
|
||||
this.useLessThan = true
|
||||
this.lessThanValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in less-than or equal-to comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
lessThanOrEqualTo(value: T) {
|
||||
this._lessThanBit = true
|
||||
lessThanOrEqualTo(value: T): this {
|
||||
this.lessThanBit = true
|
||||
this.lessThan(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in greater-than comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
greaterThan(value: T) {
|
||||
this._useGreaterThan = true
|
||||
this._greaterThanValue = value
|
||||
greaterThan(value: T): this {
|
||||
this.useGreaterThan = true
|
||||
this.greaterThanValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in greater-than or equal-to comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
greaterThanOrEqualTo(value: T) {
|
||||
this._greateerThanBit = true
|
||||
greaterThanOrEqualTo(value: T): this {
|
||||
this.greaterThanBit = true
|
||||
this.greaterThan(value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the value to be used in equality comparison and enables that comparison.
|
||||
* @param {*} value
|
||||
*/
|
||||
equals(value: T) {
|
||||
this._useEquality = true
|
||||
this._equalityValue = value
|
||||
equals(value: T): this {
|
||||
this.useEquality = true
|
||||
this.equalityValue = value
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified value passes the configured comparisons.
|
||||
* @param {*} value
|
||||
* @param value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate(value: any) {
|
||||
let is_valid = true
|
||||
if ( this._useEquality ) {
|
||||
is_valid = is_valid && (this._equalityValue === value)
|
||||
validate(value: T): boolean {
|
||||
let isValid = true
|
||||
if ( this.useEquality ) {
|
||||
isValid = isValid && (this.equalityValue === value)
|
||||
}
|
||||
|
||||
if ( this._useLessThan && typeof this._lessThanValue !== 'undefined' ) {
|
||||
if ( this._lessThanBit ) {
|
||||
is_valid = is_valid && (value <= this._lessThanValue)
|
||||
if ( this.useLessThan && typeof this.lessThanValue !== 'undefined' ) {
|
||||
if ( this.lessThanBit ) {
|
||||
isValid = isValid && (value <= this.lessThanValue)
|
||||
} else {
|
||||
is_valid = is_valid && (value < this._lessThanValue)
|
||||
isValid = isValid && (value < this.lessThanValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ( this._useGreaterThan && typeof this._greaterThanValue !== 'undefined' ) {
|
||||
if ( this._greateerThanBit ) {
|
||||
is_valid = is_valid && (value >= this._greaterThanValue)
|
||||
if ( this.useGreaterThan && typeof this.greaterThanValue !== 'undefined' ) {
|
||||
if ( this.greaterThanBit ) {
|
||||
isValid = isValid && (value >= this.greaterThanValue)
|
||||
} else {
|
||||
is_valid = is_valid && (value > this._greaterThanValue)
|
||||
isValid = isValid && (value > this.greaterThanValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ( this._useWhitelist ) {
|
||||
is_valid = is_valid && this._whitelist.some(x => {
|
||||
if ( this.useWhitelist ) {
|
||||
isValid = isValid && this.whitelistItems.some(x => {
|
||||
return x === value
|
||||
})
|
||||
}
|
||||
|
||||
if ( this._useBlacklist ) {
|
||||
is_valid = is_valid && !(this._blacklist.some(x => x === value))
|
||||
if ( this.useBlacklist ) {
|
||||
isValid = isValid && !(this.blacklistItems.some(x => x === value))
|
||||
}
|
||||
|
||||
return is_valid
|
||||
return isValid
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Option as optional.
|
||||
*/
|
||||
optional() {
|
||||
this._optional = true
|
||||
optional(): this {
|
||||
this.isOptional = true
|
||||
return this
|
||||
}
|
||||
|
||||
@ -216,27 +223,27 @@ export abstract class CLIOption<T> {
|
||||
* Get an array of strings denoting the human-readable requirements for this option to be valid.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
getRequirementDisplays() {
|
||||
getRequirementDisplays(): string[] {
|
||||
const clauses = []
|
||||
|
||||
if ( this._useBlacklist ) {
|
||||
clauses.push(`must not be one of: ${this._blacklist.map(x => String(x)).join(', ')}`)
|
||||
if ( this.useBlacklist ) {
|
||||
clauses.push(`must not be one of: ${this.blacklistItems.map(x => String(x)).join(', ')}`)
|
||||
}
|
||||
|
||||
if ( this._useWhitelist ) {
|
||||
clauses.push(`must be one of: ${this._whitelist.map(x => String(x)).join(', ')}`)
|
||||
if ( this.useWhitelist ) {
|
||||
clauses.push(`must be one of: ${this.whitelistItems.map(x => String(x)).join(', ')}`)
|
||||
}
|
||||
|
||||
if ( this._useGreaterThan ) {
|
||||
clauses.push(`must be greater than${this._greateerThanBit ? ' or equal to' : ''}: ${String(this._greaterThanValue)}`)
|
||||
if ( this.useGreaterThan ) {
|
||||
clauses.push(`must be greater than${this.greaterThanBit ? ' or equal to' : ''}: ${String(this.greaterThanValue)}`)
|
||||
}
|
||||
|
||||
if ( this._useLessThan ) {
|
||||
clauses.push(`must be less than${this._lessThanBit ? ' or equal to' : ''}: ${String(this._lessThanValue)}`)
|
||||
if ( this.useLessThan ) {
|
||||
clauses.push(`must be less than${this.lessThanBit ? ' or equal to' : ''}: ${String(this.lessThanValue)}`)
|
||||
}
|
||||
|
||||
if ( this._useEquality ) {
|
||||
clauses.push(`must be equal to: ${String(this._equalityValue)}`)
|
||||
if ( this.useEquality ) {
|
||||
clauses.push(`must be equal to: ${String(this.equalityValue)}`)
|
||||
}
|
||||
|
||||
return clauses
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {CLIOption} from "./CLIOption"
|
||||
import {CLIOption} from './CLIOption'
|
||||
|
||||
/**
|
||||
* Non-positional, flag-based CLI option.
|
||||
@ -24,8 +24,10 @@ export class FlagOption<T> extends CLIOption<T> {
|
||||
* Description of the argument required by this flag.
|
||||
* If this is set, the flag will expect a positional argument to follow as a param.
|
||||
*/
|
||||
public readonly argumentDescription?: string
|
||||
) { super() }
|
||||
public readonly argumentDescription?: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the referential name for this option.
|
||||
@ -33,7 +35,7 @@ export class FlagOption<T> extends CLIOption<T> {
|
||||
* be found, the short flag (without the '-') is used.
|
||||
* @returns {string}
|
||||
*/
|
||||
getArgumentName() {
|
||||
getArgumentName(): string {
|
||||
if ( this.longFlag ) {
|
||||
return this.longFlag.replace('--', '')
|
||||
} else if ( this.shortFlag ) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {CLIOption} from "./CLIOption"
|
||||
import {CLIOption} from './CLIOption'
|
||||
|
||||
/**
|
||||
* A positional CLI option. Defined without a flag.
|
||||
@ -19,14 +19,16 @@ export class PositionalOption<T> extends CLIOption<T> {
|
||||
/**
|
||||
* A usage message describing this parameter.
|
||||
*/
|
||||
public readonly message: string = ''
|
||||
) { super() }
|
||||
public readonly message: string = '',
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the option.
|
||||
* @returns {string}
|
||||
*/
|
||||
getArgumentName () {
|
||||
getArgumentName(): string {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {Singleton, Instantiable, Inject} from "../../di"
|
||||
import {Collection} from "../../util"
|
||||
import {CommandLineApplication} from "./CommandLineApplication"
|
||||
import {Directive} from "../Directive"
|
||||
import {Template} from "../Template"
|
||||
import {directive_template} from "../templates/directive"
|
||||
import {unit_template} from "../templates/unit";
|
||||
import {controller_template} from "../templates/controller";
|
||||
import {middleware_template} from "../templates/middleware";
|
||||
import {routes_template} from "../templates/routes";
|
||||
import {config_template} from "../templates/config";
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Singleton, Instantiable, Inject} from '../../di'
|
||||
import {Collection} from '../../util'
|
||||
import {CommandLineApplication} from './CommandLineApplication'
|
||||
import {Directive} from '../Directive'
|
||||
import {Template} from '../Template'
|
||||
import {templateDirective} from '../templates/directive'
|
||||
import {templateUnit} from '../templates/unit'
|
||||
import {templateController} from '../templates/controller'
|
||||
import {templateMiddleware} from '../templates/middleware'
|
||||
import {templateRoutes} from '../templates/routes'
|
||||
import {templateConfig} from '../templates/config'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Service for managing directives, templates, and other resources related
|
||||
@ -27,28 +27,30 @@ export class CommandLine extends Unit {
|
||||
/** Templates registered with the CLI command. These can be created with the TemplateDirective. */
|
||||
protected templates: Collection<Template> = new Collection<Template>()
|
||||
|
||||
constructor() { super() }
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
async up() {
|
||||
this.registerTemplate(directive_template)
|
||||
this.registerTemplate(unit_template)
|
||||
this.registerTemplate(controller_template)
|
||||
this.registerTemplate(middleware_template)
|
||||
this.registerTemplate(routes_template)
|
||||
this.registerTemplate(config_template)
|
||||
async up(): Promise<void> {
|
||||
this.registerTemplate(templateDirective)
|
||||
this.registerTemplate(templateUnit)
|
||||
this.registerTemplate(templateController)
|
||||
this.registerTemplate(templateMiddleware)
|
||||
this.registerTemplate(templateRoutes)
|
||||
this.registerTemplate(templateConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the application was started from the command line.
|
||||
*/
|
||||
public isCLI() {
|
||||
public isCLI(): boolean {
|
||||
return this.app().hasUnit(CommandLineApplication)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the Extollo ASCII logo.
|
||||
*/
|
||||
public getASCIILogo() {
|
||||
public getASCIILogo(): string {
|
||||
return ` _
|
||||
/ /\\ ______ _ _ _
|
||||
/ / \\ | ____| | | | | |
|
||||
@ -64,24 +66,26 @@ export class CommandLine extends Unit {
|
||||
* the directive available for use on the CLI.
|
||||
* @param directiveClass
|
||||
*/
|
||||
public registerDirective(directiveClass: Instantiable<Directive>) {
|
||||
public registerDirective(directiveClass: Instantiable<Directive>): this {
|
||||
if ( !this.directives.includes(directiveClass) ) {
|
||||
this.directives.push(directiveClass)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given directive is registered with this service.
|
||||
* @param directiveClass
|
||||
*/
|
||||
public hasDirective(directiveClass: Instantiable<Directive>) {
|
||||
public hasDirective(directiveClass: Instantiable<Directive>): boolean {
|
||||
return this.directives.includes(directiveClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of all registered directives.
|
||||
*/
|
||||
public getDirectives() {
|
||||
public getDirectives(): Collection<Instantiable<Directive>> {
|
||||
return this.directives.clone()
|
||||
}
|
||||
|
||||
@ -90,21 +94,23 @@ export class CommandLine extends Unit {
|
||||
* available for use with the TemplateDirective service.
|
||||
* @param template
|
||||
*/
|
||||
public registerTemplate(template: Template) {
|
||||
public registerTemplate(template: Template): this {
|
||||
if ( !this.templates.firstWhere('name', '=', template.name) ) {
|
||||
this.templates.push(template)
|
||||
} else {
|
||||
this.logging.warn(`Duplicate template will not be registered: ${template.name}`)
|
||||
this.logging.debug(`Duplicate template registered at: ${(new Error()).stack}`)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a template with the given name exists.
|
||||
* @param name
|
||||
*/
|
||||
public hasTemplate(name: string) {
|
||||
return !!this.templates.firstWhere('name', '=', name)
|
||||
public hasTemplate(name: string): boolean {
|
||||
return Boolean(this.templates.firstWhere('name', '=', name))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,7 +124,7 @@ export class CommandLine extends Unit {
|
||||
/**
|
||||
* Get a collection of all registered templates.
|
||||
*/
|
||||
public getTemplates() {
|
||||
public getTemplates(): Collection<Template> {
|
||||
return this.templates.clone()
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {Unit} from "../../lifecycle/Unit"
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Singleton, Inject} from "../../di/decorator/injection"
|
||||
import {CommandLine} from "./CommandLine"
|
||||
import {UsageDirective} from "../directive/UsageDirective";
|
||||
import {Directive} from "../Directive";
|
||||
import {ShellDirective} from "../directive/ShellDirective";
|
||||
import {TemplateDirective} from "../directive/TemplateDirective";
|
||||
import {RunDirective} from "../directive/RunDirective";
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {Singleton, Inject} from '../../di/decorator/injection'
|
||||
import {CommandLine} from './CommandLine'
|
||||
import {UsageDirective} from '../directive/UsageDirective'
|
||||
import {Directive} from '../Directive'
|
||||
import {ShellDirective} from '../directive/ShellDirective'
|
||||
import {TemplateDirective} from '../directive/TemplateDirective'
|
||||
import {RunDirective} from '../directive/RunDirective'
|
||||
|
||||
/**
|
||||
* Unit that takes the place of the final unit in the application that handles
|
||||
@ -18,12 +18,12 @@ export class CommandLineApplication extends Unit {
|
||||
private static replacement?: typeof Unit
|
||||
|
||||
/** Set the replaced unit. */
|
||||
public static setReplacement(unitClass?: typeof Unit) {
|
||||
public static setReplacement(unitClass?: typeof Unit): void {
|
||||
this.replacement = unitClass
|
||||
}
|
||||
|
||||
/** Get the replaced unit. */
|
||||
public static getReplacement() {
|
||||
public static getReplacement(): typeof Unit | undefined {
|
||||
return this.replacement
|
||||
}
|
||||
|
||||
@ -33,9 +33,11 @@ export class CommandLineApplication extends Unit {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
constructor() { super() }
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public async up() {
|
||||
public async up(): Promise<void> {
|
||||
this.cli.registerDirective(UsageDirective)
|
||||
this.cli.registerDirective(ShellDirective)
|
||||
this.cli.registerDirective(TemplateDirective)
|
||||
@ -50,7 +52,7 @@ export class CommandLineApplication extends Unit {
|
||||
await match.invoke(argv.slice(1))
|
||||
} else {
|
||||
const usage = this.make<UsageDirective>(UsageDirective)
|
||||
await usage.handle(process.argv)
|
||||
await usage.handle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,21 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* A template that generates a new configuration file in the app/configs directory.
|
||||
*/
|
||||
const config_template: Template = {
|
||||
const templateConfig: Template = {
|
||||
name: 'config',
|
||||
fileSuffix: '.config.ts',
|
||||
description: 'Create a new config file.',
|
||||
baseAppPath: ['configs'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
render() {
|
||||
return `import { env } from '@extollo/lib'
|
||||
|
||||
export default {
|
||||
key: env('VALUE_ENV_VAR', 'default value'),
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { config_template }
|
||||
export { templateConfig }
|
||||
|
@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new controller in the app/http/controllers directory.
|
||||
*/
|
||||
const controller_template: Template = {
|
||||
const templateController: Template = {
|
||||
name: 'controller',
|
||||
fileSuffix: '.controller.ts',
|
||||
description: 'Create a controller class that can be used to handle requests.',
|
||||
baseAppPath: ['http', 'controllers'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Controller, view} from "@extollo/lib"
|
||||
import {Inject, Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Controller, view, Inject, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Controller
|
||||
@ -25,7 +23,7 @@ export class ${name} extends Controller {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { controller_template }
|
||||
export { templateController }
|
||||
|
@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new Directive class in the app/directives directory.
|
||||
*/
|
||||
const directive_template: Template = {
|
||||
const templateDirective: Template = {
|
||||
name: 'directive',
|
||||
fileSuffix: '.directive.ts',
|
||||
description: 'Create a new Directive class which adds functionality to the ./ex command.',
|
||||
baseAppPath: ['directives'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Directive, OptionDefinition} from "@extollo/cli"
|
||||
import {Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Directive, OptionDefinition, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Directive
|
||||
@ -37,7 +35,7 @@ export class ${name}Directive extends Directive {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { directive_template }
|
||||
export { templateDirective }
|
||||
|
@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new middleware class in app/http/middlewares.
|
||||
*/
|
||||
const middleware_template: Template = {
|
||||
const templateMiddleware: Template = {
|
||||
name: 'middleware',
|
||||
fileSuffix: '.middleware.ts',
|
||||
description: 'Create a middleware class that can be applied to routes.',
|
||||
baseAppPath: ['http', 'middlewares'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Middleware} from "@extollo/lib"
|
||||
import {Injectable} from "@extollo/di"
|
||||
render(name: string) {
|
||||
return `import {Middleware, Injectable} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Middleware
|
||||
@ -25,7 +23,7 @@ export class ${name} extends Middleware {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { middleware_template }
|
||||
export { templateMiddleware }
|
||||
|
@ -1,16 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new route definition file in app/http/routes.
|
||||
*/
|
||||
const routes_template: Template = {
|
||||
const templateRoutes: Template = {
|
||||
name: 'routes',
|
||||
fileSuffix: '.routes.ts',
|
||||
description: 'Create a file for route definitions.',
|
||||
baseAppPath: ['http', 'routes'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Route} from "@extollo/lib"
|
||||
render(name: string) {
|
||||
return `import {Route} from '@extollo/lib'
|
||||
|
||||
/*
|
||||
* ${name} Routes
|
||||
@ -20,7 +19,7 @@ const routes_template: Template = {
|
||||
|
||||
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { routes_template }
|
||||
export { templateRoutes }
|
||||
|
@ -1,17 +1,15 @@
|
||||
import {Template} from "../Template"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Template} from '../Template'
|
||||
|
||||
/**
|
||||
* Template that generates a new application unit class in app/units.
|
||||
*/
|
||||
const unit_template: Template = {
|
||||
const templateUnit: Template = {
|
||||
name: 'unit',
|
||||
fileSuffix: '.ts',
|
||||
description: 'Create a service unit that will start and stop with your application.',
|
||||
baseAppPath: ['units'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {Singleton, Inject} from "@extollo/di"
|
||||
import {Unit, Logging} from "@extollo/lib"
|
||||
render(name: string) {
|
||||
return `import {Singleton, Inject, Unit, Logging} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} Unit
|
||||
@ -32,7 +30,7 @@ export class ${name} extends Unit {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { unit_template }
|
||||
export { templateUnit }
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from "./types";
|
||||
import {AbstractFactory} from "./factory/AbstractFactory";
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from "../util";
|
||||
import {Factory} from "./factory/Factory";
|
||||
import {DuplicateFactoryKeyError} from "./error/DuplicateFactoryKeyError";
|
||||
import {ClosureFactory} from "./factory/ClosureFactory";
|
||||
import NamedFactory from "./factory/NamedFactory";
|
||||
import SingletonFactory from "./factory/SingletonFactory";
|
||||
import {InvalidDependencyKeyError} from "./error/InvalidDependencyKeyError";
|
||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
|
||||
import {AbstractFactory} from './factory/AbstractFactory'
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
||||
import {Factory} from './factory/Factory'
|
||||
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
|
||||
import {ClosureFactory} from './factory/ClosureFactory'
|
||||
import NamedFactory from './factory/NamedFactory'
|
||||
import SingletonFactory from './factory/SingletonFactory'
|
||||
import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
|
||||
|
||||
export type MaybeFactory = AbstractFactory | undefined
|
||||
export type MaybeFactory<T> = AbstractFactory<T> | undefined
|
||||
export type MaybeDependency = any | undefined
|
||||
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
|
||||
|
||||
@ -34,7 +34,7 @@ export class Container {
|
||||
* Collection of factories registered with this container.
|
||||
* @type Collection<AbstractFactory>
|
||||
*/
|
||||
protected factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
||||
protected factories: Collection<AbstractFactory<unknown>> = new Collection<AbstractFactory<unknown>>()
|
||||
|
||||
/**
|
||||
* Collection of singleton instances produced by this container.
|
||||
@ -51,12 +51,14 @@ export class Container {
|
||||
* Register a basic instantiable class as a standard Factory with this container.
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
register(dependency: Instantiable<any>) {
|
||||
if ( this.resolve(dependency) )
|
||||
register(dependency: Instantiable<any>): this {
|
||||
if ( this.resolve(dependency) ) {
|
||||
throw new DuplicateFactoryKeyError(dependency)
|
||||
}
|
||||
|
||||
const factory = new Factory(dependency)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,12 +66,14 @@ export class Container {
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {function} producer - factory to produce a value
|
||||
*/
|
||||
registerProducer(name: string | StaticClass<any, any>, producer: () => any) {
|
||||
if ( this.resolve(name) )
|
||||
registerProducer(name: DependencyKey, producer: () => any): this {
|
||||
if ( this.resolve(name) ) {
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
}
|
||||
|
||||
const factory = new ClosureFactory(name, producer)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,12 +82,14 @@ export class Container {
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
registerNamed(name: string, dependency: Instantiable<any>) {
|
||||
if ( this.resolve(name) )
|
||||
registerNamed(name: string, dependency: Instantiable<any>): this {
|
||||
if ( this.resolve(name) ) {
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
}
|
||||
|
||||
const factory = new NamedFactory(name, dependency)
|
||||
this.factories.push(factory)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,11 +98,13 @@ export class Container {
|
||||
* @param {string} key - unique name to identify the singleton in the container
|
||||
* @param value
|
||||
*/
|
||||
registerSingleton(key: string, value: any) {
|
||||
if ( this.resolve(key) )
|
||||
registerSingleton<T>(key: DependencyKey, value: T): this {
|
||||
if ( this.resolve(key) ) {
|
||||
throw new DuplicateFactoryKeyError(key)
|
||||
}
|
||||
|
||||
this.factories.push(new SingletonFactory(value, key))
|
||||
this.factories.push(new SingletonFactory(key, value))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,24 +113,30 @@ export class Container {
|
||||
* @param staticClass
|
||||
* @param instance
|
||||
*/
|
||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T) {
|
||||
if ( this.resolve(staticClass) )
|
||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
|
||||
if ( this.resolve(staticClass) ) {
|
||||
throw new DuplicateFactoryKeyError(staticClass)
|
||||
}
|
||||
|
||||
this.register(staticClass)
|
||||
this.instances.push({
|
||||
key: staticClass,
|
||||
value: instance,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a given factory with the container.
|
||||
* @param {AbstractFactory} factory
|
||||
*/
|
||||
registerFactory(factory: AbstractFactory) {
|
||||
if ( !this.factories.includes(factory) )
|
||||
registerFactory(factory: AbstractFactory<unknown>): this {
|
||||
if ( !this.factories.includes(factory) ) {
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +152,7 @@ export class Container {
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
hasKey(key: DependencyKey): boolean {
|
||||
return !!this.resolve(key)
|
||||
return Boolean(this.resolve(key))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,17 +161,22 @@ export class Container {
|
||||
*/
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const instances = this.instances.where('key', '=', key)
|
||||
if ( instances.isNotEmpty() ) return instances.first()
|
||||
if ( instances.isNotEmpty() ) {
|
||||
return instances.first()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the factory for the given key, if one is registered with this container.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
resolve(key: DependencyKey): MaybeFactory<unknown> {
|
||||
const factory = this.factories.firstWhere(item => item.match(key))
|
||||
if ( factory ) return factory
|
||||
else logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||
if ( factory ) {
|
||||
return factory
|
||||
} else {
|
||||
logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -172,22 +191,25 @@ export class Container {
|
||||
// If we've already instantiated this, just return that
|
||||
const instance = this.getExistingInstance(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
|
||||
if ( typeof instance !== 'undefined' ) return instance.value
|
||||
if ( typeof instance !== 'undefined' ) {
|
||||
return instance.value
|
||||
}
|
||||
|
||||
// Otherwise, attempt to create it
|
||||
const factory = this.resolve(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
|
||||
if ( !factory )
|
||||
if ( !factory ) {
|
||||
throw new InvalidDependencyKeyError(key)
|
||||
}
|
||||
|
||||
// Produce and store a new instance
|
||||
const new_instance = this.produceFactory(factory, parameters)
|
||||
const newInstance = this.produceFactory(factory, parameters)
|
||||
this.instances.push({
|
||||
key,
|
||||
value: new_instance,
|
||||
value: newInstance,
|
||||
})
|
||||
|
||||
return new_instance
|
||||
return newInstance
|
||||
}
|
||||
|
||||
/**
|
||||
@ -196,7 +218,7 @@ export class Container {
|
||||
* @param {AbstractFactory} factory
|
||||
* @param {array} parameters
|
||||
*/
|
||||
protected produceFactory(factory: AbstractFactory, parameters: any[]) {
|
||||
protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
|
||||
// Create the dependencies for the factory
|
||||
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
|
||||
const dependencies = keys.map<ResolvedDependency>(req => {
|
||||
@ -210,20 +232,23 @@ export class Container {
|
||||
// Build the arguments for the factory, using dependencies in the
|
||||
// correct paramIndex positions, or parameters of we don't have
|
||||
// the dependency.
|
||||
const construction_args = []
|
||||
let params = collect(parameters).reverse()
|
||||
const constructorArguments = []
|
||||
const params = collect(parameters).reverse()
|
||||
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
|
||||
const dep = dependencies.firstWhere('paramIndex', '=', i)
|
||||
if ( dep ) construction_args.push(dep.resolved)
|
||||
else construction_args.push(params.pop())
|
||||
if ( dep ) {
|
||||
constructorArguments.push(dep.resolved)
|
||||
} else {
|
||||
constructorArguments.push(params.pop())
|
||||
}
|
||||
}
|
||||
|
||||
// Produce a new instance
|
||||
const inst = factory.produce(construction_args, params.reverse().all())
|
||||
const inst = factory.produce(constructorArguments, params.reverse().all())
|
||||
|
||||
factory.getInjectedProperties().each(dependency => {
|
||||
if ( dependency.key && inst ) {
|
||||
inst[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||
(inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||
}
|
||||
})
|
||||
|
||||
@ -240,12 +265,13 @@ export class Container {
|
||||
* @param {...any} parameters
|
||||
*/
|
||||
make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||
if ( this.hasKey(target) )
|
||||
if ( this.hasKey(target) ) {
|
||||
return this.resolveAndCreate(target, ...parameters)
|
||||
else if ( typeof target !== 'string' && isInstantiable(target) )
|
||||
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
|
||||
return this.produceFactory(new Factory(target), parameters)
|
||||
else
|
||||
} else {
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -255,8 +281,9 @@ export class Container {
|
||||
getDependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
if ( !factory )
|
||||
if ( !factory ) {
|
||||
throw new InvalidDependencyKeyError(target)
|
||||
}
|
||||
|
||||
return factory.getDependencyKeys().pluck('key')
|
||||
}
|
||||
@ -265,8 +292,9 @@ export class Container {
|
||||
* Given a different container, copy the factories and instances from this container over to it.
|
||||
* @param container
|
||||
*/
|
||||
cloneTo(container: Container) {
|
||||
cloneTo(container: Container): this {
|
||||
container.factories = this.factories.clone()
|
||||
container.instances = this.instances.clone()
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Container, MaybeDependency, MaybeFactory} from "./Container"
|
||||
import {DependencyKey} from "./types"
|
||||
import {Container, MaybeDependency, MaybeFactory} from './Container'
|
||||
import {DependencyKey} from './types'
|
||||
|
||||
/**
|
||||
* A container that uses some parent container as a base, but
|
||||
@ -26,8 +26,8 @@ export class ScopedContainer extends Container {
|
||||
* Create a new scoped container based on a parent container instance.
|
||||
* @param container
|
||||
*/
|
||||
public static fromParent(container: Container) {
|
||||
return new ScopedContainer(container);
|
||||
public static fromParent(container: Container): ScopedContainer {
|
||||
return new ScopedContainer(container)
|
||||
}
|
||||
|
||||
constructor(
|
||||
@ -47,15 +47,19 @@ export class ScopedContainer extends Container {
|
||||
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const inst = super.getExistingInstance(key)
|
||||
if ( inst ) return inst;
|
||||
if ( inst ) {
|
||||
return inst
|
||||
}
|
||||
|
||||
return this.parentContainer.getExistingInstance(key);
|
||||
return this.parentContainer.getExistingInstance(key)
|
||||
}
|
||||
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
const factory = super.resolve(key);
|
||||
if ( factory ) return factory;
|
||||
resolve(key: DependencyKey): MaybeFactory<any> {
|
||||
const factory = super.resolve(key)
|
||||
if ( factory ) {
|
||||
return factory
|
||||
}
|
||||
|
||||
return this.parentContainer?.resolve(key);
|
||||
return this.parentContainer?.resolve(key)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
import {collect, Collection} from "../../util";
|
||||
import {collect, Collection} from '../../util'
|
||||
import {
|
||||
DependencyKey,
|
||||
DependencyRequirement,
|
||||
@ -9,16 +9,16 @@ import {
|
||||
InjectionType,
|
||||
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
|
||||
PropertyDependency,
|
||||
} from "../types";
|
||||
import {Container} from "../Container";
|
||||
} from '../types'
|
||||
import {Container} from '../Container'
|
||||
|
||||
/**
|
||||
* Get a collection of dependency requirements for the given target object.
|
||||
* @param {Object} target
|
||||
* @return Collection<DependencyRequirement>
|
||||
*/
|
||||
function initDependencyMetadata(target: Object): Collection<DependencyRequirement> {
|
||||
const paramTypes = Reflect.getMetadata('design:paramtypes', target)
|
||||
function initDependencyMetadata(target: unknown): Collection<DependencyRequirement> {
|
||||
const paramTypes = Reflect.getMetadata('design:paramtypes', target as any)
|
||||
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
|
||||
return {
|
||||
paramIndex: idx,
|
||||
@ -37,32 +37,32 @@ export const Injectable = (): ClassDecorator => {
|
||||
return (target) => {
|
||||
const meta = initDependencyMetadata(target)
|
||||
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||
const new_meta = new Collection<DependencyRequirement>()
|
||||
const newMetadata = new Collection<DependencyRequirement>()
|
||||
|
||||
if ( existing ) {
|
||||
const max_new = meta.max('paramIndex')
|
||||
const max_existing = existing.max('paramIndex')
|
||||
for ( let i = 0; i <= Math.max(max_new, max_existing); i++ ) {
|
||||
const existing_dr = existing.firstWhere('paramIndex', '=', i)
|
||||
const new_dr = meta.firstWhere('paramIndex', '=', i)
|
||||
const maxNew = meta.max('paramIndex')
|
||||
const maxExisting = existing.max('paramIndex')
|
||||
for ( let i = 0; i <= Math.max(maxNew, maxExisting); i++ ) {
|
||||
const existingDR = existing.firstWhere('paramIndex', '=', i)
|
||||
const newDR = meta.firstWhere('paramIndex', '=', i)
|
||||
|
||||
if ( existing_dr && !new_dr ) {
|
||||
new_meta.push(existing_dr)
|
||||
} else if ( new_dr && !existing_dr ) {
|
||||
new_meta.push(new_dr)
|
||||
} else if ( new_dr && existing_dr ) {
|
||||
if ( existing_dr.overridden ) {
|
||||
new_meta.push(existing_dr)
|
||||
if ( existingDR && !newDR ) {
|
||||
newMetadata.push(existingDR)
|
||||
} else if ( newDR && !existingDR ) {
|
||||
newMetadata.push(newDR)
|
||||
} else if ( newDR && existingDR ) {
|
||||
if ( existingDR.overridden ) {
|
||||
newMetadata.push(existingDR)
|
||||
} else {
|
||||
new_meta.push(new_dr)
|
||||
newMetadata.push(newDR)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new_meta.concat(meta)
|
||||
newMetadata.concat(meta)
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, new_meta, target)
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, newMetadata, target)
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,14 +82,17 @@ export const Inject = (key?: DependencyKey): PropertyDecorator => {
|
||||
}
|
||||
|
||||
const type = Reflect.getMetadata('design:type', target, property)
|
||||
if ( !key && type ) key = type
|
||||
if ( !key && type ) {
|
||||
key = type
|
||||
}
|
||||
|
||||
if ( key ) {
|
||||
const existing = propertyMetadata.firstWhere('property', '=', property)
|
||||
if ( existing ) {
|
||||
existing.key = key
|
||||
} else {
|
||||
propertyMetadata.push({ property, key })
|
||||
propertyMetadata.push({ property,
|
||||
key })
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,7 +121,7 @@ export const InjectParam = (key: DependencyKey): ParameterDecorator => {
|
||||
meta.push({
|
||||
paramIndex,
|
||||
key,
|
||||
overridden: true
|
||||
overridden: true,
|
||||
})
|
||||
}
|
||||
|
||||
@ -135,7 +138,7 @@ export const Singleton = (name?: string): ClassDecorator => {
|
||||
if ( isInstantiable(target) ) {
|
||||
const injectionType: InjectionType = {
|
||||
type: name ? 'named' : 'singleton',
|
||||
...(name ? { name } : {})
|
||||
...(name ? { name } : {}),
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {DependencyKey} from "../types";
|
||||
import {DependencyKey} from '../types'
|
||||
|
||||
/**
|
||||
* Error thrown when a factory is registered with a duplicate dependency key.
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {DependencyKey} from "../types";
|
||||
import {DependencyKey} from '../types'
|
||||
|
||||
/**
|
||||
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
import { Collection } from "../../util";
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
import { Collection } from '../../util'
|
||||
|
||||
/**
|
||||
* Abstract base class for dependency container factories.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class AbstractFactory {
|
||||
export abstract class AbstractFactory<T> {
|
||||
protected constructor(
|
||||
/**
|
||||
* Token that was registered for this factory. In most cases, this is the static
|
||||
@ -13,7 +13,7 @@ export abstract class AbstractFactory {
|
||||
* @var
|
||||
* @protected
|
||||
*/
|
||||
protected token: any
|
||||
protected token: DependencyKey,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -21,14 +21,14 @@ export abstract class AbstractFactory {
|
||||
* @param {Array} dependencies - the resolved dependencies, in order
|
||||
* @param {Array} parameters - the bound constructor parameters, in order
|
||||
*/
|
||||
abstract produce(dependencies: any[], parameters: any[]): any
|
||||
abstract produce(dependencies: any[], parameters: any[]): T
|
||||
|
||||
/**
|
||||
* Should return true if the given identifier matches the token for this factory.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
abstract match(something: any): boolean
|
||||
abstract match(something: unknown): boolean
|
||||
|
||||
/**
|
||||
* Get the dependency requirements required by this factory's token.
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {DependencyRequirement, PropertyDependency, StaticClass} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
import {AbstractFactory} from './AbstractFactory'
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
|
||||
/**
|
||||
* A factory whose token is produced by calling a function.
|
||||
@ -17,19 +17,19 @@ import {Collection} from "../../util";
|
||||
* fact.produce([], []) // => 4
|
||||
* ```
|
||||
*/
|
||||
export class ClosureFactory extends AbstractFactory {
|
||||
export class ClosureFactory<T> extends AbstractFactory<T> {
|
||||
constructor(
|
||||
protected readonly name: string | StaticClass<any, any>,
|
||||
protected readonly token: () => any,
|
||||
protected readonly name: DependencyKey,
|
||||
protected readonly token: () => T,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): any {
|
||||
produce(): any {
|
||||
return this.token()
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.name
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {AbstractFactory} from './AbstractFactory'
|
||||
import {
|
||||
DEPENDENCY_KEYS_METADATA_KEY,
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||
DependencyRequirement,
|
||||
Instantiable,
|
||||
PropertyDependency
|
||||
} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
PropertyDependency,
|
||||
} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
import 'reflect-metadata'
|
||||
|
||||
/**
|
||||
@ -29,9 +29,9 @@ import 'reflect-metadata'
|
||||
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
|
||||
* ```
|
||||
*/
|
||||
export class Factory extends AbstractFactory {
|
||||
export class Factory<T> extends AbstractFactory<T> {
|
||||
constructor(
|
||||
protected readonly token: Instantiable<any>
|
||||
protected readonly token: Instantiable<T>,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
@ -40,13 +40,15 @@ export class Factory extends AbstractFactory {
|
||||
return new this.token(...dependencies, ...parameters)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.token // || (something?.name && something.name === this.token.name)
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
|
||||
if ( meta ) return meta
|
||||
if ( meta ) {
|
||||
return meta
|
||||
}
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
@ -56,7 +58,9 @@ export class Factory extends AbstractFactory {
|
||||
|
||||
do {
|
||||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
|
||||
if ( loadedMeta ) meta.concat(loadedMeta)
|
||||
if ( loadedMeta ) {
|
||||
meta.concat(loadedMeta)
|
||||
}
|
||||
currentToken = Object.getPrototypeOf(currentToken)
|
||||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {Factory} from "./Factory";
|
||||
import {Instantiable} from "../types";
|
||||
import {Factory} from './Factory'
|
||||
import {Instantiable} from '../types'
|
||||
|
||||
/**
|
||||
* Container factory that produces an instance of the token, however the token
|
||||
* is identified by a string name rather than a class reference.
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class NamedFactory extends Factory {
|
||||
export default class NamedFactory<T> extends Factory<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The name identifying this factory in the container.
|
||||
@ -18,12 +18,12 @@ export default class NamedFactory extends Factory {
|
||||
* The token to be instantiated.
|
||||
* @type {Instantiable}
|
||||
*/
|
||||
protected token: Instantiable<any>,
|
||||
protected token: Instantiable<T>,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === this.name
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Factory } from './Factory'
|
||||
import { Collection } from '../../util'
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
import {DependencyKey, DependencyRequirement, PropertyDependency} from '../types'
|
||||
|
||||
/**
|
||||
* Container factory which returns its token as its value, without attempting
|
||||
@ -19,29 +19,23 @@ import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
*
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class SingletonFactory extends Factory {
|
||||
export default class SingletonFactory<T> extends Factory<T> {
|
||||
constructor(
|
||||
/**
|
||||
* Instantiated value of this factory.
|
||||
* @type FunctionConstructor
|
||||
* Token identifying this singleton.
|
||||
*/
|
||||
protected token: FunctionConstructor,
|
||||
protected token: DependencyKey,
|
||||
|
||||
/**
|
||||
* String name of this singleton identifying it in the container.
|
||||
* @type string
|
||||
* The value of this singleton.
|
||||
*/
|
||||
protected key: string,
|
||||
protected value: T,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]) {
|
||||
return this.token
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.key
|
||||
produce(): T {
|
||||
return this.value
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor';
|
||||
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties';
|
||||
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type';
|
||||
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor'
|
||||
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties'
|
||||
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type'
|
||||
|
||||
/**
|
||||
* Interface that designates a particular value as able to be constructed.
|
||||
@ -13,20 +13,26 @@ export interface Instantiable<T> {
|
||||
* Returns true if the given value is instantiable.
|
||||
* @param what
|
||||
*/
|
||||
export function isInstantiable<T>(what: any): what is Instantiable<T> {
|
||||
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
|
||||
export function isInstantiable<T>(what: unknown): what is Instantiable<T> {
|
||||
return (
|
||||
Boolean(what)
|
||||
&& (typeof what === 'object' || typeof what === 'function')
|
||||
&& (what !== null)
|
||||
&& 'constructor' in what
|
||||
&& typeof what.constructor === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Type that identifies a value as a static class, even if it is not instantiable.
|
||||
*/
|
||||
export type StaticClass<T, T2> = Function & {prototype: T} & T2
|
||||
export type StaticClass<T, T2> = Function & {prototype: T} & T2 // eslint-disable-line @typescript-eslint/ban-types
|
||||
|
||||
/**
|
||||
* Returns true if the parameter is a static class.
|
||||
* @param something
|
||||
*/
|
||||
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
|
||||
export function isStaticClass<T, T2>(something: unknown): something is StaticClass<T, T2> {
|
||||
return typeof something === 'function' && typeof something.prototype !== 'undefined'
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {Container, Injectable, InjectParam} from '../di'
|
||||
import {Request} from "../http/lifecycle/Request";
|
||||
import {Request} from '../http/lifecycle/Request'
|
||||
import {Valid, ValidationRules} from './rules/types'
|
||||
import {Validator} from './Validator'
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {DataContainer} from "../http/lifecycle/Request";
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {DataContainer} from '../http/lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Base class for defining reusable validators for request routes.
|
||||
@ -30,10 +30,12 @@ export abstract class FormRequest<T> extends AppClass {
|
||||
|
||||
constructor(
|
||||
@InjectParam(Request)
|
||||
protected readonly data: DataContainer
|
||||
) { super() }
|
||||
protected readonly data: DataContainer,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected container() {
|
||||
protected container(): Container {
|
||||
return (this.data as unknown) as Container
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from "./rules/types";
|
||||
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from "../util";
|
||||
import {Valid, ValidationResult, ValidationRules, ValidatorFunction, ValidatorFunctionParams} from './rules/types'
|
||||
import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from '../util'
|
||||
|
||||
/**
|
||||
* An error thrown thrown when an object fails its validation.
|
||||
@ -7,15 +7,16 @@ import {Messages, ErrorWithContext, dataWalkUnsafe, dataSetUnsafe} from "../util
|
||||
export class ValidationError<T> extends ErrorWithContext {
|
||||
constructor(
|
||||
/** The original input data. */
|
||||
public readonly data: any,
|
||||
public readonly data: unknown,
|
||||
|
||||
/** The validator instance used. */
|
||||
public readonly validator: Validator<T>,
|
||||
|
||||
/** Validation error messages, by field. */
|
||||
public readonly errors: Messages
|
||||
public readonly errors: Messages,
|
||||
) {
|
||||
super('One or more fields were invalid.', { data, messages: errors.all() });
|
||||
super('One or more fields were invalid.', { data,
|
||||
messages: errors.all() })
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +26,7 @@ export class ValidationError<T> extends ErrorWithContext {
|
||||
export class Validator<T> {
|
||||
constructor(
|
||||
/** The rules used to validate input objects. */
|
||||
protected readonly rules: ValidationRules
|
||||
protected readonly rules: ValidationRules,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -47,7 +48,7 @@ export class Validator<T> {
|
||||
* Returns true if the given data is valid and type aliases it as Valid<T>.
|
||||
* @param data
|
||||
*/
|
||||
public async isValid(data: any): Promise<boolean> {
|
||||
public async isValid(data: unknown): Promise<boolean> {
|
||||
return !(await this.validateAndGetErrors(data)).any()
|
||||
}
|
||||
|
||||
@ -56,18 +57,20 @@ export class Validator<T> {
|
||||
* @param data
|
||||
* @protected
|
||||
*/
|
||||
protected async validateAndGetErrors(data: any): Promise<Messages> {
|
||||
protected async validateAndGetErrors(data: unknown): Promise<Messages> {
|
||||
const messages = new Messages()
|
||||
const params: ValidatorFunctionParams = { data }
|
||||
|
||||
for ( const key in this.rules ) {
|
||||
if ( !this.rules.hasOwnProperty(key) ) continue;
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.rules, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
// This walks over all of the values in the data structure using the nested
|
||||
// key notation. It's not type-safe, but neither is the original input object
|
||||
// yet, so it's useful here.
|
||||
for ( const walkEntry of dataWalkUnsafe<any>(data, key) ) {
|
||||
let [entry, dataKey] = walkEntry
|
||||
for ( const walkEntry of dataWalkUnsafe<any>(data as any, key) ) {
|
||||
let [entry, dataKey] = walkEntry // eslint-disable-line prefer-const
|
||||
const rules = (Array.isArray(this.rules[key]) ? this.rules[key] : [this.rules[key]]) as ValidatorFunction[]
|
||||
|
||||
for ( const rule of rules ) {
|
||||
@ -83,13 +86,15 @@ export class Validator<T> {
|
||||
}
|
||||
|
||||
for ( const error of errors ) {
|
||||
if ( !messages.has(dataKey, error) ) messages.put(dataKey, error)
|
||||
if ( !messages.has(dataKey, error) ) {
|
||||
messages.put(dataKey, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( result.valid && result.castValue ) {
|
||||
entry = result.castValue
|
||||
data = dataSetUnsafe(dataKey, entry, data)
|
||||
data = dataSetUnsafe(dataKey, entry, data as any)
|
||||
}
|
||||
|
||||
if ( result.stopValidation ) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Instantiable} from '../di'
|
||||
import {FormRequest} from './FormRequest'
|
||||
import {ValidationError} from './Validator'
|
||||
import {ResponseObject, RouteHandler} from "../http/routing/Route";
|
||||
import {Request} from "../http/lifecycle/Request";
|
||||
import {ResponseObject, RouteHandler} from '../http/routing/Route'
|
||||
import {Request} from '../http/lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Builds a middleware function that validates a request's input against
|
||||
|
@ -1,130 +1,150 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
|
||||
export namespace Arr {
|
||||
/** Requires the input value to be an array. */
|
||||
export function is(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( Array.isArray(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an array'
|
||||
}
|
||||
/** Requires the input value to be an array. */
|
||||
function is(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( Array.isArray(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
/** Requires the values in the input value array to be distinct. */
|
||||
export function distinct(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an array',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the values in the input value array to be distinct. */
|
||||
function distinct(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && (new Set(inputValue)).size === inputValue.length ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must not contain duplicate values',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
function includes(value: unknown): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( (new Set(inputValue)).size === inputValue.length ) {
|
||||
if ( Array.isArray(inputValue) && inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must not contain duplicate values'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function includes(value: any): ValidatorFunction {
|
||||
return function includes(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must include ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array NOT to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function excludes(value: any): ValidatorFunction {
|
||||
return function excludes(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( !inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must not include ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have exactly `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function length(len: number): ValidatorFunction {
|
||||
return function length(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at least `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMin(len: number): ValidatorFunction {
|
||||
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at most `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMax(len: number): ValidatorFunction {
|
||||
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) return arr
|
||||
|
||||
if ( inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`
|
||||
}
|
||||
message: `must include ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array NOT to contain the given value.
|
||||
* @param value
|
||||
*/
|
||||
function excludes(value: unknown): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && !inputValue.includes(value) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must not include ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have exactly `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function length(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at least `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMin(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input array to have at most `len` many entries.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMax(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
const arr = is(fieldName, inputValue)
|
||||
if ( !arr.valid ) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if ( Array.isArray(inputValue) && inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Arr = {
|
||||
is,
|
||||
distinct,
|
||||
includes,
|
||||
excludes,
|
||||
length,
|
||||
lengthMin,
|
||||
lengthMax,
|
||||
}
|
||||
|
@ -1,70 +1,80 @@
|
||||
import {infer as inferUtil} from '../../util'
|
||||
import {ValidationResult} from "./types";
|
||||
import {ValidationResult} from './types'
|
||||
|
||||
export namespace Cast {
|
||||
/** Attempt to infer the native type of a string value. */
|
||||
export function infer(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the input value to a boolean.
|
||||
* Note that this assumes the value may be boolish. The strings "true", "True",
|
||||
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
|
||||
* evaluate to `false`.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
export function boolean(fieldName: string, inputValue: any): ValidationResult {
|
||||
let castValue = !!inputValue
|
||||
|
||||
if ( ['true', 'True', 'TRUE', '1'].includes(inputValue) ) castValue = true
|
||||
if ( ['false', 'False', 'FALSE', '0'].includes(inputValue) ) castValue = false
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
castValue,
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a string. */
|
||||
export function string(fieldName: string, inputValue: any): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: String(inputValue),
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
|
||||
export function numeric(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseFloat(inputValue)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseFloat(inputValue)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to an integer. Fails otherwise. */
|
||||
export function integer(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseInt(inputValue)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseInt(inputValue)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
/** Attempt to infer the native type of a string value. */
|
||||
function infer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the input value to a boolean.
|
||||
* Note that this assumes the value may be boolish. The strings "true", "True",
|
||||
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
|
||||
* evaluate to `false`.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
let castValue = Boolean(inputValue)
|
||||
|
||||
if ( ['true', 'True', 'TRUE', '1'].includes(String(inputValue)) ) {
|
||||
castValue = true
|
||||
}
|
||||
if ( ['false', 'False', 'FALSE', '0'].includes(String(inputValue)) ) {
|
||||
castValue = false
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
castValue,
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a string. */
|
||||
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: String(inputValue),
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
|
||||
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseFloat(String(inputValue)),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Casts the input value to an integer. Fails otherwise. */
|
||||
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseInt(String(inputValue), 10)) ) {
|
||||
return {
|
||||
valid: true,
|
||||
castValue: parseInt(String(inputValue), 10),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
export const Cast = {
|
||||
infer,
|
||||
boolean,
|
||||
string,
|
||||
numeric,
|
||||
integer,
|
||||
}
|
||||
|
@ -1,197 +1,210 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
|
||||
export namespace Num {
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be greater than some value.
|
||||
* @param value
|
||||
*/
|
||||
export function greaterThan(value: number): ValidatorFunction {
|
||||
return function greaterThan(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue > value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be greater than ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at least some value.
|
||||
* @param value
|
||||
*/
|
||||
export function atLeast(value: number): ValidatorFunction {
|
||||
return function atLeast(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue >= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be less than some value.
|
||||
* @param value
|
||||
*/
|
||||
export function lessThan(value: number): ValidatorFunction {
|
||||
return function lessThan(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue < value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be less than ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at most some value.
|
||||
* @param value
|
||||
*/
|
||||
export function atMost(value: number): ValidatorFunction {
|
||||
return function atMost(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue <= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most ${value}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digits(num: number): ValidatorFunction {
|
||||
return function digits(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length === num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have exactly ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digitsMin(num: number): ValidatorFunction {
|
||||
return function digitsMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length >= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at least ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
export function digitsMax(num: number): ValidatorFunction {
|
||||
return function digitsMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).replace('.', '').length <= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at most ${num} digits`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to end with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
export function ends(num: number): ValidatorFunction {
|
||||
return function ends(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).endsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${num}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to begin with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
export function begins(num: number): ValidatorFunction {
|
||||
return function begins(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).startsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${num}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be a multiple of the given number.
|
||||
* @param num
|
||||
*/
|
||||
export function multipleOf(num: number): ValidatorFunction {
|
||||
return function multipleOf(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % num === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be a multiple of ${num}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be even. */
|
||||
export function even(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % 2 === 0 ) {
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be greater than some value.
|
||||
* @param value
|
||||
*/
|
||||
function greaterThan(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) > value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be even',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be odd. */
|
||||
export function odd(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be odd',
|
||||
message: `must be greater than ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at least some value.
|
||||
* @param value
|
||||
*/
|
||||
function atLeast(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) >= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be less than some value.
|
||||
* @param value
|
||||
*/
|
||||
function lessThan(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) < value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be less than ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be at most some value.
|
||||
* @param value
|
||||
*/
|
||||
function atMost(value: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( Number(inputValue) <= value ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most ${value}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digits(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length === num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have exactly ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digitsMin(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length >= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at least ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most `num` many digits.
|
||||
* @param num
|
||||
*/
|
||||
function digitsMax(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).replace('.', '').length <= num ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must have at most ${num} digits`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to end with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
function ends(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).endsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${num}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to begin with the given number sequence.
|
||||
* @param num
|
||||
*/
|
||||
function begins(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).startsWith(String(num)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${num}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to be a multiple of the given number.
|
||||
* @param num
|
||||
*/
|
||||
function multipleOf(num: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( parseFloat(String(inputValue)) % num === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be a multiple of ${num}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be even. */
|
||||
function even(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be even',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be odd. */
|
||||
function odd(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( parseFloat(String(inputValue)) % 2 === 0 ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be odd',
|
||||
}
|
||||
}
|
||||
|
||||
export const Num = {
|
||||
greaterThan,
|
||||
atLeast,
|
||||
lessThan,
|
||||
atMost,
|
||||
digits,
|
||||
digitsMin,
|
||||
digitsMax,
|
||||
ends,
|
||||
begins,
|
||||
multipleOf,
|
||||
even,
|
||||
odd,
|
||||
}
|
||||
|
@ -1,175 +1,191 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
import {UniversalPath} from '../../util'
|
||||
|
||||
export namespace Is {
|
||||
/** Requires the given input value to be some form of affirmative boolean. */
|
||||
export function accepted(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be accepted'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of boolean. */
|
||||
export function boolean(fieldName: string, inputValue: any): ValidationResult {
|
||||
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
|
||||
if ( boolish.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be true or false'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be of type string. */
|
||||
export function string(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue === 'string' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a string'
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be present and non-nullish. */
|
||||
export function required(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is required',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
export function present(fieldName: string, inputValue: any): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
export function filled(fieldName: string, inputValue: any): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Requires the given input value to be absent or nullish. */
|
||||
export function prohibited(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not allowed',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
export function absent(fieldName: string, inputValue: any): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
export function empty(fieldName: string, inputValue: any): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
export function foundIn(values: any[]): ValidatorFunction {
|
||||
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input NOT to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
export function notFoundIn(values: any[]): ValidatorFunction {
|
||||
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be number-like. */
|
||||
export function numeric(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseFloat(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be integer-like. */
|
||||
export function integer(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !isNaN(parseInt(inputValue)) && parseInt(inputValue) === parseFloat(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be a UniversalPath. */
|
||||
export function file(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue instanceof UniversalPath ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a file'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special validator function that marks a field as optional.
|
||||
* If the value of the field is nullish, no further validation rules will be applied.
|
||||
* If it is non-nullish, validation will continue.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
export function optional(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue ?? true ) {
|
||||
return {
|
||||
valid: true,
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of affirmative boolean. */
|
||||
function accepted(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be accepted',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be some form of boolean. */
|
||||
function boolean(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
|
||||
if ( boolish.includes(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be true or false',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be of type string. */
|
||||
function string(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue === 'string' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a string',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be present and non-nullish. */
|
||||
function required(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is required',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
function present(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of required(). */
|
||||
function filled(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return required(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Requires the given input value to be absent or nullish. */
|
||||
function prohibited(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not allowed',
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
function absent(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/** Alias of prohibited(). */
|
||||
function empty(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return prohibited(fieldName, inputValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
function foundIn(values: any[]): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the given input NOT to be found in an array of values.
|
||||
* @param values
|
||||
*/
|
||||
function notFoundIn(values: any[]): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( values.includes(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be one of: ${values.join(', ')}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be number-like. */
|
||||
function numeric(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseFloat(String(inputValue))) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be numeric',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be integer-like. */
|
||||
function integer(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( !isNaN(parseInt(String(inputValue), 10)) && parseInt(String(inputValue), 10) === parseFloat(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be an integer',
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the given input value to be a UniversalPath. */
|
||||
function file(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( inputValue instanceof UniversalPath ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be a file',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A special validator function that marks a field as optional.
|
||||
* If the value of the field is nullish, no further validation rules will be applied.
|
||||
* If it is non-nullish, validation will continue.
|
||||
* @param fieldName
|
||||
* @param inputValue
|
||||
*/
|
||||
function optional(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( inputValue ?? true ) {
|
||||
return {
|
||||
valid: true,
|
||||
stopValidation: true,
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
export const Is = {
|
||||
accepted,
|
||||
boolean,
|
||||
string,
|
||||
required,
|
||||
present,
|
||||
filled,
|
||||
prohibited,
|
||||
absent,
|
||||
empty,
|
||||
foundIn,
|
||||
notFoundIn,
|
||||
numeric,
|
||||
integer,
|
||||
file,
|
||||
optional,
|
||||
}
|
||||
|
@ -1,224 +1,245 @@
|
||||
import {ValidationResult, ValidatorFunction} from "./types";
|
||||
import {ValidationResult, ValidatorFunction} from './types'
|
||||
import {isJSON} from '../../util'
|
||||
|
||||
/**
|
||||
* String-related validation rules.
|
||||
*/
|
||||
export namespace Str {
|
||||
const regexes: {[key: string]: RegExp} = {
|
||||
'string.is.alpha': /[a-zA-Z]*/,
|
||||
'string.is.alpha_num': /[a-zA-Z0-9]*/,
|
||||
'string.is.alpha_dash': /[a-zA-Z\-]*/,
|
||||
'string.is.alpha_score': /[a-zA-Z_]*/,
|
||||
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
|
||||
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/,
|
||||
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
|
||||
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
|
||||
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
|
||||
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=+$,\w]+@)?[A-Za-z0-9.\-]+|(?:www\.|[\-;:&=+$,\w]+@)[A-Za-z0-9.\-]+)((?:\/[+~%\/.\w\-_]*)?\??(?:[\-+=&;%@.\w_]*)#?(?:[.!\/\\\w]*))?)/,
|
||||
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
||||
const regexes: {[key: string]: RegExp} = {
|
||||
'string.is.alpha': /[a-zA-Z]*/,
|
||||
'string.is.alpha_num': /[a-zA-Z0-9]*/,
|
||||
'string.is.alpha_dash': /[a-zA-Z-]*/,
|
||||
'string.is.alpha_score': /[a-zA-Z_]*/,
|
||||
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
|
||||
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/, // eslint-disable-line no-control-regex
|
||||
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
|
||||
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
|
||||
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
|
||||
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
|
||||
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w\-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/,
|
||||
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
|
||||
}
|
||||
|
||||
function validateRex(key: string, inputValue: unknown, message: string): ValidationResult {
|
||||
if ( regexes[key].test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
function validateRex(key: string, inputValue: any, message: string): ValidationResult {
|
||||
if ( regexes[key].test(inputValue) ) {
|
||||
return {
|
||||
valid: false,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters only. */
|
||||
function alpha(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphanumeric characters only. */
|
||||
function alphaNum(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "-" character only. */
|
||||
function alphaDash(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "_" character only. */
|
||||
function alphaScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
|
||||
function alphaNumDashScore(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC email address format. */
|
||||
function email(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.email', inputValue, 'must be an email address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 or IPv6 address. */
|
||||
function ip(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 address. */
|
||||
function ipv4(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv6 address. */
|
||||
function ipv6(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid file MIME type. */
|
||||
function mime(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC URL format. */
|
||||
function url(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.url', inputValue, 'must be a valid URL')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC UUID format. */
|
||||
function uuid(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input value to match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
function regex(rex: RegExp): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( rex.test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters only. */
|
||||
export function alpha(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphanumeric characters only. */
|
||||
export function alphaNum(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "-" character only. */
|
||||
export function alphaDash(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters or the "_" character only. */
|
||||
export function alphaScore(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
|
||||
export function alphaNumDashScore(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC email address format. */
|
||||
export function email(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.email', inputValue, 'must be an email address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 or IPv6 address. */
|
||||
export function ip(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv4 address. */
|
||||
export function ipv4(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid IPv6 address. */
|
||||
export function ipv6(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid file MIME type. */
|
||||
export function mime(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC URL format. */
|
||||
export function url(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.url', inputValue, 'must be a valid URL')
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid RFC UUID format. */
|
||||
export function uuid(fieldName: string, inputValue: any): ValidationResult {
|
||||
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input value to match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
export function regex(rex: RegExp): ValidatorFunction {
|
||||
return function regex(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( rex.test(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input to NOT match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
export function notRegex(rex: RegExp): ValidatorFunction {
|
||||
return function notRegex(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( !rex.test(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to end with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
export function ends(substr: string): ValidatorFunction {
|
||||
return function ends(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).endsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${substr}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to begin with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
export function begins(substr: string): ValidatorFunction {
|
||||
return function begins(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( String(inputValue).startsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${substr}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid JSON string. */
|
||||
export function json(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( isJSON(inputValue) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be valid JSON'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function length(len: number): ValidatorFunction {
|
||||
return function length(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMin(len: number): ValidatorFunction {
|
||||
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most len many characters.
|
||||
* @param len
|
||||
*/
|
||||
export function lengthMax(len: number): ValidatorFunction {
|
||||
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
|
||||
if ( inputValue.length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`
|
||||
}
|
||||
message: 'is not valid',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the input to NOT match the given regex.
|
||||
* @param rex
|
||||
*/
|
||||
function notRegex(rex: RegExp): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( !rex.test(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'is not valid',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to end with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
function ends(substr: string): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).endsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must end with "${substr}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validation function that requires the given input to begin with the substring.
|
||||
* @param substr
|
||||
*/
|
||||
function begins(substr: string): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).startsWith(substr) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must begin with "${substr}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires the input value to be a valid JSON string. */
|
||||
function json(fieldName: string, inputValue: unknown): ValidationResult {
|
||||
if ( isJSON(String(inputValue)) ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: 'must be valid JSON',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have exactly len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function length(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length === len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be exactly of length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at least len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMin(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length >= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at least length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a validator function that requires the input value to have at most len many characters.
|
||||
* @param len
|
||||
*/
|
||||
function lengthMax(len: number): ValidatorFunction {
|
||||
return (fieldName: string, inputValue: unknown): ValidationResult => {
|
||||
if ( String(inputValue).length <= len ) {
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: `must be at most length ${len}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Str = {
|
||||
alpha,
|
||||
alphaNum,
|
||||
alphaDash,
|
||||
alphaScore,
|
||||
alphaNumDashScore,
|
||||
email,
|
||||
ip,
|
||||
ipv4,
|
||||
ipv6,
|
||||
mime,
|
||||
url,
|
||||
uuid,
|
||||
regex,
|
||||
notRegex,
|
||||
ends,
|
||||
begins,
|
||||
json,
|
||||
length,
|
||||
lengthMin,
|
||||
lengthMax,
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
import {UniversalPath} from '../../util'
|
||||
import {Template} from '../../cli'
|
||||
|
||||
const form_template: Template = {
|
||||
const templateForm: Template = {
|
||||
name: 'form',
|
||||
fileSuffix: '.form.ts',
|
||||
description: 'Create a new form request validator',
|
||||
baseAppPath: ['http', 'forms'],
|
||||
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
|
||||
return `import {FormRequest, ValidationRules, Rule} from '@extollo/forms'
|
||||
import {Injectable} from '@extollo/di'
|
||||
render(name: string) {
|
||||
return `import {Injectable, FormRequest, ValidationRules, Rule} from '@extollo/lib'
|
||||
|
||||
/**
|
||||
* ${name} object
|
||||
@ -40,7 +38,7 @@ export class ${name}FormRequest extends FormRequest<${name}Form> {
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export { form_template }
|
||||
export { templateForm }
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Singleton, Inject} from '../../di'
|
||||
import {CommandLine} from '../../cli'
|
||||
import {form_template} from '../templates/form'
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {templateForm} from '../templates/form'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
@Singleton()
|
||||
export class Forms extends Unit {
|
||||
@ -13,6 +13,6 @@ export class Forms extends Unit {
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public async up(): Promise<void> {
|
||||
this.cli.registerTemplate(form_template)
|
||||
this.cli.registerTemplate(templateForm)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {Request} from "./lifecycle/Request";
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {Request} from './lifecycle/Request'
|
||||
import {Container} from '../di'
|
||||
|
||||
/**
|
||||
* Base class for controllers that define methods that
|
||||
@ -7,10 +8,12 @@ import {Request} from "./lifecycle/Request";
|
||||
*/
|
||||
export class Controller extends AppClass {
|
||||
constructor(
|
||||
protected readonly request: Request
|
||||
) { super() }
|
||||
protected readonly request: Request,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected container() {
|
||||
protected container(): Container {
|
||||
return this.request
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from "../util"
|
||||
import {ErrorWithContext, HTTPStatus, HTTPMessage} from '../util'
|
||||
|
||||
/**
|
||||
* An error class that has an associated HTTP status.
|
||||
@ -9,7 +9,7 @@ import {ErrorWithContext, HTTPStatus, HTTPMessage} from "../util"
|
||||
export class HTTPError extends ErrorWithContext {
|
||||
constructor(
|
||||
public readonly status: HTTPStatus = 500,
|
||||
public readonly message: string = ''
|
||||
public readonly message: string = '',
|
||||
) {
|
||||
super('HTTP ERROR')
|
||||
this.message = message || HTTPMessage[status]
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {uninfer, infer, uuid_v4} from "../../util";
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {uninfer, infer, uuid4} from '../../util'
|
||||
|
||||
/**
|
||||
* Base type representing a parsed cookie.
|
||||
@ -61,7 +61,7 @@ export class HTTPCookieJar {
|
||||
* @param value
|
||||
* @param options
|
||||
*/
|
||||
set(name: string, value: any, options?: HTTPCookieOptions) {
|
||||
set(name: string, value: unknown, options?: HTTPCookieOptions): this {
|
||||
this.parsed[name] = {
|
||||
key: name,
|
||||
value,
|
||||
@ -69,14 +69,16 @@ export class HTTPCookieJar {
|
||||
exists: false,
|
||||
options,
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a cookie exists with the given name.
|
||||
* @param name
|
||||
*/
|
||||
has(name: string) {
|
||||
return !!this.parsed[name]
|
||||
has(name: string): boolean {
|
||||
return Boolean(this.parsed[name])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,17 +90,21 @@ export class HTTPCookieJar {
|
||||
* @param name
|
||||
* @param options
|
||||
*/
|
||||
clear(name: string, options?: HTTPCookieOptions) {
|
||||
if ( !options ) options = {}
|
||||
clear(name: string, options?: HTTPCookieOptions): this {
|
||||
if ( !options ) {
|
||||
options = {}
|
||||
}
|
||||
options.expires = new Date(0)
|
||||
|
||||
this.parsed[name] = {
|
||||
key: name,
|
||||
value: undefined,
|
||||
originalValue: uuid_v4(),
|
||||
originalValue: uuid4(),
|
||||
exists: false,
|
||||
options,
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,10 +114,14 @@ export class HTTPCookieJar {
|
||||
const headers: string[] = []
|
||||
|
||||
for ( const key in this.parsed ) {
|
||||
if ( !this.parsed.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.parsed, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const cookie = this.parsed[key]
|
||||
if ( cookie.exists ) continue
|
||||
if ( cookie.exists ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parts = []
|
||||
parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`)
|
||||
@ -144,7 +154,7 @@ export class HTTPCookieJar {
|
||||
const map = {
|
||||
strict: 'Strict',
|
||||
lax: 'Lax',
|
||||
'none-secure': 'None; Secure'
|
||||
'none-secure': 'None; Secure',
|
||||
}
|
||||
|
||||
parts.push(map[cookie.options.sameSite])
|
||||
@ -163,7 +173,9 @@ export class HTTPCookieJar {
|
||||
const parts = cookie.split('=')
|
||||
|
||||
const key = parts.shift()?.trim()
|
||||
if ( !key ) return;
|
||||
if ( !key ) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = decodeURI(parts.join('='))
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Inject, Instantiable, Singleton} from "../../di"
|
||||
import {Collection, HTTPStatus} from "../../util"
|
||||
import {HTTPKernelModule} from "./HTTPKernelModule";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {error} from "../response/ErrorResponseFactory";
|
||||
import {Inject, Instantiable, Singleton} from '../../di'
|
||||
import {Collection, HTTPStatus} from '../../util'
|
||||
import {HTTPKernelModule} from './HTTPKernelModule'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {error} from '../response/ErrorResponseFactory'
|
||||
|
||||
/**
|
||||
* Interface for fluently registering kernel modules into the kernel.
|
||||
@ -105,7 +105,8 @@ export class HTTPKernel extends AppClass {
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.logging.error(e)
|
||||
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR).write(request)
|
||||
await error(e).status(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
.write(request)
|
||||
}
|
||||
|
||||
this.logging.verbose('Finished kernel lifecycle')
|
||||
@ -127,16 +128,16 @@ export class HTTPKernel extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(found_index, this.app().make(module))
|
||||
let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(foundIdx, this.app().make(module))
|
||||
return this
|
||||
} else {
|
||||
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
foundIdx = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
}
|
||||
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(found_index, this.app().make(module))
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(foundIdx, this.app().make(module))
|
||||
} else {
|
||||
throw new KernelModuleNotFoundError(other.name)
|
||||
}
|
||||
@ -149,16 +150,16 @@ export class HTTPKernel extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(found_index + 1, this.app().make(module))
|
||||
let foundIdx = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.preflight = this.preflight.put(foundIdx + 1, this.app().make(module))
|
||||
return this
|
||||
} else {
|
||||
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
foundIdx = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||
}
|
||||
|
||||
if ( typeof found_index !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(found_index + 1, this.app().make(module))
|
||||
if ( typeof foundIdx !== 'undefined' ) {
|
||||
this.postflight = this.postflight.put(foundIdx + 1, this.app().make(module))
|
||||
} else {
|
||||
throw new KernelModuleNotFoundError(other.name)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Injectable} from "../../di";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {HTTPKernel} from "./HTTPKernel";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {Injectable} from '../../di'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {HTTPKernel} from './HTTPKernel'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Base class for modules that define logic that is applied to requests
|
||||
@ -23,7 +23,7 @@ export class HTTPKernelModule extends AppClass {
|
||||
* @param {Request} request
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
public async match(request: Request): Promise<boolean> {
|
||||
public async match(request: Request): Promise<boolean> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return true
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ export class HTTPKernelModule extends AppClass {
|
||||
* Register this module with the given HTTP kernel.
|
||||
* @param {HTTPKernel} kernel
|
||||
*/
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before()
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {plaintext} from "../../response/StringResponseFactory";
|
||||
import {ResponseFactory} from "../../response/ResponseFactory";
|
||||
import {json} from "../../response/JSONResponseFactory";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {plaintext} from '../../response/StringResponseFactory'
|
||||
import {ResponseFactory} from '../../response/ResponseFactory'
|
||||
import {json} from '../../response/JSONResponseFactory'
|
||||
|
||||
/**
|
||||
* Base class for HTTP kernel modules that apply some response from a route handler to the request.
|
||||
@ -15,7 +15,7 @@ export abstract class AbstractResolvedRouteHandlerHTTPModule extends HTTPKernelM
|
||||
* @param request
|
||||
* @protected
|
||||
*/
|
||||
protected async applyResponseObject(object: ResponseObject, request: Request) {
|
||||
protected async applyResponseObject(object: ResponseObject, request: Request): Promise<void> {
|
||||
if ( (typeof object === 'string') || (typeof object === 'number') ) {
|
||||
object = plaintext(String(object))
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {http} from "../../response/HTTPErrorResponseFactory";
|
||||
import {HTTPStatus} from "../../../util";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {http} from '../../response/HTTPErrorResponseFactory'
|
||||
import {HTTPStatus} from '../../../util'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that runs the handler for the request's route.
|
||||
@ -12,14 +12,14 @@ import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHan
|
||||
* In most cases, this is the controller method defined by the route.
|
||||
*/
|
||||
export class ExecuteResolvedRouteHandlerHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).core()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
let object: ResponseObject = await route.handler(request)
|
||||
const object: ResponseObject = await route.handler(request)
|
||||
|
||||
await this.applyResponseObject(object, request)
|
||||
} else {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
import {PersistSessionHTTPModule} from './PersistSessionHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that executes the postflight handlers for the route.
|
||||
@ -11,18 +11,18 @@ import {PersistSessionHTTPModule} from "./PersistSessionHTTPModule";
|
||||
* Usually, this is post middleware.
|
||||
*/
|
||||
export class ExecuteResolvedRoutePostflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before(PersistSessionHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const postflight = route.postflight
|
||||
|
||||
for ( const handler of postflight ) {
|
||||
const result: ResponseObject = await handler(request)
|
||||
if ( typeof result !== "undefined" ) {
|
||||
if ( typeof result !== 'undefined' ) {
|
||||
await this.applyResponseObject(result, request)
|
||||
request.response.blockingWriteback(true)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {MountActivatedRouteHTTPModule} from "./MountActivatedRouteHTTPModule";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {ResponseObject} from "../../routing/Route";
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHandlerHTTPModule";
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {MountActivatedRouteHTTPModule} from './MountActivatedRouteHTTPModule'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {ResponseObject} from '../../routing/Route'
|
||||
import {AbstractResolvedRouteHandlerHTTPModule} from './AbstractResolvedRouteHandlerHTTPModule'
|
||||
|
||||
/**
|
||||
* HTTP Kernel module that executes the preflight handlers for the route.
|
||||
@ -11,18 +11,18 @@ import {AbstractResolvedRouteHandlerHTTPModule} from "./AbstractResolvedRouteHan
|
||||
* Usually, this is the pre middleware.
|
||||
*/
|
||||
export class ExecuteResolvedRoutePreflightHTTPModule extends AbstractResolvedRouteHandlerHTTPModule {
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after(MountActivatedRouteHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.hasInstance(ActivatedRoute) ) {
|
||||
const route = <ActivatedRoute> request.make(ActivatedRoute)
|
||||
const preflight = route.preflight
|
||||
|
||||
for ( const handler of preflight ) {
|
||||
const result: ResponseObject = await handler(request)
|
||||
if ( typeof result !== "undefined" ) {
|
||||
if ( typeof result !== 'undefined' ) {
|
||||
await this.applyResponseObject(result, request)
|
||||
request.response.blockingWriteback(true)
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable} from "../../../di"
|
||||
import {ErrorWithContext} from "../../../util"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {SetSessionCookieHTTPModule} from "./SetSessionCookieHTTPModule";
|
||||
import {SessionFactory} from "../../session/SessionFactory";
|
||||
import {Session} from "../../session/Session";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable} from '../../../di'
|
||||
import {ErrorWithContext} from '../../../util'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {SetSessionCookieHTTPModule} from './SetSessionCookieHTTPModule'
|
||||
import {SessionFactory} from '../../session/SessionFactory'
|
||||
import {Session} from '../../session/Session'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that creates the session using the configured driver
|
||||
@ -15,11 +15,11 @@ import {Session} from "../../session/Session";
|
||||
export class InjectSessionHTTPModule extends HTTPKernelModule {
|
||||
public readonly executeWithBlockingWriteback = true
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after(SetSessionCookieHTTPModule)
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
request.registerFactory(new SessionFactory())
|
||||
|
||||
const session = <Session> request.make(Session)
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Injectable, Inject} from "../../../di"
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Routing} from "../../../service/Routing";
|
||||
import {ActivatedRoute} from "../../routing/ActivatedRoute";
|
||||
import {Logging} from "../../../service/Logging";
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Routing} from '../../../service/Routing'
|
||||
import {ActivatedRoute} from '../../routing/ActivatedRoute'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that tries to find a registered route matching the request's
|
||||
@ -20,7 +20,7 @@ export class MountActivatedRouteHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).before()
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule"
|
||||
import {HTTPKernel} from "../HTTPKernel"
|
||||
import * as Busboy from "busboy"
|
||||
import {Request} from "../../lifecycle/Request"
|
||||
import {infer, uuid_v4} from "../../../util"
|
||||
import {Files} from "../../../service/Files"
|
||||
import {Config} from "../../../service/Config"
|
||||
import {Logging} from "../../../service/Logging"
|
||||
import {Injectable, Inject, Container} from "../../../di"
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import * as Busboy from 'busboy'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {infer, uuid4} from '../../../util'
|
||||
import {Files} from '../../../service/Files'
|
||||
import {Config} from '../../../service/Config'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
import {Injectable, Inject, Container} from '../../../di'
|
||||
|
||||
@Injectable()
|
||||
export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
static register(kernel: HTTPKernel) {
|
||||
static register(kernel: HTTPKernel): void {
|
||||
const files = <Files> Container.getContainer().make(Files)
|
||||
const logging = <Logging> Container.getContainer().make(Logging)
|
||||
if ( !files.hasFilesystem() ) {
|
||||
@ -32,8 +32,11 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const contentType = request.getHeader('content-type')
|
||||
const contentTypes = (Array.isArray(contentType) ? contentType : [contentType])
|
||||
.filter(Boolean).map(x => x!.toLowerCase().split(';')[0])
|
||||
if ( !contentType ) return request
|
||||
.filter(Boolean).map(x => String(x).toLowerCase()
|
||||
.split(';')[0])
|
||||
if ( !contentType ) {
|
||||
return request
|
||||
}
|
||||
|
||||
if (
|
||||
contentTypes.includes('multipart/form-data')
|
||||
@ -65,7 +68,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
try {
|
||||
const body = JSON.parse(data)
|
||||
for ( const key in body ) {
|
||||
if ( !body.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(body, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
request.parsedInput[key] = body[key]
|
||||
}
|
||||
res()
|
||||
@ -94,8 +100,10 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
request.parsedInput[field] = infer(val)
|
||||
})
|
||||
|
||||
busboy.on('file', async (field, file, filename, encoding, mimetype) => {
|
||||
if ( !this.files.hasFilesystem() ) return
|
||||
busboy.on('file', async (field, file, filename, encoding, mimetype) => { // eslint-disable-line max-params
|
||||
if ( !this.files.hasFilesystem() ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( !config?.enable ) {
|
||||
this.logging.warn(`Skipping uploaded file '${filename}' because uploading is disabled. Set the server.uploads.enable config to allow uploads.`)
|
||||
@ -122,7 +130,7 @@ export class ParseIncomingBodyHTTPModule extends HTTPKernelModule {
|
||||
}
|
||||
|
||||
const fs = this.files.getFilesystem()
|
||||
const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid_v4()}`
|
||||
const storePath = `${config.filesystemPrefix ? config.filesystemPrefix : ''}${(config.filesystemPrefix && !config.filesystemPrefix.endsWith('/')) ? '/' : ''}${field}-${uuid4()}`
|
||||
this.logging.verbose(`Uploading file in field ${field} to ${fs.getPrefix()}${storePath}`)
|
||||
file.pipe(await fs.putStoreFileAsStream({ storePath })) // FIXME might need to revisit this to ensure we don't res() before pipe finishes
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable} from "../../../di"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Session} from "../../session/Session";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Session} from '../../session/Session'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that runs after the main logic in the request to persist
|
||||
@ -12,7 +12,7 @@ import {Session} from "../../session/Session";
|
||||
export class PersistSessionHTTPModule extends HTTPKernelModule {
|
||||
public readonly executeWithBlockingWriteback = true
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).last()
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Injectable, Inject} from "../../../di"
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Config} from "../../../service/Config";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Config} from '../../../service/Config'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that sets the `X-Powered-By` header.
|
||||
@ -14,11 +14,11 @@ export class PoweredByHeaderInjectionHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly config!: Config;
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !this.config.get('server.poweredBy.hide', false) ) {
|
||||
request.response.setHeader('X-Powered-By', this.config.get('server.poweredBy.header', 'Extollo'))
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||
import {Injectable, Inject} from "../../../di";
|
||||
import {uuid_v4} from "../../../util";
|
||||
import {HTTPKernel} from "../HTTPKernel";
|
||||
import {Request} from "../../lifecycle/Request";
|
||||
import {Logging} from "../../../service/Logging";
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Injectable, Inject} from '../../../di'
|
||||
import {uuid4} from '../../../util'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Logging} from '../../../service/Logging'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware that tries to look up the session ID from the request.
|
||||
@ -16,13 +16,13 @@ export class SetSessionCookieHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).first()
|
||||
}
|
||||
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !request.cookies.has('extollo.session') ) {
|
||||
const session = `${uuid_v4()}-${uuid_v4()}`
|
||||
const session = `${uuid4()}-${uuid4()}`
|
||||
|
||||
this.logging.verbose(`Starting session: ${session}`)
|
||||
request.cookies.set('extollo.session', session)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {Injectable, ScopedContainer, Container} from "../../di"
|
||||
import {infer, UniversalPath} from "../../util"
|
||||
import {IncomingMessage, ServerResponse} from "http"
|
||||
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||
import {TLSSocket} from "tls";
|
||||
import * as url from "url";
|
||||
import {Response} from "./Response";
|
||||
import * as Negotiator from "negotiator";
|
||||
import {Injectable, ScopedContainer, Container} from '../../di'
|
||||
import {infer, UniversalPath} from '../../util'
|
||||
import {IncomingMessage, ServerResponse} from 'http'
|
||||
import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
|
||||
import {TLSSocket} from 'tls'
|
||||
import * as url from 'url'
|
||||
import {Response} from './Response'
|
||||
import * as Negotiator from 'negotiator'
|
||||
|
||||
/**
|
||||
* Enumeration of different HTTP verbs.
|
||||
@ -17,8 +17,8 @@ export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown'
|
||||
* Returns true if the given item is a valid HTTP verb.
|
||||
* @param what
|
||||
*/
|
||||
export function isHTTPMethod(what: any): what is HTTPMethod {
|
||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(what)
|
||||
export function isHTTPMethod(what: unknown): what is HTTPMethod {
|
||||
return ['post', 'get', 'patch', 'put', 'delete'].includes(String(what))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,7 +98,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
public readonly uploadedFiles: {[key: string]: UniversalPath} = {}
|
||||
|
||||
/** If true, the response lifecycle will not time out and send errors. */
|
||||
public bypassTimeout: boolean = false
|
||||
public bypassTimeout = false
|
||||
|
||||
constructor(
|
||||
/** The native Node.js request. */
|
||||
@ -109,7 +109,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
) {
|
||||
super(Container.getContainer())
|
||||
|
||||
this.secure = !!(clientRequest.connection as TLSSocket).encrypted
|
||||
this.secure = Boolean((clientRequest.connection as TLSSocket).encrypted)
|
||||
|
||||
this.cookies = new HTTPCookieJar(this)
|
||||
this.url = String(clientRequest.url)
|
||||
@ -137,6 +137,10 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
|
||||
const query: {[key: string]: any} = {}
|
||||
for ( const key in this.rawQueryData ) {
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.rawQueryData, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = this.rawQueryData[key]
|
||||
|
||||
if ( Array.isArray(value) ) {
|
||||
@ -151,12 +155,11 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
this.query = query
|
||||
this.isXHR = String(this.clientRequest.headers['x-requested-with']).toLowerCase() === 'xmlhttprequest'
|
||||
|
||||
// @ts-ignore
|
||||
const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address()
|
||||
const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address() as any
|
||||
this.address = {
|
||||
address,
|
||||
family,
|
||||
port
|
||||
port,
|
||||
}
|
||||
|
||||
this.mediaTypes = (new Negotiator(clientRequest)).mediaTypes()
|
||||
@ -164,12 +167,12 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
}
|
||||
|
||||
/** Get the value of a header, if it exists. */
|
||||
public getHeader(name: string) {
|
||||
public getHeader(name: string): string | string[] | undefined {
|
||||
return this.clientRequest.headers[name.toLowerCase()]
|
||||
}
|
||||
|
||||
/** Get the native Node.js IncomingMessage object. */
|
||||
public toNative() {
|
||||
public toNative(): IncomingMessage {
|
||||
return this.clientRequest
|
||||
}
|
||||
|
||||
@ -177,7 +180,7 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
* Get the value of an input field on the request. Spans multiple input sources.
|
||||
* @param key
|
||||
*/
|
||||
public input(key?: string) {
|
||||
public input(key?: string): unknown {
|
||||
if ( !key ) {
|
||||
return {
|
||||
...this.parsedInput,
|
||||
@ -206,17 +209,21 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
* Returns true if the request accepts the given media type.
|
||||
* @param type - a mimetype, or the short forms json, xml, or html
|
||||
*/
|
||||
accepts(type: string) {
|
||||
if ( type === 'json' ) type = 'application/json'
|
||||
else if ( type === 'xml' ) type = 'application/xml'
|
||||
else if ( type === 'html' ) type = 'text/html'
|
||||
accepts(type: string): boolean {
|
||||
if ( type === 'json' ) {
|
||||
type = 'application/json'
|
||||
} else if ( type === 'xml' ) {
|
||||
type = 'application/xml'
|
||||
} else if ( type === 'html' ) {
|
||||
type = 'text/html'
|
||||
}
|
||||
|
||||
type = type.toLowerCase()
|
||||
|
||||
const possible = [
|
||||
type,
|
||||
type.split('/')[0] + '/*',
|
||||
'*/*'
|
||||
'*/*',
|
||||
]
|
||||
|
||||
return this.mediaTypes.some(media => possible.includes(media.toLowerCase()))
|
||||
@ -230,9 +237,15 @@ export class Request extends ScopedContainer implements DataContainer {
|
||||
const xmlIdx = this.mediaTypes.indexOf('application/xml') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*')
|
||||
const htmlIdx = this.mediaTypes.indexOf('text/html') ?? this.mediaTypes.indexOf('text/*') ?? this.mediaTypes.indexOf('*/*')
|
||||
|
||||
if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) return 'html'
|
||||
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) return 'json'
|
||||
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) return 'xml'
|
||||
if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) {
|
||||
return 'html'
|
||||
}
|
||||
if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) {
|
||||
return 'json'
|
||||
}
|
||||
if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) {
|
||||
return 'xml'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {Request} from "./Request";
|
||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from "../../util"
|
||||
import {ServerResponse} from "http"
|
||||
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||
import {Request} from './Request'
|
||||
import {ErrorWithContext, HTTPStatus, BehaviorSubject} from '../../util'
|
||||
import {ServerResponse} from 'http'
|
||||
import {HTTPCookieJar} from '../kernel/HTTPCookieJar'
|
||||
|
||||
/**
|
||||
* Error thrown when the server tries to re-send headers after they have been sent once.
|
||||
*/
|
||||
export class HeadersAlreadySentError extends ErrorWithContext {
|
||||
constructor(response: Response, headerName?: string) {
|
||||
super(`Cannot modify or re-send headers for this request as they have already been sent.`);
|
||||
super(`Cannot modify or re-send headers for this request as they have already been sent.`)
|
||||
this.context = { headerName }
|
||||
}
|
||||
}
|
||||
@ -17,8 +17,8 @@ export class HeadersAlreadySentError extends ErrorWithContext {
|
||||
* Error thrown when the server tries to re-send a response that has already been sent.
|
||||
*/
|
||||
export class ResponseAlreadySentError extends ErrorWithContext {
|
||||
constructor(response: Response) {
|
||||
super(`Cannot modify or re-send response as it has already ended.`);
|
||||
constructor(public readonly response: Response) {
|
||||
super(`Cannot modify or re-send response as it has already ended.`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,13 +30,13 @@ export class Response {
|
||||
private headers: {[key: string]: string | string[]} = {}
|
||||
|
||||
/** True if the headers have been sent. */
|
||||
private _sentHeaders: boolean = false
|
||||
private sentHeaders = false
|
||||
|
||||
/** True if the response has been sent and closed. */
|
||||
private _responseEnded: boolean = false
|
||||
private responseEnded = false
|
||||
|
||||
/** The HTTP status code that should be sent to the client. */
|
||||
private _status: HTTPStatus = HTTPStatus.OK
|
||||
private status: HTTPStatus = HTTPStatus.OK
|
||||
|
||||
/**
|
||||
* If this is true, then some module in the kernel has flagged the response
|
||||
@ -44,10 +44,10 @@ export class Response {
|
||||
* the response.
|
||||
* @private
|
||||
*/
|
||||
private _blockingWriteback: boolean = false
|
||||
private isBlockingWriteback = false
|
||||
|
||||
/** The body contents that should be written to the response. */
|
||||
public body: string = ''
|
||||
public body = ''
|
||||
|
||||
/**
|
||||
* Behavior subject fired right before the response content is written.
|
||||
@ -68,14 +68,18 @@ export class Response {
|
||||
) { }
|
||||
|
||||
/** Get the currently set response status. */
|
||||
public getStatus() {
|
||||
return this._status
|
||||
public getStatus(): HTTPStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
/** Set a new response status. */
|
||||
public setStatus(status: HTTPStatus) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
|
||||
this._status = status
|
||||
public setStatus(status: HTTPStatus): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, 'status')
|
||||
}
|
||||
|
||||
this.status = status
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get the HTTPCookieJar for the client. */
|
||||
@ -89,8 +93,11 @@ export class Response {
|
||||
}
|
||||
|
||||
/** Set the value of the response header. */
|
||||
public setHeader(name: string, value: string | string[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
public setHeader(name: string, value: string | string[]): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, name)
|
||||
}
|
||||
|
||||
this.headers[name] = value
|
||||
return this
|
||||
}
|
||||
@ -99,9 +106,13 @@ export class Response {
|
||||
* Bulk set the specified headers in the response.
|
||||
* @param data
|
||||
*/
|
||||
public setHeaders(data: {[name: string]: string | string[]}) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
|
||||
this.headers = {...this.headers, ...data}
|
||||
public setHeaders(data: {[name: string]: string | string[]}): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this)
|
||||
}
|
||||
|
||||
this.headers = {...this.headers,
|
||||
...data}
|
||||
return this
|
||||
}
|
||||
|
||||
@ -110,67 +121,88 @@ export class Response {
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
public appendHeader(name: string, value: string | string[]) {
|
||||
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
|
||||
if ( !Array.isArray(value) ) value = [value]
|
||||
public appendHeader(name: string, value: string | string[]): this {
|
||||
if ( this.sentHeaders ) {
|
||||
throw new HeadersAlreadySentError(this, name)
|
||||
}
|
||||
if ( !Array.isArray(value) ) {
|
||||
value = [value]
|
||||
}
|
||||
let existing = this.headers[name] ?? []
|
||||
if ( !Array.isArray(existing) ) existing = [existing]
|
||||
if ( !Array.isArray(existing) ) {
|
||||
existing = [existing]
|
||||
}
|
||||
|
||||
existing = [...existing, ...value]
|
||||
if ( existing.length === 1 ) existing = existing[0]
|
||||
if ( existing.length === 1 ) {
|
||||
existing = existing[0]
|
||||
}
|
||||
|
||||
this.headers[name] = existing
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers to the client.
|
||||
*/
|
||||
public sendHeaders() {
|
||||
public sendHeaders(): this {
|
||||
const headers = {} as any
|
||||
|
||||
const setCookieHeaders = this.cookies.getSetCookieHeaders()
|
||||
if ( setCookieHeaders.length ) headers['Set-Cookie'] = setCookieHeaders
|
||||
if ( setCookieHeaders.length ) {
|
||||
headers['Set-Cookie'] = setCookieHeaders
|
||||
}
|
||||
|
||||
for ( const key in this.headers ) {
|
||||
if ( !this.headers.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(this.headers, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
headers[key] = this.headers[key]
|
||||
}
|
||||
|
||||
this.serverResponse.writeHead(this._status, headers)
|
||||
this._sentHeaders = true
|
||||
this.serverResponse.writeHead(this.status, headers)
|
||||
this.sentHeaders = true
|
||||
return this
|
||||
}
|
||||
|
||||
/** Returns true if the headers have been sent. */
|
||||
public hasSentHeaders() {
|
||||
return this._sentHeaders
|
||||
public hasSentHeaders(): boolean {
|
||||
return this.sentHeaders
|
||||
}
|
||||
|
||||
/** Returns true if a body has been set in the response. */
|
||||
public hasBody() {
|
||||
return !!this.body
|
||||
public hasBody(): boolean {
|
||||
return Boolean(this.body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the flag for whether the writeback should be blocked.
|
||||
* @param set - if this is specified, the value will be set.
|
||||
*/
|
||||
public blockingWriteback(set?: boolean) {
|
||||
public blockingWriteback(set?: boolean): boolean {
|
||||
if ( typeof set !== 'undefined' ) {
|
||||
this._blockingWriteback = set
|
||||
this.isBlockingWriteback = set
|
||||
}
|
||||
|
||||
return this._blockingWriteback
|
||||
return this.isBlockingWriteback
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the headers and specified data to the client.
|
||||
* @param data
|
||||
*/
|
||||
public async write(data: any) {
|
||||
public async write(data: unknown): Promise<void> {
|
||||
return new Promise<void>((res, rej) => {
|
||||
if ( !this._sentHeaders ) this.sendHeaders()
|
||||
if ( !this.sentHeaders ) {
|
||||
this.sendHeaders()
|
||||
}
|
||||
this.serverResponse.write(data, error => {
|
||||
if ( error ) rej(error)
|
||||
else res()
|
||||
if ( error ) {
|
||||
rej(error)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -178,7 +210,7 @@ export class Response {
|
||||
/**
|
||||
* Send the response to the client, writing the headers and configured body.
|
||||
*/
|
||||
public async send() {
|
||||
public async send(): Promise<void> {
|
||||
await this.sending$.next(this)
|
||||
this.setHeader('Content-Length', String(this.body?.length ?? 0))
|
||||
await this.write(this.body ?? '')
|
||||
@ -189,10 +221,14 @@ export class Response {
|
||||
/**
|
||||
* Mark the response as ended and close the socket.
|
||||
*/
|
||||
public end() {
|
||||
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
|
||||
this._sentHeaders = true
|
||||
public end(): this {
|
||||
if ( this.responseEnded ) {
|
||||
throw new ResponseAlreadySentError(this)
|
||||
}
|
||||
|
||||
this.sentHeaders = true
|
||||
this.serverResponse.end()
|
||||
return this
|
||||
}
|
||||
|
||||
// location?
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ResponseFactory} from "./ResponseFactory"
|
||||
import {Rehydratable} from "../../util"
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Rehydratable} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a DehydratedStateResponseFactory.
|
||||
@ -15,10 +15,12 @@ export function dehydrate(value: Rehydratable): DehydratedStateResponseFactory {
|
||||
*/
|
||||
export class DehydratedStateResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly rehydratable: Rehydratable
|
||||
) { super() }
|
||||
public readonly rehydratable: Rehydratable,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.body = JSON.stringify(this.rehydratable.dehydrate())
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
|
@ -1,21 +1,23 @@
|
||||
import {ResponseFactory} from "./ResponseFactory"
|
||||
import {ErrorWithContext, HTTPStatus} from "../../util"
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import * as api from "./api"
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {ErrorWithContext, HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import * as api from './api'
|
||||
|
||||
/**
|
||||
* Helper to create a new ErrorResponseFactory, with the given HTTP status and output format.
|
||||
* @param error
|
||||
* @param thrownError
|
||||
* @param status
|
||||
* @param output
|
||||
*/
|
||||
export function error(
|
||||
error: Error | string,
|
||||
thrownError: Error | string,
|
||||
status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
output: 'json' | 'html' | 'auto' = 'auto'
|
||||
output: 'json' | 'html' | 'auto' = 'auto',
|
||||
): ErrorResponseFactory {
|
||||
if ( typeof error === 'string' ) error = new Error(error)
|
||||
return new ErrorResponseFactory(error, status, output)
|
||||
if ( typeof thrownError === 'string' ) {
|
||||
thrownError = new Error(thrownError)
|
||||
}
|
||||
return new ErrorResponseFactory(thrownError, status, output)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -25,9 +27,9 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
protected targetMode: 'json' | 'html' | 'auto' = 'auto'
|
||||
|
||||
constructor(
|
||||
public readonly error: Error,
|
||||
public readonly thrownError: Error,
|
||||
status: HTTPStatus,
|
||||
output: 'json' | 'html' | 'auto' = 'auto'
|
||||
output: 'json' | 'html' | 'auto' = 'auto',
|
||||
) {
|
||||
super()
|
||||
this.status(status)
|
||||
@ -39,16 +41,16 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
return this
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
const wants = request.wants()
|
||||
|
||||
if ( this.targetMode === 'json' || (this.targetMode === 'auto' && wants === 'json') ) {
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
request.response.body = this.buildJSON(this.error)
|
||||
request.response.body = this.buildJSON(this.thrownError)
|
||||
} else if ( this.targetMode === 'html' || (this.targetMode === 'auto' && (wants === 'html' || wants === 'unknown')) ) {
|
||||
request.response.setHeader('Content-Type', 'text/html')
|
||||
request.response.body = this.buildHTML(this.error)
|
||||
request.response.body = this.buildHTML(this.thrownError)
|
||||
}
|
||||
|
||||
// FIXME XML support
|
||||
@ -61,12 +63,12 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
* @param {Error} error
|
||||
* @return string
|
||||
*/
|
||||
protected buildHTML(error: Error) {
|
||||
protected buildHTML(thrownError: Error): string {
|
||||
let context: any
|
||||
if ( error instanceof ErrorWithContext ) {
|
||||
context = error.context
|
||||
if ( error.originalError ) {
|
||||
error = error.originalError
|
||||
if ( thrownError instanceof ErrorWithContext ) {
|
||||
context = thrownError.context
|
||||
if ( thrownError.originalError ) {
|
||||
thrownError = thrownError.originalError
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,10 +76,11 @@ export class ErrorResponseFactory extends ResponseFactory {
|
||||
<b>Sorry, an unexpected error occurred while processing your request.</b>
|
||||
<br>
|
||||
<pre><code>
|
||||
Name: ${error.name}
|
||||
Message: ${error.message}
|
||||
Name: ${thrownError.name}
|
||||
Message: ${thrownError.message}
|
||||
Stack trace:
|
||||
- ${error.stack ? error.stack.split(/\s+at\s+/).slice(1).join('<br> - ') : 'none'}
|
||||
- ${thrownError.stack ? thrownError.stack.split(/\s+at\s+/).slice(1)
|
||||
.join('<br> - ') : 'none'}
|
||||
</code></pre>
|
||||
`
|
||||
|
||||
@ -85,7 +88,8 @@ Stack trace:
|
||||
str += `
|
||||
<pre><code>
|
||||
Context:
|
||||
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')}
|
||||
${Object.keys(context).map(key => ` - ${key} : ${context[key]}`)
|
||||
.join('\n')}
|
||||
</code></pre>
|
||||
`
|
||||
}
|
||||
@ -93,7 +97,7 @@ ${Object.keys(context).map(key => ` - ${key} : ${context[key]}`).join('\n')}
|
||||
return str
|
||||
}
|
||||
|
||||
protected buildJSON(error: Error) {
|
||||
return JSON.stringify(api.error(error))
|
||||
protected buildJSON(thrownError: Error): string {
|
||||
return JSON.stringify(api.error(thrownError))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a new HTMLResponseFactory.
|
||||
@ -15,9 +15,11 @@ export function html(value: string): HTMLResponseFactory {
|
||||
export class HTMLResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: string,
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
request.response.body = this.value
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ErrorResponseFactory} from "./ErrorResponseFactory";
|
||||
import {HTTPError} from "../HTTPError";
|
||||
import {HTTPStatus} from "../../util"
|
||||
import {ErrorResponseFactory} from './ErrorResponseFactory'
|
||||
import {HTTPError} from '../HTTPError'
|
||||
import {HTTPStatus} from '../../util'
|
||||
|
||||
/**
|
||||
* Helper that generates a new HTTPErrorResponseFactory given the HTTP status and message.
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function to create a new JSONResponseFactory of the given value.
|
||||
* @param value
|
||||
*/
|
||||
export function json(value: any): JSONResponseFactory {
|
||||
export function json(value: unknown): JSONResponseFactory {
|
||||
return new JSONResponseFactory(value)
|
||||
}
|
||||
|
||||
@ -14,10 +14,12 @@ export function json(value: any): JSONResponseFactory {
|
||||
*/
|
||||
export class JSONResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
public readonly value: any
|
||||
) { super() }
|
||||
public readonly value: unknown,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'application/json')
|
||||
request.response.body = JSON.stringify(this.value)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {HTTPStatus} from "../../util"
|
||||
import {Request} from "../lifecycle/Request"
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Abstract class that defines "factory" that knows how to write a particular
|
||||
@ -19,7 +19,7 @@ export abstract class ResponseFactory {
|
||||
}
|
||||
|
||||
/** Set the target status of this factory. */
|
||||
public status(status: HTTPStatus) {
|
||||
public status(status: HTTPStatus): this {
|
||||
this.targetStatus = status
|
||||
return this
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function that creates a new StringResponseFactory for the given string value.
|
||||
@ -16,9 +16,11 @@ export class StringResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
/** The string to write as the body. */
|
||||
public readonly value: string,
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Content-Type', 'text/plain')
|
||||
request.response.body = this.value
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {HTTPStatus} from "../../util";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {HTTPStatus} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Helper function to create a new TemporaryRedirectResponseFactory to the given destination.
|
||||
@ -18,10 +18,12 @@ export class TemporaryRedirectResponseFactory extends ResponseFactory {
|
||||
|
||||
constructor(
|
||||
/** THe URL where the client should redirect to. */
|
||||
public readonly destination: string
|
||||
) { super() }
|
||||
public readonly destination: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
request.response.setHeader('Location', this.destination)
|
||||
return request
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Container} from "../../di";
|
||||
import {ResponseFactory} from "./ResponseFactory";
|
||||
import {Request} from "../lifecycle/Request";
|
||||
import {ViewEngine} from "../../views/ViewEngine";
|
||||
import {Container} from '../../di'
|
||||
import {ResponseFactory} from './ResponseFactory'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {ViewEngine} from '../../views/ViewEngine'
|
||||
|
||||
/**
|
||||
* Helper function that creates a new ViewResponseFactory to render the given view
|
||||
@ -22,10 +22,12 @@ export class ViewResponseFactory extends ResponseFactory {
|
||||
/** The name of the view to render. */
|
||||
public readonly viewName: string,
|
||||
/** Optional data that should be passed to the view engine as params. */
|
||||
public readonly data?: {[key: string]: any}
|
||||
) { super() }
|
||||
public readonly data?: {[key: string]: any},
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public async write(request: Request) {
|
||||
public async write(request: Request): Promise<Request> {
|
||||
const viewEngine = <ViewEngine> Container.getContainer().make(ViewEngine)
|
||||
request.response.body = await viewEngine.renderByName(this.viewName, this.data || {})
|
||||
request.response.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||||
|
@ -14,13 +14,13 @@ export interface APIResponse {
|
||||
|
||||
/**
|
||||
* Formats a mesage as a successful API response.
|
||||
* @param {string} message
|
||||
* @param {string} displayMessage
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function message(message: string): APIResponse {
|
||||
export function message(displayMessage: string): APIResponse {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
message: displayMessage,
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export function message(message: string): APIResponse {
|
||||
* @param record
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function one(record: any): APIResponse {
|
||||
export function one(record: unknown): APIResponse {
|
||||
return {
|
||||
success: true,
|
||||
data: record,
|
||||
@ -53,23 +53,23 @@ export function many(records: any[]): APIResponse {
|
||||
|
||||
/**
|
||||
* Formats an error message or Error instance as an API response.
|
||||
* @param {string|Error} error
|
||||
* @return APIResponse
|
||||
* @param thrownError
|
||||
*/
|
||||
export function error(error: string | Error): APIResponse {
|
||||
if ( typeof error === 'string' ) {
|
||||
export function error(thrownError: string | Error): APIResponse {
|
||||
if ( typeof thrownError === 'string' ) {
|
||||
return {
|
||||
success: false,
|
||||
message: error,
|
||||
message: thrownError,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
message: thrownError.message,
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack ? error.stack.split(/\s+at\s+/).slice(1) : [],
|
||||
name: thrownError.name,
|
||||
message: thrownError.message,
|
||||
stack: thrownError.stack ? thrownError.stack.split(/\s+at\s+/).slice(1) : [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {ErrorWithContext} from "../../util";
|
||||
import {ResolvedRouteHandler, Route} from "./Route";
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {ResolvedRouteHandler, Route} from './Route'
|
||||
|
||||
/**
|
||||
* Class representing a resolved route that a request is mounted to.
|
||||
@ -42,7 +42,7 @@ export class ActivatedRoute {
|
||||
public readonly route: Route,
|
||||
|
||||
/** The request path that activated that route. */
|
||||
public readonly path: string
|
||||
public readonly path: string,
|
||||
) {
|
||||
const params = route.extract(path)
|
||||
if ( !params ) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {AppClass} from "../../lifecycle/AppClass"
|
||||
import {Request} from "../lifecycle/Request"
|
||||
import {ResponseObject} from "./Route"
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
import {ResponseObject} from './Route'
|
||||
import {Container} from '../../di'
|
||||
|
||||
/**
|
||||
* Base class representing a middleware handler that can be applied to routes.
|
||||
@ -8,10 +9,12 @@ import {ResponseObject} from "./Route"
|
||||
export abstract class Middleware extends AppClass {
|
||||
constructor(
|
||||
/** The request that will be handled by this middleware. */
|
||||
protected readonly request: Request
|
||||
) { super() }
|
||||
protected readonly request: Request,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected container() {
|
||||
protected container(): Container {
|
||||
return this.request
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {HTTPMethod, Request} from "../lifecycle/Request";
|
||||
import {Application} from "../../lifecycle/Application";
|
||||
import {RouteGroup} from "./RouteGroup";
|
||||
import {ResponseFactory} from "../response/ResponseFactory";
|
||||
import {Response} from "../lifecycle/Response";
|
||||
import {Controllers} from "../../service/Controllers";
|
||||
import {ErrorWithContext, Collection} from "../../util";
|
||||
import {Container} from "../../di";
|
||||
import {Controller} from "../Controller";
|
||||
import {Middlewares} from "../../service/Middlewares";
|
||||
import {Middleware} from "./Middleware";
|
||||
import {Config} from "../../service/Config";
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||
import {Application} from '../../lifecycle/Application'
|
||||
import {RouteGroup} from './RouteGroup'
|
||||
import {ResponseFactory} from '../response/ResponseFactory'
|
||||
import {Response} from '../lifecycle/Response'
|
||||
import {Controllers} from '../../service/Controllers'
|
||||
import {ErrorWithContext, Collection} from '../../util'
|
||||
import {Container} from '../../di'
|
||||
import {Controller} from '../Controller'
|
||||
import {Middlewares} from '../../service/Middlewares'
|
||||
import {Middleware} from './Middleware'
|
||||
import {Config} from '../../service/Config'
|
||||
|
||||
/**
|
||||
* Type alias for an item that is a valid response object, or lack thereof.
|
||||
@ -61,7 +61,7 @@ export class Route extends AppClass {
|
||||
private static compiledGroupStack: RouteGroup[] = []
|
||||
|
||||
/** Register a route group handler. */
|
||||
public static registerGroup(group: RouteGroup) {
|
||||
public static registerGroup(group: RouteGroup): void {
|
||||
this.registeredGroups.push(group)
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ export class Route extends AppClass {
|
||||
* @param definition
|
||||
* @param handler
|
||||
*/
|
||||
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler) {
|
||||
public static endpoint(method: HTTPMethod | HTTPMethod[], definition: string, handler: RouteHandler): Route {
|
||||
const route = new Route(method, handler, definition)
|
||||
this.registeredRoutes.push(route)
|
||||
return route
|
||||
@ -163,53 +163,53 @@ export class Route extends AppClass {
|
||||
* @param definition
|
||||
* @param handler
|
||||
*/
|
||||
public static get(definition: string, handler: RouteHandler) {
|
||||
public static get(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('get', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new POST route on the given endpoint. */
|
||||
public static post(definition: string, handler: RouteHandler) {
|
||||
public static post(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('post', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new PUT route on the given endpoint. */
|
||||
public static put(definition: string, handler: RouteHandler) {
|
||||
public static put(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('put', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new PATCH route on the given endpoint. */
|
||||
public static patch(definition: string, handler: RouteHandler) {
|
||||
public static patch(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('patch', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new DELETE route on the given endpoint. */
|
||||
public static delete(definition: string, handler: RouteHandler) {
|
||||
public static delete(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint('delete', definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new route on all HTTP verbs, on the given endpoint. */
|
||||
public static any(definition: string, handler: RouteHandler) {
|
||||
public static any(definition: string, handler: RouteHandler): Route {
|
||||
return this.endpoint(['get', 'put', 'patch', 'post', 'delete'], definition, handler)
|
||||
}
|
||||
|
||||
/** Create a new route group with the given prefix. */
|
||||
public static group(prefix: string, group: () => void | Promise<void>) {
|
||||
public static group(prefix: string, group: () => void | Promise<void>): RouteGroup {
|
||||
const grp = <RouteGroup> Application.getApplication().make(RouteGroup, group, prefix)
|
||||
this.registeredGroups.push(grp)
|
||||
return grp
|
||||
}
|
||||
|
||||
/** Middlewares that should be applied to this route. */
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>()
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: 'pre' | 'post'; handler: RouteHandler}>()
|
||||
|
||||
/** Pre-compiled route handlers for the pre-middleware for this route. */
|
||||
protected _compiledPreflight?: ResolvedRouteHandler[]
|
||||
protected compiledPreflight?: ResolvedRouteHandler[]
|
||||
|
||||
/** Pre-compiled route handlers for the post-middleware for this route. */
|
||||
protected _compiledHandler?: ResolvedRouteHandler
|
||||
protected compiledHandler?: ResolvedRouteHandler
|
||||
|
||||
/** Pre-compiled route handler for the main route handler for this route. */
|
||||
protected _compiledPostflight?: ResolvedRouteHandler[]
|
||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
||||
|
||||
constructor(
|
||||
/** The HTTP method(s) that this route listens on. */
|
||||
@ -219,8 +219,10 @@ export class Route extends AppClass {
|
||||
protected readonly handler: RouteHandler,
|
||||
|
||||
/** The route path this route listens on. */
|
||||
protected route: string
|
||||
) { super() }
|
||||
protected route: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this route matches the given HTTP verb and request path.
|
||||
@ -228,10 +230,13 @@ export class Route extends AppClass {
|
||||
* @param potential
|
||||
*/
|
||||
public match(method: HTTPMethod, potential: string): boolean {
|
||||
if ( Array.isArray(this.method) && !this.method.includes(method) ) return false
|
||||
else if ( !Array.isArray(this.method) && this.method !== method ) return false
|
||||
if ( Array.isArray(this.method) && !this.method.includes(method) ) {
|
||||
return false
|
||||
} else if ( !Array.isArray(this.method) && this.method !== method ) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!this.extract(potential)
|
||||
return Boolean(this.extract(potential))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -279,7 +284,9 @@ export class Route extends AppClass {
|
||||
|
||||
// If we got here, we didn't find a **
|
||||
// So, if the lengths are different, fail
|
||||
if ( routeParts.length !== potentialParts.length ) return
|
||||
if ( routeParts.length !== potentialParts.length ) {
|
||||
return
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
@ -287,47 +294,47 @@ export class Route extends AppClass {
|
||||
* Try to pre-compile and return the preflight handlers for this route.
|
||||
*/
|
||||
public resolvePreflight(): ResolvedRouteHandler[] {
|
||||
if ( !this._compiledPreflight ) {
|
||||
this._compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
||||
if ( !this.compiledPreflight ) {
|
||||
this.compiledPreflight = this.resolveMiddlewareHandlersForStage('pre')
|
||||
}
|
||||
|
||||
return this._compiledPreflight
|
||||
return this.compiledPreflight
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the postflight handlers for this route.
|
||||
*/
|
||||
public resolvePostflight(): ResolvedRouteHandler[] {
|
||||
if ( !this._compiledPostflight ) {
|
||||
this._compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
||||
if ( !this.compiledPostflight ) {
|
||||
this.compiledPostflight = this.resolveMiddlewareHandlersForStage('post')
|
||||
}
|
||||
|
||||
return this._compiledPostflight
|
||||
return this.compiledPostflight
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to pre-compile and return the main handler for this route.
|
||||
*/
|
||||
public resolveHandler(): ResolvedRouteHandler {
|
||||
if ( !this._compiledHandler ) {
|
||||
this._compiledHandler = this._resolveHandler()
|
||||
if ( !this.compiledHandler ) {
|
||||
this.compiledHandler = this.compileResolvedHandler()
|
||||
}
|
||||
|
||||
return this._compiledHandler
|
||||
return this.compiledHandler
|
||||
}
|
||||
|
||||
/** Register the given middleware as a preflight handler for this route. */
|
||||
pre(middleware: RouteHandler) {
|
||||
pre(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
handler: middleware
|
||||
handler: middleware,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware as a postflight handler for this route. */
|
||||
post(middleware: RouteHandler) {
|
||||
post(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
handler: middleware,
|
||||
@ -337,19 +344,24 @@ export class Route extends AppClass {
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
private prepend(prefix: string) {
|
||||
if ( !prefix.endsWith('/') ) prefix = `${prefix}/`
|
||||
if ( this.route.startsWith('/') ) this.route = this.route.substring(1)
|
||||
private prepend(prefix: string): this {
|
||||
if ( !prefix.endsWith('/') ) {
|
||||
prefix = `${prefix}/`
|
||||
}
|
||||
if ( this.route.startsWith('/') ) {
|
||||
this.route = this.route.substring(1)
|
||||
}
|
||||
this.route = `${prefix}${this.route}`
|
||||
return this
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the beginning of the preflight handlers. */
|
||||
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||
private prependMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }): void {
|
||||
this.middlewares.prepend(def)
|
||||
}
|
||||
|
||||
/** Add the given middleware item to the end of the postflight handlers. */
|
||||
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }) {
|
||||
private appendMiddleware(def: { stage: 'pre' | 'post', handler: RouteHandler }): void {
|
||||
this.middlewares.push(def)
|
||||
}
|
||||
|
||||
@ -357,18 +369,18 @@ export class Route extends AppClass {
|
||||
* Resolve and return the route handler for this route.
|
||||
* @private
|
||||
*/
|
||||
private _resolveHandler(): ResolvedRouteHandler {
|
||||
if ( typeof this.handler !== 'string' ) {
|
||||
private compileResolvedHandler(): ResolvedRouteHandler {
|
||||
const handler = this.handler
|
||||
if ( typeof handler !== 'string' ) {
|
||||
return (request: Request) => {
|
||||
// @ts-ignore
|
||||
return this.handler(request, request.response)
|
||||
return handler(request, request.response)
|
||||
}
|
||||
} else {
|
||||
const parts = this.handler.split('.')
|
||||
const parts = handler.split('.')
|
||||
if ( parts.length < 2 ) {
|
||||
const e = new ErrorWithContext('Route handler does not specify a method name.')
|
||||
e.context = {
|
||||
handler: this.handler
|
||||
handler,
|
||||
}
|
||||
throw e
|
||||
}
|
||||
@ -380,7 +392,7 @@ export class Route extends AppClass {
|
||||
if ( !controllerClass ) {
|
||||
const e = new ErrorWithContext('Controller not found for route handler.')
|
||||
e.context = {
|
||||
handler: this.handler,
|
||||
handler,
|
||||
controllerName,
|
||||
methodName,
|
||||
}
|
||||
@ -406,13 +418,13 @@ export class Route extends AppClass {
|
||||
private resolveMiddlewareHandlersForStage(stage: 'pre' | 'post'): ResolvedRouteHandler[] {
|
||||
return this.middlewares.where('stage', '=', stage)
|
||||
.map<ResolvedRouteHandler>(def => {
|
||||
if ( typeof def.handler !== 'string' ) {
|
||||
const handler = def.handler
|
||||
if ( typeof handler !== 'string' ) {
|
||||
return (request: Request) => {
|
||||
// @ts-ignore
|
||||
return def.handler(request, request.response)
|
||||
return handler(request, request.response)
|
||||
}
|
||||
} else {
|
||||
const parts = def.handler.split('.')
|
||||
const parts = handler.split('.')
|
||||
if ( parts.length < 2 ) {
|
||||
parts.push('apply') // default middleware method name, if none provided
|
||||
}
|
||||
@ -424,7 +436,7 @@ export class Route extends AppClass {
|
||||
if ( !middlewareClass ) {
|
||||
const e = new ErrorWithContext('Middleware not found for route handler.')
|
||||
e.context = {
|
||||
handler: def.handler,
|
||||
handler,
|
||||
middlewareName,
|
||||
methodName,
|
||||
}
|
||||
@ -445,7 +457,7 @@ export class Route extends AppClass {
|
||||
}
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString() {
|
||||
toString(): string {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Collection, ErrorWithContext} from "../../util"
|
||||
import {AppClass} from "../../lifecycle/AppClass"
|
||||
import {RouteHandler} from "./Route"
|
||||
import {Container} from "../../di"
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Collection, ErrorWithContext} from '../../util'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {RouteHandler} from './Route'
|
||||
import {Container} from '../../di'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Class that defines a group of Routes in the application, with a prefix.
|
||||
@ -24,7 +24,7 @@ export class RouteGroup extends AppClass {
|
||||
* Array of middlewares that should apply to all routes in this group.
|
||||
* @protected
|
||||
*/
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: "pre" | "post"; handler: RouteHandler}>()
|
||||
protected middlewares: Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> = new Collection<{stage: 'pre' | 'post'; handler: RouteHandler}>()
|
||||
|
||||
/**
|
||||
* Get the current group nesting.
|
||||
@ -48,7 +48,7 @@ export class RouteGroup extends AppClass {
|
||||
* @param name
|
||||
* @param define
|
||||
*/
|
||||
public static named(name: string, define: () => void) {
|
||||
public static named(name: string, define: () => void): void {
|
||||
if ( this.namedGroups[name] ) {
|
||||
Container.getContainer()
|
||||
.make<Logging>(Logging)
|
||||
@ -69,7 +69,7 @@ export class RouteGroup extends AppClass {
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public static include(name: string) {
|
||||
public static include(name: string): void {
|
||||
if (!this.namedGroups[name]) {
|
||||
throw new ErrorWithContext(`No route group exists with name: ${name}`, {name})
|
||||
}
|
||||
@ -83,21 +83,23 @@ export class RouteGroup extends AppClass {
|
||||
public readonly group: () => void | Promise<void>,
|
||||
|
||||
/** The route prefix of this group. */
|
||||
public readonly prefix: string
|
||||
) { super() }
|
||||
public readonly prefix: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/** Register the given middleware to be applied before all routes in this group. */
|
||||
pre(middleware: RouteHandler) {
|
||||
pre(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'pre',
|
||||
handler: middleware
|
||||
handler: middleware,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Register the given middleware to be applied after all routes in this group. */
|
||||
post(middleware: RouteHandler) {
|
||||
post(middleware: RouteHandler): this {
|
||||
this.middlewares.push({
|
||||
stage: 'post',
|
||||
handler: middleware,
|
||||
@ -107,7 +109,7 @@ export class RouteGroup extends AppClass {
|
||||
}
|
||||
|
||||
/** Return the middlewares that apply to this group. */
|
||||
getGroupMiddlewareDefinitions() {
|
||||
getGroupMiddlewareDefinitions(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> {
|
||||
return this.middlewares
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from "./Session";
|
||||
import {Injectable} from "../../di";
|
||||
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from './Session'
|
||||
import {Injectable} from '../../di'
|
||||
|
||||
/**
|
||||
* Implementation of the session driver that stores session data in memory.
|
||||
@ -31,44 +31,66 @@ export class MemorySession extends Session {
|
||||
/** The associated data for this session. */
|
||||
protected data?: SessionData
|
||||
|
||||
constructor() { super() }
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public getKey(): string {
|
||||
if ( !this.sessionID ) throw new NoSessionKeyError()
|
||||
if ( !this.sessionID ) {
|
||||
throw new NoSessionKeyError()
|
||||
}
|
||||
return this.sessionID
|
||||
}
|
||||
|
||||
public setKey(key: string) {
|
||||
public setKey(key: string): void {
|
||||
this.sessionID = key
|
||||
}
|
||||
|
||||
public load() {
|
||||
if ( !this.sessionID ) throw new NoSessionKeyError()
|
||||
public load(): void {
|
||||
if ( !this.sessionID ) {
|
||||
throw new NoSessionKeyError()
|
||||
}
|
||||
|
||||
this.data = MemorySession.getSession(this.sessionID)
|
||||
}
|
||||
|
||||
public persist() {
|
||||
if ( !this.sessionID ) throw new NoSessionKeyError()
|
||||
if ( !this.data ) throw new SessionNotLoadedError()
|
||||
public persist(): void {
|
||||
if ( !this.sessionID ) {
|
||||
throw new NoSessionKeyError()
|
||||
}
|
||||
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
MemorySession.setSession(this.sessionID, this.data)
|
||||
}
|
||||
|
||||
public getData(): SessionData {
|
||||
if ( !this.data ) throw new SessionNotLoadedError()
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
return this.data
|
||||
}
|
||||
|
||||
public setData(data: SessionData) {
|
||||
public setData(data: SessionData): void {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
public get(key: string, fallback?: any): any {
|
||||
if ( !this.data ) throw new SessionNotLoadedError()
|
||||
public get(key: string, fallback?: unknown): any {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
return this.data?.[key] ?? fallback
|
||||
}
|
||||
|
||||
public set(key: string, value: any) {
|
||||
if ( !this.data ) throw new SessionNotLoadedError()
|
||||
public set(key: string, value: unknown): void {
|
||||
if ( !this.data ) {
|
||||
throw new SessionNotLoadedError()
|
||||
}
|
||||
|
||||
this.data[key] = value
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Injectable, Inject} from "../../di"
|
||||
import {ErrorWithContext} from "../../util"
|
||||
import {Request} from "../lifecycle/Request"
|
||||
import {Injectable, Inject} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {Request} from '../lifecycle/Request'
|
||||
|
||||
/**
|
||||
* Type alias describing some inflated session data.
|
||||
@ -53,8 +53,8 @@ export abstract class Session {
|
||||
public abstract setData(data: SessionData): void
|
||||
|
||||
/** Get a value from the session by key. */
|
||||
public abstract get(key: string, fallback?: any): any
|
||||
public abstract get(key: string, fallback?: unknown): any
|
||||
|
||||
/** Set a value in the session by key. */
|
||||
public abstract set(key: string, value: any): void
|
||||
public abstract set(key: string, value: unknown): void
|
||||
}
|
||||
|
@ -5,20 +5,21 @@ import {
|
||||
PropertyDependency,
|
||||
isInstantiable,
|
||||
DEPENDENCY_KEYS_METADATA_KEY,
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY
|
||||
} from "../../di"
|
||||
import {Collection, ErrorWithContext} from "../../util"
|
||||
import {MemorySession} from "./MemorySession";
|
||||
import {Session} from "./Session";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Config} from "../../service/Config";
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, Instantiable,
|
||||
} from '../../di'
|
||||
import {Collection, ErrorWithContext} from '../../util'
|
||||
import {MemorySession} from './MemorySession'
|
||||
import {Session} from './Session'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {Config} from '../../service/Config'
|
||||
|
||||
/**
|
||||
* A dependency injection factory that matches the abstract Session class
|
||||
* and produces an instance of the configured session driver implementation.
|
||||
*/
|
||||
export class SessionFactory extends AbstractFactory {
|
||||
export class SessionFactory extends AbstractFactory<Session> {
|
||||
protected readonly logging: Logging
|
||||
|
||||
protected readonly config: Config
|
||||
|
||||
/** True if we have printed the memory session warning at least once. */
|
||||
@ -30,17 +31,19 @@ export class SessionFactory extends AbstractFactory {
|
||||
this.config = Container.getContainer().make<Config>(Config)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): Session {
|
||||
return new (this.getSessionClass())
|
||||
produce(): Session {
|
||||
return new (this.getSessionClass())()
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
match(something: unknown): boolean {
|
||||
return something === Session
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.getSessionClass())
|
||||
if ( meta ) return meta
|
||||
if ( meta ) {
|
||||
return meta
|
||||
}
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
@ -50,7 +53,9 @@ export class SessionFactory extends AbstractFactory {
|
||||
|
||||
do {
|
||||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
|
||||
if ( loadedMeta ) meta.concat(loadedMeta)
|
||||
if ( loadedMeta ) {
|
||||
meta.concat(loadedMeta)
|
||||
}
|
||||
currentToken = Object.getPrototypeOf(currentToken)
|
||||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
|
||||
|
||||
@ -62,7 +67,7 @@ export class SessionFactory extends AbstractFactory {
|
||||
* @protected
|
||||
* @return Instantiable<Session>
|
||||
*/
|
||||
protected getSessionClass() {
|
||||
protected getSessionClass(): Instantiable<Session> {
|
||||
const SessionClass = this.config.get('server.session.driver', MemorySession)
|
||||
if ( SessionClass === MemorySession && !SessionFactory.loggedMemorySessionWarningOnce ) {
|
||||
this.logging.warn(`You are using the default memory-based session driver. It is recommended you configure a persistent session driver instead.`)
|
||||
@ -70,9 +75,9 @@ export class SessionFactory extends AbstractFactory {
|
||||
}
|
||||
|
||||
if ( !isInstantiable(SessionClass) || !(SessionClass.prototype instanceof Session) ) {
|
||||
const e = new ErrorWithContext('Provided session class does not extend from @extollo/lib.Session');
|
||||
const e = new ErrorWithContext('Provided session class does not extend from @extollo/lib.Session')
|
||||
e.context = {
|
||||
config_key: 'server.session.driver',
|
||||
configKey: 'server.session.driver',
|
||||
class: SessionClass.toString(),
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Request} from "../../http/lifecycle/Request";
|
||||
import {Injectable} from "../../di"
|
||||
import {Locale} from "../service/Locale";
|
||||
import {HTTPKernelModule} from "../../http/kernel/HTTPKernelModule";
|
||||
import {HTTPKernel} from "../../http/kernel/HTTPKernel";
|
||||
import {InjectSessionHTTPModule} from "../../http/kernel/module/InjectSessionHTTPModule";
|
||||
import {Session} from "../../http/session/Session";
|
||||
import {Request} from '../../http/lifecycle/Request'
|
||||
import {Injectable} from '../../di'
|
||||
import {Locale} from '../service/Locale'
|
||||
import {HTTPKernelModule} from '../../http/kernel/HTTPKernelModule'
|
||||
import {HTTPKernel} from '../../http/kernel/HTTPKernel'
|
||||
import {InjectSessionHTTPModule} from '../../http/kernel/module/InjectSessionHTTPModule'
|
||||
import {Session} from '../../http/session/Session'
|
||||
|
||||
/**
|
||||
* An HTTP kernel module that adds the Locale service to the request container.
|
||||
@ -14,7 +14,7 @@ export class InjectRequestLocale extends HTTPKernelModule {
|
||||
public executeWithBlockingWriteback = true
|
||||
|
||||
/** Register this kernel module to the given kernel. */
|
||||
public static register(kernel: HTTPKernel) {
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).after(InjectSessionHTTPModule)
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export class InjectRequestLocale extends HTTPKernelModule {
|
||||
* service into the Request container based on said locale.
|
||||
* @param request
|
||||
*/
|
||||
public async apply(request: Request) {
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const session = <Session> request.make(Session)
|
||||
const locale = <Locale> request.make(Locale)
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {Singleton, Inject} from "../../di"
|
||||
import {CommandLine} from "../../cli"
|
||||
import {InjectRequestLocale} from "../modules/InjectRequestLocale"
|
||||
import {locale_template} from "../template/locale"
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {HTTPKernel} from "../../http/kernel/HTTPKernel";
|
||||
import {Config} from "../../service/Config";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Singleton, Inject} from '../../di'
|
||||
import {CommandLine} from '../../cli'
|
||||
import {InjectRequestLocale} from '../modules/InjectRequestLocale'
|
||||
import {templateLocale} from '../template/locale'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {HTTPKernel} from '../../http/kernel/HTTPKernel'
|
||||
import {Config} from '../../service/Config'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Application unit to register @extollo/i18n resources.
|
||||
@ -30,9 +30,9 @@ export class Internationalization extends Unit {
|
||||
* You can set the "locale.enable" config property to `false` to disable
|
||||
* the InjectRequestLocale HTTP kernel module.
|
||||
*/
|
||||
up() {
|
||||
up(): void {
|
||||
this.logging.debug(`Registering locale template with CLI...`)
|
||||
this.cli.registerTemplate(locale_template)
|
||||
this.cli.registerTemplate(templateLocale)
|
||||
|
||||
if ( this.config.get('locale.enable', true) ) {
|
||||
this.kernel.register(InjectRequestLocale).before()
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Injectable} from "../../di"
|
||||
import {ErrorWithContext} from "../../util"
|
||||
import * as pluralize from "pluralize"
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {Config} from "../../service/Config";
|
||||
import {Injectable} from '../../di'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import * as pluralize from 'pluralize'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Config} from '../../service/Config'
|
||||
|
||||
/**
|
||||
* Type name for the standalone localization helper function that can be passed around
|
||||
@ -15,7 +15,7 @@ export type LocaleHelper = (phrase: string, { plural, fallback, interp }: {plura
|
||||
*/
|
||||
@Injectable()
|
||||
export class Locale extends AppClass {
|
||||
protected get config() {
|
||||
protected get config(): Config {
|
||||
// For some reason, was having issues with this not injecting properly.
|
||||
// TODO convert this back to @Inject() and solve that bug
|
||||
return this.app().make<Config>(Config)
|
||||
@ -27,7 +27,7 @@ export class Locale extends AppClass {
|
||||
* @example en_US means "lang:en_US" config scope
|
||||
* @example es_MX means "lang:es_MX" config scope
|
||||
*/
|
||||
protected locale?: string
|
||||
protected locale?: string,
|
||||
) {
|
||||
super()
|
||||
if ( !this.locale ) {
|
||||
@ -39,7 +39,7 @@ export class Locale extends AppClass {
|
||||
* Get the default locale that should be assigned if none is specified in the session.
|
||||
* @return string
|
||||
*/
|
||||
getDefaultLocale() {
|
||||
getDefaultLocale(): string {
|
||||
return this.config.get('locale.default', 'en_US')
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export class Locale extends AppClass {
|
||||
* Set the preferred locale for lookups by this service.
|
||||
* @param locale
|
||||
*/
|
||||
setLocale(locale: string) {
|
||||
setLocale(locale: string): void {
|
||||
this.locale = locale
|
||||
}
|
||||
|
||||
@ -56,7 +56,9 @@ export class Locale extends AppClass {
|
||||
*/
|
||||
helper(): LocaleHelper {
|
||||
return (phrase: string, { plural, fallback, interp }: {plural?: number, fallback?: string, interp?: {[key: string]: any}} = {}) => {
|
||||
return this.get(phrase, {plural, fallback, interp})
|
||||
return this.get(phrase, {plural,
|
||||
fallback,
|
||||
interp})
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,11 +133,14 @@ export class Locale extends AppClass {
|
||||
* @param interp
|
||||
*/
|
||||
get(phrase: string, { plural, fallback, interp }: {plural?: number, fallback?: string, interp?: {[key: string]: any}} = {}): string {
|
||||
const scope = phrase.split(':').reverse().slice(1).reverse().join(':')
|
||||
const scope = phrase.split(':').reverse()
|
||||
.slice(1)
|
||||
.reverse()
|
||||
.join(':')
|
||||
const specific = phrase.split(':').reverse()[0]
|
||||
const load = `${this.locale}${scope ? ':' + scope : ''}.${specific}`
|
||||
const translated = this.load(load)
|
||||
const is_plural = plural && plural !== 1
|
||||
const isPlural = plural && plural !== 1
|
||||
|
||||
if ( !translated ) {
|
||||
return fallback ?? specific
|
||||
@ -144,13 +149,13 @@ export class Locale extends AppClass {
|
||||
let ret = ''
|
||||
|
||||
if ( typeof translated === 'object' ) {
|
||||
if ( is_plural && translated.many ) {
|
||||
if ( isPlural && translated.many ) {
|
||||
ret = translated.many
|
||||
} else if ( is_plural && translated.one ) {
|
||||
} else if ( isPlural && translated.one ) {
|
||||
ret = pluralize(translated.one, plural)
|
||||
} else if ( !is_plural && translated.one ) {
|
||||
} else if ( !isPlural && translated.one ) {
|
||||
ret = translated.one
|
||||
} else if ( !is_plural && translated.many ) {
|
||||
} else if ( !isPlural && translated.many ) {
|
||||
ret = pluralize(translated.many, 1)
|
||||
} else {
|
||||
throw new ErrorWithContext(`Invalid translation config for ${phrase}. Must provide 'one' or 'many' keys.`, {
|
||||
@ -162,7 +167,7 @@ export class Locale extends AppClass {
|
||||
})
|
||||
}
|
||||
} else if ( typeof translated === 'string' ) {
|
||||
ret = pluralize(translated, is_plural ? 5 : 1)
|
||||
ret = pluralize(translated, isPlural ? 5 : 1)
|
||||
} else {
|
||||
throw new ErrorWithContext(`Invalid translation object for ${phrase}.`, {
|
||||
locale: this.locale,
|
||||
@ -175,6 +180,10 @@ export class Locale extends AppClass {
|
||||
|
||||
if ( interp ) {
|
||||
for ( const key in interp ) {
|
||||
if ( !Object.prototype.hasOwnProperty.call(interp, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const rex = new RegExp(`:${key}:`, 'g')
|
||||
ret = ret.replace(rex, interp[key])
|
||||
}
|
||||
@ -188,19 +197,32 @@ export class Locale extends AppClass {
|
||||
* @param locale
|
||||
* @protected
|
||||
*/
|
||||
protected load(locale: string) {
|
||||
const subloc = locale.split(':').slice(1).join(':')
|
||||
const specific_loc = locale.split(':').reverse()[0].split('.').slice(1).join('.')
|
||||
protected load(locale: string): {[key: string]: string} | string {
|
||||
const subloc = locale.split(':').slice(1)
|
||||
.join(':')
|
||||
const specificLocale = locale.split(':').reverse()[0].split('.').slice(1)
|
||||
.join('.')
|
||||
|
||||
let common: any = this.config.get(`lang:common${subloc ? ':' + subloc : ''}${specific_loc ? '.' + specific_loc : ''}`, undefined)
|
||||
let common: any = this.config.get(`lang:common${subloc ? ':' + subloc : ''}${specificLocale ? '.' + specificLocale : ''}`, undefined)
|
||||
let specific: any = this.config.get(`lang:${locale}`, undefined)
|
||||
|
||||
if ( typeof specific === 'string' ) return specific
|
||||
if ( typeof common === 'string' && typeof specific === 'undefined' ) return common
|
||||
if ( typeof specific === 'string' ) {
|
||||
return specific
|
||||
}
|
||||
|
||||
if ( !common ) common = {}
|
||||
if ( !specific ) specific = {}
|
||||
if ( typeof common === 'string' && typeof specific === 'undefined' ) {
|
||||
return common
|
||||
}
|
||||
|
||||
return {...common, ...specific}
|
||||
if ( !common ) {
|
||||
common = {}
|
||||
}
|
||||
|
||||
if ( !specific ) {
|
||||
specific = {}
|
||||
}
|
||||
|
||||
return {...common,
|
||||
...specific}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,21 @@
|
||||
import {Template} from "../../cli"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Container} from "../../di"
|
||||
import {Config} from "../../service/Config";
|
||||
import {Template} from '../../cli'
|
||||
import {Container} from '../../di'
|
||||
import {Config} from '../../service/Config'
|
||||
|
||||
/**
|
||||
* CLI template that generates a new locale file.
|
||||
* Automatically adds placeholder entries for phrases that exist in the
|
||||
* associated common locale file.
|
||||
*/
|
||||
const locale_template: Template = {
|
||||
const templateLocale: Template = {
|
||||
name: 'locale',
|
||||
fileSuffix: '.config.ts',
|
||||
description: 'Create a new config file that specifies translations for a locale.',
|
||||
baseAppPath: ['configs', 'lang'],
|
||||
render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => {
|
||||
render: (name: string, fullCanonicalName: string) => {
|
||||
const config = <Config> Container.getContainer().make(Config)
|
||||
const subloc = fullCanonicalName.split(':').slice(1).join(':')
|
||||
const subloc = fullCanonicalName.split(':').slice(1)
|
||||
.join(':')
|
||||
const common: any = config.get(`lang:common${subloc ? ':' + subloc : ''}`, {})
|
||||
|
||||
return `import {env} from '@extollo/lib'
|
||||
@ -27,4 +27,4 @@ ${Object.keys(common).map(key => ' ' + key + ': \'\',\n')}
|
||||
},
|
||||
}
|
||||
|
||||
export { locale_template }
|
||||
export { templateLocale }
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Application} from './Application';
|
||||
import {Container, DependencyKey} from "../di";
|
||||
import {Application} from './Application'
|
||||
import {Container, DependencyKey} from '../di'
|
||||
|
||||
/**
|
||||
* Base type for a class that supports binding methods by string.
|
||||
@ -13,12 +13,12 @@ export interface Bindable {
|
||||
* @param what
|
||||
* @return boolean
|
||||
*/
|
||||
export function isBindable(what: any): what is Bindable {
|
||||
export function isBindable(what: unknown): what is Bindable {
|
||||
return (
|
||||
what
|
||||
&& typeof what.getBoundMethod === 'function'
|
||||
&& what.getBoundMethod.length === 1
|
||||
&& typeof what.getBoundMethod('getBoundMethod') === 'function'
|
||||
Boolean(what)
|
||||
&& typeof (what as any).getBoundMethod === 'function'
|
||||
&& (what as any).getBoundMethod.length === 1
|
||||
&& typeof (what as any).getBoundMethod('getBoundMethod') === 'function'
|
||||
)
|
||||
}
|
||||
|
||||
@ -30,17 +30,17 @@ export class AppClass {
|
||||
private readonly appClassApplication!: Application;
|
||||
|
||||
constructor() {
|
||||
this.appClassApplication = Application.getApplication();
|
||||
this.appClassApplication = Application.getApplication()
|
||||
}
|
||||
|
||||
/** Get the global Application. */
|
||||
protected app(): Application {
|
||||
return this.appClassApplication;
|
||||
return this.appClassApplication
|
||||
}
|
||||
|
||||
/** Get the global Container. */
|
||||
protected container(): Container {
|
||||
return this.appClassApplication;
|
||||
return this.appClassApplication
|
||||
}
|
||||
|
||||
/** Call the `make()` method on the global container. */
|
||||
@ -54,14 +54,12 @@ export class AppClass {
|
||||
* @return function
|
||||
*/
|
||||
public getBoundMethod(methodName: string): (...args: any[]) => any {
|
||||
// @ts-ignore
|
||||
if ( typeof this[methodName] !== 'function' ) {
|
||||
if ( typeof (this as any)[methodName] !== 'function' ) {
|
||||
throw new TypeError(`Attempt to get bound method for non-function type: ${methodName}`)
|
||||
}
|
||||
|
||||
return (...args: any[]): any => {
|
||||
// @ts-ignore
|
||||
return this[methodName](...args)
|
||||
return (this as any)[methodName](...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Container} from '../di';
|
||||
import {Container} from '../di'
|
||||
import {
|
||||
ErrorWithContext,
|
||||
globalRegistry,
|
||||
@ -7,14 +7,14 @@ import {
|
||||
PathLike,
|
||||
StandardLogger,
|
||||
universalPath,
|
||||
UniversalPath
|
||||
} from '../util';
|
||||
UniversalPath,
|
||||
} from '../util'
|
||||
|
||||
import {Logging} from '../service/Logging';
|
||||
import {RunLevelErrorHandler} from "./RunLevelErrorHandler";
|
||||
import {Unit, UnitStatus} from "./Unit";
|
||||
import * as dotenv from 'dotenv';
|
||||
import {CacheFactory} from "../support/cache/CacheFactory";
|
||||
import {Logging} from '../service/Logging'
|
||||
import {RunLevelErrorHandler} from './RunLevelErrorHandler'
|
||||
import {Unit, UnitStatus} from './Unit'
|
||||
import * as dotenv from 'dotenv'
|
||||
import {CacheFactory} from '../support/cache/CacheFactory'
|
||||
|
||||
/**
|
||||
* Helper function that resolves and infers environment variable values.
|
||||
@ -24,7 +24,7 @@ import {CacheFactory} from "../support/cache/CacheFactory";
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
*/
|
||||
export function env(key: string, defaultValue?: any): any {
|
||||
export function env(key: string, defaultValue?: unknown): any {
|
||||
return Application.getApplication().env(key, defaultValue)
|
||||
}
|
||||
|
||||
@ -107,7 +107,7 @@ export class Application extends Container {
|
||||
* If true, the "Starting Extollo..." messages will always
|
||||
* be logged.
|
||||
*/
|
||||
public forceStartupMessage: boolean = true
|
||||
public forceStartupMessage = true
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@ -129,21 +129,21 @@ export class Application extends Container {
|
||||
* Returns true if the given unit class is registered with the application.
|
||||
* @param unitClass
|
||||
*/
|
||||
public hasUnit(unitClass: typeof Unit) {
|
||||
public hasUnit(unitClass: typeof Unit): boolean {
|
||||
return this.applicationUnits.includes(unitClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a UniversalPath to the root of the application.
|
||||
*/
|
||||
get root() {
|
||||
get root(): UniversalPath {
|
||||
return this.basePath.concat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a UniversalPath to the `app/` directory in the application.
|
||||
*/
|
||||
get appRoot() {
|
||||
get appRoot(): UniversalPath {
|
||||
return this.basePath.concat('app')
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ export class Application extends Container {
|
||||
* Resolve a path relative to the root of the application.
|
||||
* @param parts
|
||||
*/
|
||||
path(...parts: PathLike[]) {
|
||||
path(...parts: PathLike[]): UniversalPath {
|
||||
return this.basePath.concat(...parts)
|
||||
}
|
||||
|
||||
@ -159,14 +159,14 @@ export class Application extends Container {
|
||||
* Resolve a path relative to the `app/` directory in the application.
|
||||
* @param parts
|
||||
*/
|
||||
appPath(...parts: PathLike[]) {
|
||||
appPath(...parts: PathLike[]): UniversalPath {
|
||||
return this.basePath.concat('app', ...parts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the RunLevelErrorHandler.
|
||||
*/
|
||||
get errorHandler() {
|
||||
get errorHandler(): (e: Error) => void {
|
||||
const rleh: RunLevelErrorHandler = this.make<RunLevelErrorHandler>(RunLevelErrorHandler)
|
||||
return rleh.handle
|
||||
}
|
||||
@ -186,7 +186,7 @@ export class Application extends Container {
|
||||
* @param absolutePathToApplicationRoot
|
||||
* @param applicationUnits
|
||||
*/
|
||||
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]) {
|
||||
scaffold(absolutePathToApplicationRoot: string, applicationUnits: (typeof Unit)[]): void {
|
||||
this.baseDir = absolutePathToApplicationRoot
|
||||
this.basePath = universalPath(absolutePathToApplicationRoot)
|
||||
this.applicationUnits = applicationUnits
|
||||
@ -203,32 +203,30 @@ export class Application extends Container {
|
||||
* Initialize the logger and load the logging level from the environment.
|
||||
* @protected
|
||||
*/
|
||||
protected setupLogging() {
|
||||
protected setupLogging(): void {
|
||||
const standard: StandardLogger = this.make<StandardLogger>(StandardLogger)
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.registerLogger(standard)
|
||||
logging.verbose('Attempting to load logging level from the environment...')
|
||||
|
||||
try {
|
||||
logging.verbose('Attempting to load logging level from the environment...')
|
||||
const envLevel = this.env('EXTOLLO_LOGGING_LEVEL')
|
||||
logging.verbose(`Read logging level: ${envLevel}`)
|
||||
const envLevel = this.env('EXTOLLO_LOGGING_LEVEL')
|
||||
logging.verbose(`Read logging level: ${envLevel}`)
|
||||
|
||||
if ( isLoggingLevel(envLevel) ) {
|
||||
logging.verbose('Logging level is valid.')
|
||||
logging.level = envLevel
|
||||
logging.debug(`Set logging level from environment: ${envLevel}`)
|
||||
}
|
||||
} catch(e) {}
|
||||
if ( isLoggingLevel(envLevel) ) {
|
||||
logging.verbose('Logging level is valid.')
|
||||
logging.level = envLevel
|
||||
logging.debug(`Set logging level from environment: ${envLevel}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the environment variable library and read from the `.env` file.
|
||||
* @protected
|
||||
*/
|
||||
protected bootstrapEnvironment() {
|
||||
protected bootstrapEnvironment(): void {
|
||||
dotenv.config({
|
||||
path: this.basePath.concat('.env').toLocal
|
||||
path: this.basePath.concat('.env').toLocal,
|
||||
})
|
||||
}
|
||||
|
||||
@ -238,14 +236,14 @@ export class Application extends Container {
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
*/
|
||||
public env(key: string, defaultValue?: any): any {
|
||||
public env(key: string, defaultValue?: unknown): any {
|
||||
return infer(process.env[key] ?? '') ?? defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the application by starting all units in order, then stopping them in reverse order.
|
||||
*/
|
||||
async run() {
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
await this.up()
|
||||
await this.down()
|
||||
@ -257,7 +255,7 @@ export class Application extends Container {
|
||||
/**
|
||||
* Start all units in the application, one at a time, in order.
|
||||
*/
|
||||
async up() {
|
||||
async up(): Promise<void> {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.info('Starting Extollo...', this.forceStartupMessage)
|
||||
@ -271,12 +269,14 @@ export class Application extends Container {
|
||||
/**
|
||||
* Stop all units in the application, one at a time, in reverse order.
|
||||
*/
|
||||
async down() {
|
||||
async down(): Promise<void> {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
logging.info('Stopping Extollo...', this.forceStartupMessage)
|
||||
for ( const unit of [...this.instantiatedUnits].reverse() ) {
|
||||
if ( !unit ) continue
|
||||
if ( !unit ) {
|
||||
continue
|
||||
}
|
||||
await this.stopUnit(unit)
|
||||
}
|
||||
}
|
||||
@ -285,7 +285,7 @@ export class Application extends Container {
|
||||
* Start a single unit, setting its status.
|
||||
* @param unit
|
||||
*/
|
||||
public async startUnit(unit: Unit) {
|
||||
public async startUnit(unit: Unit): Promise<void> {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
try {
|
||||
@ -296,8 +296,7 @@ export class Application extends Container {
|
||||
logging.info(`Started ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
unit.status = UnitStatus.Error
|
||||
console.log(e)
|
||||
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
||||
throw this.errorWrapContext(e, {unitName: unit.constructor.name})
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +304,7 @@ export class Application extends Container {
|
||||
* Stop a single unit, setting its status.
|
||||
* @param unit
|
||||
*/
|
||||
public async stopUnit(unit: Unit) {
|
||||
public async stopUnit(unit: Unit): Promise<void> {
|
||||
const logging: Logging = this.make<Logging>(Logging)
|
||||
|
||||
try {
|
||||
@ -316,7 +315,7 @@ export class Application extends Container {
|
||||
logging.info(`Stopped ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
unit.status = UnitStatus.Error
|
||||
throw this.errorWrapContext(e, {unit_name: unit.constructor.name})
|
||||
throw this.errorWrapContext(e, {unitName: unit.constructor.name})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as color from 'colors/safe'
|
||||
import {Logging} from "../service/Logging";
|
||||
import {Inject} from "../di";
|
||||
import {ErrorWithContext} from "../util";
|
||||
import {Logging} from '../service/Logging'
|
||||
import {Inject} from '../di'
|
||||
import {ErrorWithContext} from '../util'
|
||||
|
||||
/**
|
||||
* Class with logic for handling errors that are thrown at the run-level of the application.
|
||||
@ -30,7 +30,8 @@ export class RunLevelErrorHandler {
|
||||
*/
|
||||
wrapContext(e: Error, context: {[key: string]: any}): ErrorWithContext {
|
||||
if ( e instanceof ErrorWithContext ) {
|
||||
e.context = {...e.context, ...context}
|
||||
e.context = {...e.context,
|
||||
...context}
|
||||
return e
|
||||
}
|
||||
|
||||
@ -45,15 +46,18 @@ export class RunLevelErrorHandler {
|
||||
* Log the error to the logger.
|
||||
* @param {Error} e
|
||||
*/
|
||||
display(e: Error) {
|
||||
display(e: Error): void {
|
||||
let operativeError = e
|
||||
let context: {[key: string]: string} = {}
|
||||
if ( e instanceof ErrorWithContext ) {
|
||||
if ( e.originalError ) operativeError = e.originalError
|
||||
if ( e.originalError ) {
|
||||
operativeError = e.originalError
|
||||
}
|
||||
context = e.context
|
||||
}
|
||||
|
||||
const contextDisplay = Object.keys(context).map(key => ` - ${key}: ${context[key]}`).join('\n')
|
||||
const contextDisplay = Object.keys(context).map(key => ` - ${key}: ${context[key]}`)
|
||||
.join('\n')
|
||||
|
||||
try {
|
||||
let errorString = `RunLevelErrorHandler invoked:
|
||||
@ -73,12 +77,12 @@ With the following context:
|
||||
${contextDisplay}
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
this.logging.error(errorString, true)
|
||||
} catch (display_e) {
|
||||
} catch (displayError) {
|
||||
// The error display encountered an error...
|
||||
// just throw the original so it makes it out
|
||||
console.error('RunLevelErrorHandler encountered an error:', display_e.message)
|
||||
console.error('RunLevelErrorHandler encountered an error:', displayError.message) // eslint-disable-line no-console
|
||||
throw operativeError
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {AppClass} from './AppClass';
|
||||
import {AppClass} from './AppClass'
|
||||
|
||||
/**
|
||||
* The various statuses of a Unit.
|
||||
@ -23,7 +23,7 @@ export abstract class Unit extends AppClass {
|
||||
* This method is called to start the unit when the application is booting.
|
||||
* Here, you should do any setup required to get the package up and running.
|
||||
*/
|
||||
public up(): Promise<void> | void {}
|
||||
public up(): Promise<void> | void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
|
||||
/**
|
||||
* This method is called to stop the unit when the application is shutting down.
|
||||
@ -32,5 +32,5 @@ export abstract class Unit extends AppClass {
|
||||
* IN PARTICULAR take care to free blocking resources that could prevent the
|
||||
* process from exiting without a kill.
|
||||
*/
|
||||
public down(): Promise<void> | void {}
|
||||
public down(): Promise<void> | void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Connection} from "./connection/Connection";
|
||||
import {Inject, Singleton} from "../di";
|
||||
import {ErrorWithContext, uuid_v4} from "../util";
|
||||
import {AppClass} from "../lifecycle/AppClass";
|
||||
import {Logging} from "../service/Logging";
|
||||
import {Connection} from './connection/Connection'
|
||||
import {Inject, Singleton} from '../di'
|
||||
import {ErrorWithContext, uuid4} from '../util'
|
||||
import {AppClass} from '../lifecycle/AppClass'
|
||||
import {Logging} from '../service/Logging'
|
||||
|
||||
/**
|
||||
* A singleton, non-unit service that stores and retrieves database connections by name.
|
||||
@ -20,20 +20,21 @@ export class DatabaseService extends AppClass {
|
||||
* @param name
|
||||
* @param connection
|
||||
*/
|
||||
register(name: string, connection: Connection) {
|
||||
register(name: string, connection: Connection): this {
|
||||
if ( this.connections[name] ) {
|
||||
this.logging.warn(`Overriding duplicate connection: ${name}`)
|
||||
}
|
||||
|
||||
this.connections[name] = connection
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a connection is registered with the given name.
|
||||
* @param name
|
||||
*/
|
||||
has(name: string) {
|
||||
return !!this.connections[name]
|
||||
has(name: string): boolean {
|
||||
return Boolean(this.connections[name])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,10 +58,10 @@ export class DatabaseService extends AppClass {
|
||||
|
||||
/** Get a guaranteed-unique connection name. */
|
||||
uniqueName(): string {
|
||||
let name: string;
|
||||
let name: string
|
||||
|
||||
do {
|
||||
name = uuid_v4()
|
||||
name = uuid4()
|
||||
} while (this.has(name))
|
||||
|
||||
return name
|
||||
|
@ -1,19 +1,19 @@
|
||||
import {Inject} from "../../di";
|
||||
import {DatabaseService} from "../DatabaseService";
|
||||
import {Inject} from '../../di'
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {
|
||||
Constraint, ConstraintConnectionOperator,
|
||||
ConstraintOperator,
|
||||
OrderDirection,
|
||||
OrderStatement, QueryResult,
|
||||
QuerySource,
|
||||
SpecifiedField
|
||||
} from "../types";
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {deepCopy, ErrorWithContext} from "../../util";
|
||||
import {EscapeValue, QuerySafeValue, raw} from "../dialect/SQLDialect";
|
||||
import {ResultCollection} from "./result/ResultCollection";
|
||||
import {AbstractResultIterable} from "./result/AbstractResultIterable";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
SpecifiedField,
|
||||
} from '../types'
|
||||
import {Connection} from '../connection/Connection'
|
||||
import {deepCopy, ErrorWithContext} from '../../util'
|
||||
import {EscapeValue, QuerySafeValue, raw} from '../dialect/SQLDialect'
|
||||
import {ResultCollection} from './result/ResultCollection'
|
||||
import {AbstractResultIterable} from './result/AbstractResultIterable'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
|
||||
/**
|
||||
* Type alias for a function that applies some constraints to a builder group.
|
||||
@ -35,25 +35,25 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
protected source?: QuerySource
|
||||
|
||||
/** The fields to query from the table. */
|
||||
protected _fields: SpecifiedField[] = []
|
||||
protected registeredFields: SpecifiedField[] = []
|
||||
|
||||
/** The number of records to skip before the result set. */
|
||||
protected _skip?: number
|
||||
protected registeredSkip?: number
|
||||
|
||||
/** The max number of records to include in the result set. */
|
||||
protected _take?: number
|
||||
protected registeredTake?: number
|
||||
|
||||
/** If true, the query should refer to distinct records. */
|
||||
protected _distinct: boolean = false
|
||||
protected registeredDistinct = false
|
||||
|
||||
/** Array of SQL group-by clauses. */
|
||||
protected _groupings: string[] = []
|
||||
protected registeredGroupings: string[] = []
|
||||
|
||||
/** Array of SQL order-by clauses. */
|
||||
protected _orders: OrderStatement[] = []
|
||||
protected registeredOrders: OrderStatement[] = []
|
||||
|
||||
/** The connection on which the query should be executed. */
|
||||
protected _connection?: Connection
|
||||
protected registeredConnection?: Connection
|
||||
|
||||
/**
|
||||
* Create a new, empty, instance of the current builder.
|
||||
@ -73,50 +73,53 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
|
||||
bldr.constraints = deepCopy(this.constraints)
|
||||
bldr.source = deepCopy(this.source)
|
||||
bldr._fields = deepCopy(this._fields)
|
||||
bldr._skip = deepCopy(this._skip)
|
||||
bldr._take = deepCopy(this._take)
|
||||
bldr._distinct = deepCopy(this._distinct)
|
||||
bldr._groupings = deepCopy(this._groupings)
|
||||
bldr._orders = deepCopy(this._orders)
|
||||
bldr._connection = this._connection
|
||||
bldr.registeredFields = deepCopy(this.registeredFields)
|
||||
bldr.registeredSkip = deepCopy(this.registeredSkip)
|
||||
bldr.registeredTake = deepCopy(this.registeredTake)
|
||||
bldr.registeredDistinct = deepCopy(this.registeredDistinct)
|
||||
bldr.registeredGroupings = deepCopy(this.registeredGroupings)
|
||||
bldr.registeredOrders = deepCopy(this.registeredOrders)
|
||||
bldr.registeredConnection = this.registeredConnection
|
||||
|
||||
return bldr
|
||||
}
|
||||
|
||||
/** Get the constraints applied to this query. */
|
||||
public get appliedConstraints() {
|
||||
public get appliedConstraints(): Constraint[] {
|
||||
return deepCopy(this.constraints)
|
||||
}
|
||||
|
||||
/** Get the fields that should be included in this query. */
|
||||
public get appliedFields() {
|
||||
return deepCopy(this._fields)
|
||||
public get appliedFields(): SpecifiedField[] {
|
||||
return deepCopy(this.registeredFields)
|
||||
}
|
||||
|
||||
/** Get the skip/take values of this query. */
|
||||
public get appliedPagination() {
|
||||
return { skip: this._skip, take: this._take }
|
||||
public get appliedPagination(): { skip: number | undefined, take: number | undefined} {
|
||||
return { skip: this.registeredSkip,
|
||||
take: this.registeredTake }
|
||||
}
|
||||
|
||||
/** True if the query should be DISTINCT */
|
||||
public get appliedDistinction() {
|
||||
return this._distinct
|
||||
public get appliedDistinction(): boolean {
|
||||
return this.registeredDistinct
|
||||
}
|
||||
|
||||
/** Get the SQL group-by clauses applied to this query. */
|
||||
public get appliedGroupings() {
|
||||
return deepCopy(this._groupings)
|
||||
public get appliedGroupings(): string[] {
|
||||
return deepCopy(this.registeredGroupings)
|
||||
}
|
||||
|
||||
/** Get the SQL order-by clauses applied to this query. */
|
||||
public get appliedOrder() {
|
||||
return deepCopy(this._orders)
|
||||
public get appliedOrder(): OrderStatement[] {
|
||||
return deepCopy(this.registeredOrders)
|
||||
}
|
||||
|
||||
/** Get the source table for this query. */
|
||||
public get querySource() {
|
||||
if ( this.source ) return deepCopy(this.source)
|
||||
public get querySource(): QuerySource | undefined {
|
||||
if ( this.source ) {
|
||||
return deepCopy(this.source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,9 +127,10 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param table
|
||||
* @param alias
|
||||
*/
|
||||
from(table: string, alias?: string) {
|
||||
from(table: string, alias?: string): this {
|
||||
if ( alias ) {
|
||||
this.source = { table, alias }
|
||||
this.source = { table,
|
||||
alias }
|
||||
} else {
|
||||
this.source = table
|
||||
}
|
||||
@ -138,7 +142,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param table
|
||||
* @param alias
|
||||
*/
|
||||
table(table: string, alias?: string) {
|
||||
table(table: string, alias?: string): this {
|
||||
return this.from(table, alias)
|
||||
}
|
||||
|
||||
@ -147,11 +151,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param alias
|
||||
*/
|
||||
field(field: string | QuerySafeValue, alias?: string) {
|
||||
field(field: string | QuerySafeValue, alias?: string): this {
|
||||
if ( alias ) {
|
||||
this._fields.push({ field, alias })
|
||||
this.registeredFields.push({ field,
|
||||
alias })
|
||||
} else {
|
||||
this._fields.push(field)
|
||||
this.registeredFields.push(field)
|
||||
}
|
||||
return this
|
||||
}
|
||||
@ -161,7 +166,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param fields
|
||||
*/
|
||||
fields(...fields: SpecifiedField[]): this {
|
||||
this._fields = [...this._fields, ...fields]
|
||||
this.registeredFields = [...this.registeredFields, ...fields]
|
||||
return this
|
||||
}
|
||||
|
||||
@ -184,8 +189,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
/**
|
||||
* Remove all selected fields from this query.
|
||||
*/
|
||||
clearFields() {
|
||||
this._fields = []
|
||||
clearFields(): this {
|
||||
this.registeredFields = []
|
||||
return this
|
||||
}
|
||||
|
||||
@ -195,7 +200,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
where(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
|
||||
where(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
|
||||
this.createConstraint('AND', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
@ -206,7 +211,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
whereRaw(field: string, operator: ConstraintOperator, operand: string) {
|
||||
whereRaw(field: string, operator: ConstraintOperator, operand: string): this {
|
||||
this.createConstraint('AND', field, operator, raw(operand))
|
||||
return this
|
||||
}
|
||||
@ -217,7 +222,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
whereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
|
||||
whereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
|
||||
this.createConstraint('AND NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
@ -228,7 +233,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
orWhere(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
|
||||
orWhere(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
|
||||
this.createConstraint('OR', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
@ -239,7 +244,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
orWhereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
|
||||
orWhereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
|
||||
this.createConstraint('OR NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
@ -250,7 +255,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operator
|
||||
* @param operand
|
||||
*/
|
||||
orWhereRaw(field: string, operator: ConstraintOperator, operand: string) {
|
||||
orWhereRaw(field: string, operator: ConstraintOperator, operand: string): this {
|
||||
this.createConstraint('OR', field, operator, raw(operand))
|
||||
return this
|
||||
}
|
||||
@ -260,7 +265,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param values
|
||||
*/
|
||||
whereIn(field: string, values: EscapeValue) {
|
||||
whereIn(field: string, values: EscapeValue): this {
|
||||
this.constraints.push({
|
||||
field,
|
||||
operator: 'IN',
|
||||
@ -275,7 +280,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param values
|
||||
*/
|
||||
whereNotIn(field: string, values: EscapeValue) {
|
||||
whereNotIn(field: string, values: EscapeValue): this {
|
||||
this.constraints.push({
|
||||
field,
|
||||
operator: 'NOT IN',
|
||||
@ -290,12 +295,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param values
|
||||
*/
|
||||
orWhereIn(field: string, values: EscapeValue) {
|
||||
orWhereIn(field: string, values: EscapeValue): this {
|
||||
this.constraints.push({
|
||||
field,
|
||||
operator: 'IN',
|
||||
operand: values,
|
||||
preop: 'OR'
|
||||
preop: 'OR',
|
||||
})
|
||||
return this
|
||||
}
|
||||
@ -305,12 +310,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param values
|
||||
*/
|
||||
orWhereNotIn(field: string, values: EscapeValue) {
|
||||
orWhereNotIn(field: string, values: EscapeValue): this {
|
||||
this.constraints.push({
|
||||
field,
|
||||
operator: 'NOT IN',
|
||||
operand: values,
|
||||
preop: 'OR'
|
||||
preop: 'OR',
|
||||
})
|
||||
return this
|
||||
}
|
||||
@ -319,8 +324,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Limit the query to a maximum number of rows.
|
||||
* @param rows
|
||||
*/
|
||||
limit(rows: number) {
|
||||
this._take = rows
|
||||
limit(rows: number): this {
|
||||
this.registeredTake = rows
|
||||
return this
|
||||
}
|
||||
|
||||
@ -328,7 +333,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Alias of `limit()`.
|
||||
* @param rows
|
||||
*/
|
||||
take(rows: number) {
|
||||
take(rows: number): this {
|
||||
return this.limit(rows)
|
||||
}
|
||||
|
||||
@ -336,8 +341,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Skip the first `rows` many rows in the result set.
|
||||
* @param rows
|
||||
*/
|
||||
skip(rows: number) {
|
||||
this._skip = rows
|
||||
skip(rows: number): this {
|
||||
this.registeredSkip = rows
|
||||
return this
|
||||
}
|
||||
|
||||
@ -345,23 +350,23 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Alias of `skip()`.
|
||||
* @param rows
|
||||
*/
|
||||
offset(rows: number) {
|
||||
offset(rows: number): this {
|
||||
return this.skip(rows)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the query return only distinct rows.
|
||||
*/
|
||||
distinct() {
|
||||
this._distinct = true
|
||||
distinct(): this {
|
||||
this.registeredDistinct = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the query to return non-distinct rows. (Undoes `distinct()`.)
|
||||
*/
|
||||
notDistinct() {
|
||||
this._distinct = false
|
||||
notDistinct(): this {
|
||||
this.registeredDistinct = false
|
||||
return this
|
||||
}
|
||||
|
||||
@ -371,7 +376,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param pageNum
|
||||
* @param pageSize
|
||||
*/
|
||||
page(pageNum: number = 1, pageSize: number = 20) {
|
||||
page(pageNum = 1, pageSize = 20): this {
|
||||
this.skip(pageSize * (pageNum - 1))
|
||||
this.take(pageSize)
|
||||
return this
|
||||
@ -381,8 +386,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Apply one or more GROUP-BY clauses to the query.
|
||||
* @param groupings
|
||||
*/
|
||||
groupBy(...groupings: string[]) {
|
||||
this._groupings = groupings
|
||||
groupBy(...groupings: string[]): this {
|
||||
this.registeredGroupings = groupings
|
||||
return this
|
||||
}
|
||||
|
||||
@ -391,8 +396,9 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param field
|
||||
* @param direction
|
||||
*/
|
||||
orderBy(field: string, direction: OrderDirection = 'ASC') {
|
||||
this._orders.push({ field, direction })
|
||||
orderBy(field: string, direction: OrderDirection = 'ASC'): this {
|
||||
this.registeredOrders.push({ field,
|
||||
direction })
|
||||
return this
|
||||
}
|
||||
|
||||
@ -400,7 +406,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Order the query by the given field, ascending.
|
||||
* @param field
|
||||
*/
|
||||
orderByAscending(field: string) {
|
||||
orderByAscending(field: string): this {
|
||||
return this.orderBy(field, 'ASC')
|
||||
}
|
||||
|
||||
@ -408,7 +414,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Order the query by the given field, descending.
|
||||
* @param field
|
||||
*/
|
||||
orderByDescending(field: string) {
|
||||
orderByDescending(field: string): this {
|
||||
return this.orderBy(field, 'DESC')
|
||||
}
|
||||
|
||||
@ -416,11 +422,11 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Specify the connection name or instance to execute the query on.
|
||||
* @param nameOrInstance
|
||||
*/
|
||||
connection(nameOrInstance: string | Connection) {
|
||||
connection(nameOrInstance: string | Connection): this {
|
||||
if ( nameOrInstance instanceof Connection ) {
|
||||
this._connection = nameOrInstance
|
||||
this.registeredConnection = nameOrInstance
|
||||
} else {
|
||||
this._connection = this.databaseService.get(nameOrInstance)
|
||||
this.registeredConnection = this.databaseService.get(nameOrInstance)
|
||||
}
|
||||
|
||||
return this
|
||||
@ -430,11 +436,11 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* Get a result iterable for the rows of this query.
|
||||
*/
|
||||
iterator(): AbstractResultIterable<T> {
|
||||
if ( !this._connection ) {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to fetch iterator for query.`)
|
||||
}
|
||||
|
||||
return this.getResultIterable();
|
||||
return this.getResultIterable()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -469,12 +475,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param data
|
||||
*/
|
||||
async update(data: {[key: string]: EscapeValue}): Promise<QueryResult> {
|
||||
if ( !this._connection ) {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this._connection.dialect().renderUpdate(this, data)
|
||||
return this._connection.query(query)
|
||||
const query = this.registeredConnection.dialect().renderUpdate(this, data)
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -495,12 +501,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
*
|
||||
*/
|
||||
async delete(): Promise<QueryResult> {
|
||||
if ( !this._connection ) {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this._connection.dialect().renderDelete(this)
|
||||
return this._connection.query(query)
|
||||
const query = this.registeredConnection.dialect().renderDelete(this)
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -527,26 +533,26 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
*
|
||||
* @param rowOrRows
|
||||
*/
|
||||
async insert(rowOrRows: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]) {
|
||||
if ( !this._connection ) {
|
||||
async insert(rowOrRows: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]): Promise<QueryResult> {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this._connection.dialect().renderInsert(this, rowOrRows)
|
||||
return this._connection.query(query)
|
||||
const query = this.registeredConnection.dialect().renderInsert(this, rowOrRows)
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least one row matches the current query.
|
||||
*/
|
||||
async exists() {
|
||||
if ( !this._connection ) {
|
||||
async exists(): Promise<boolean> {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this._connection.dialect().renderExistential(this)
|
||||
const result = await this._connection.query(query)
|
||||
return !!result.rows.first()
|
||||
const query = this.registeredConnection.dialect().renderExistential(this)
|
||||
const result = await this.registeredConnection.query(query)
|
||||
return Boolean(result.rows.first())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -557,17 +563,20 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
* @param operand
|
||||
* @private
|
||||
*/
|
||||
private createConstraint(preop: ConstraintConnectionOperator, field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: any) {
|
||||
private createConstraint(preop: ConstraintConnectionOperator, field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: any): void {
|
||||
if ( typeof field === 'function' ) {
|
||||
const builder = this.getNewInstance()
|
||||
field(builder)
|
||||
this.constraints.push({
|
||||
preop,
|
||||
items: builder.appliedConstraints
|
||||
items: builder.appliedConstraints,
|
||||
})
|
||||
} else if ( field && operator && typeof operand !== 'undefined' ) {
|
||||
this.constraints.push({
|
||||
field, operator, operand, preop, // FIXME escape operand
|
||||
field,
|
||||
operator,
|
||||
operand,
|
||||
preop, // FIXME escape operand
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
import {ErrorWithContext} from "../../util";
|
||||
import {Container} from "../../di";
|
||||
import {ResultIterable} from "./result/ResultIterable";
|
||||
import {QueryRow} from "../types";
|
||||
import {AbstractBuilder} from "./AbstractBuilder";
|
||||
import {AbstractResultIterable} from "./result/AbstractResultIterable";
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {Container} from '../../di'
|
||||
import {ResultIterable} from './result/ResultIterable'
|
||||
import {QueryRow} from '../types'
|
||||
import {AbstractBuilder} from './AbstractBuilder'
|
||||
import {AbstractResultIterable} from './result/AbstractResultIterable'
|
||||
|
||||
/**
|
||||
* Implementation of the abstract builder class that returns simple QueryRow objects.
|
||||
*/
|
||||
export class Builder extends AbstractBuilder<QueryRow> {
|
||||
public getNewInstance(): AbstractBuilder<QueryRow> {
|
||||
return Container.getContainer().make<Builder>(Builder);
|
||||
return Container.getContainer().make<Builder>(Builder)
|
||||
}
|
||||
|
||||
public getResultIterable(): AbstractResultIterable<QueryRow> {
|
||||
if ( !this._connection ) {
|
||||
if ( !this.registeredConnection ) {
|
||||
throw new ErrorWithContext(`No connection specified to fetch iterator for query.`)
|
||||
}
|
||||
|
||||
return Container.getContainer().make<ResultIterable>(ResultIterable, this, this._connection)
|
||||
return Container.getContainer().make<ResultIterable>(ResultIterable, this, this.registeredConnection)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Collection, Iterable} from "../../../util"
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {AbstractBuilder} from "../AbstractBuilder";
|
||||
import {Collection, Iterable} from '../../../util'
|
||||
import {Connection} from '../../connection/Connection'
|
||||
import {AbstractBuilder} from '../AbstractBuilder'
|
||||
|
||||
/**
|
||||
* Base Iterable class that generates the results of a Builder query.
|
||||
@ -12,7 +12,9 @@ export abstract class AbstractResultIterable<T> extends Iterable<T> {
|
||||
|
||||
/** The connection on which to execute the builder. */
|
||||
public readonly connection: Connection,
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL string for the SELECT query for this iterable.
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {AsyncCollection} from "../../../util";
|
||||
import {AbstractResultIterable} from "./AbstractResultIterable";
|
||||
import {AsyncCollection} from '../../../util'
|
||||
import {AbstractResultIterable} from './AbstractResultIterable'
|
||||
|
||||
/**
|
||||
* Async collection class that iterates AbstractResultIterables in chunks.
|
||||
@ -10,7 +10,7 @@ export class ResultCollection<T> extends AsyncCollection<T> {
|
||||
iterator: AbstractResultIterable<T>,
|
||||
|
||||
/** The max number of records to request per-query, by default. */
|
||||
chunkSize: number = 500
|
||||
chunkSize = 500,
|
||||
) {
|
||||
super(iterator, chunkSize)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {QueryRow} from "../../types";
|
||||
import {Builder} from "../Builder";
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {AbstractResultIterable} from "./AbstractResultIterable";
|
||||
import {Collection} from "../../../util";
|
||||
import {QueryRow} from '../../types'
|
||||
import {Builder} from '../Builder'
|
||||
import {Connection} from '../../connection/Connection'
|
||||
import {AbstractResultIterable} from './AbstractResultIterable'
|
||||
import {Collection} from '../../../util'
|
||||
|
||||
/**
|
||||
* Implementation of AbstractResultIterable that yields simple QueryRow instances (objects).
|
||||
@ -11,9 +11,11 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
|
||||
constructor(
|
||||
public readonly builder: Builder,
|
||||
public readonly connection: Connection,
|
||||
) { super(builder, connection) }
|
||||
) {
|
||||
super(builder, connection)
|
||||
}
|
||||
|
||||
public get selectSQL() {
|
||||
public get selectSQL(): string {
|
||||
return this.connection.dialect().renderSelect(this.builder)
|
||||
}
|
||||
|
||||
@ -27,7 +29,7 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
|
||||
return (await this.connection.query(query)).rows
|
||||
}
|
||||
|
||||
async count() {
|
||||
async count(): Promise<number> {
|
||||
const query = this.connection.dialect().renderCount(this.selectSQL)
|
||||
const result = (await this.connection.query(query)).rows.first()
|
||||
return result?.extollo_render_count ?? 0
|
||||
@ -38,7 +40,7 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
|
||||
return result.rows
|
||||
}
|
||||
|
||||
clone() {
|
||||
clone(): ResultIterable {
|
||||
return new ResultIterable(this.builder, this.connection)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Collection, ErrorWithContext} from "../../util";
|
||||
import {QueryResult} from "../types";
|
||||
import {SQLDialect} from "../dialect/SQLDialect";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {QueryResult} from '../types'
|
||||
import {SQLDialect} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
|
||||
/**
|
||||
* Error thrown when a connection is used before it is ready.
|
||||
@ -30,7 +30,9 @@ export abstract class Connection extends AppClass {
|
||||
* This connection's config object
|
||||
*/
|
||||
public readonly config: any = {},
|
||||
) { super() }
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public abstract dialect(): SQLDialect
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {Connection, ConnectionNotReadyError} from "./Connection";
|
||||
import {Client} from "pg";
|
||||
import {Inject} from "../../di";
|
||||
import {QueryResult} from "../types";
|
||||
import {collect} from "../../util";
|
||||
import {SQLDialect} from "../dialect/SQLDialect";
|
||||
import {PostgreSQLDialect} from "../dialect/PostgreSQLDialect";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Connection, ConnectionNotReadyError} from './Connection'
|
||||
import {Client} from 'pg'
|
||||
import {Inject} from '../../di'
|
||||
import {QueryResult} from '../types'
|
||||
import {collect} from '../../util'
|
||||
import {SQLDialect} from '../dialect/SQLDialect'
|
||||
import {PostgreSQLDialect} from '../dialect/PostgreSQLDialect'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Type interface representing the config for a PostgreSQL connection.
|
||||
@ -32,13 +32,13 @@ export class PostgresConnection extends Connection {
|
||||
return <PostgreSQLDialect> this.app().make(PostgreSQLDialect)
|
||||
}
|
||||
|
||||
public async init() {
|
||||
public async init(): Promise<void> {
|
||||
this.logging.debug(`Initializing PostgreSQL connection ${this.name}...`)
|
||||
this.client = new Client(this.config)
|
||||
await this.client.connect()
|
||||
}
|
||||
|
||||
public async close() {
|
||||
public async close(): Promise<void> {
|
||||
this.logging.debug(`Closing PostgreSQL connection ${this.name}...`)
|
||||
if ( this.client ) {
|
||||
await this.client.end()
|
||||
@ -46,8 +46,11 @@ export class PostgresConnection extends Connection {
|
||||
}
|
||||
|
||||
public async query(query: string): Promise<QueryResult> {
|
||||
if ( !this.client ) throw new ConnectionNotReadyError(this.name, { config: JSON.stringify(this.config) })
|
||||
this.logging.verbose(`Executing query in connection ${this.name}: \n${query.split('\n').map(x => ' ' + x).join('\n')}`)
|
||||
if ( !this.client ) {
|
||||
throw new ConnectionNotReadyError(this.name, { config: JSON.stringify(this.config) })
|
||||
}
|
||||
this.logging.verbose(`Executing query in connection ${this.name}: \n${query.split('\n').map(x => ' ' + x)
|
||||
.join('\n')}`)
|
||||
|
||||
try {
|
||||
const result = await this.client.query(query)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect';
|
||||
import {Constraint, isConstraintGroup, isConstraintItem, SpecifiedField} from "../types";
|
||||
import {AbstractBuilder} from "../builder/AbstractBuilder";
|
||||
import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect'
|
||||
import {Constraint, isConstraintGroup, isConstraintItem, SpecifiedField} from '../types'
|
||||
import {AbstractBuilder} from '../builder/AbstractBuilder'
|
||||
|
||||
/**
|
||||
* An implementation of the SQLDialect specific to PostgreSQL.
|
||||
@ -8,8 +8,9 @@ import {AbstractBuilder} from "../builder/AbstractBuilder";
|
||||
export class PostgreSQLDialect extends SQLDialect {
|
||||
|
||||
public escape(value: EscapeValue): QuerySafeValue {
|
||||
if ( value instanceof QuerySafeValue ) return value
|
||||
else if ( Array.isArray(value) ) {
|
||||
if ( value instanceof QuerySafeValue ) {
|
||||
return value
|
||||
} else if ( Array.isArray(value) ) {
|
||||
return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`)
|
||||
} else if ( String(value).toLowerCase() === 'true' || value === true ) {
|
||||
return new QuerySafeValue(value, 'TRUE')
|
||||
@ -34,7 +35,7 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
} else if ( value === null || typeof value === 'undefined' ) {
|
||||
return new QuerySafeValue(value, 'NULL')
|
||||
} else {
|
||||
const escaped = value.replace(/'/g, '\\\'') //.replace(/"/g, '\\"').replace(/`/g, '\\`')
|
||||
const escaped = value.replace(/'/g, '\\\'') // .replace(/"/g, '\\"').replace(/`/g, '\\`')
|
||||
return new QuerySafeValue(value, `'${escaped}'`)
|
||||
}
|
||||
}
|
||||
@ -44,7 +45,7 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
'SELECT COUNT(*) AS "extollo_render_count"',
|
||||
'FROM (',
|
||||
...query.split('\n').map(x => ` ${x}`),
|
||||
') AS extollo_target_query'
|
||||
') AS extollo_target_query',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@ -54,35 +55,46 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
'FROM (',
|
||||
...query.split('\n').map(x => ` ${x}`),
|
||||
') AS extollo_target_query',
|
||||
`OFFSET ${start} LIMIT ${(end - start) + 1}`
|
||||
`OFFSET ${start} LIMIT ${(end - start) + 1}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** Render the fields from the builder class to PostgreSQL syntax. */
|
||||
protected renderFields(builder: AbstractBuilder<any>) {
|
||||
protected renderFields(builder: AbstractBuilder<any>): string[] {
|
||||
return builder.appliedFields.map((field: SpecifiedField) => {
|
||||
let columnString: string
|
||||
if ( typeof field === 'string' ) columnString = field.split('.').map(x => `"${x}"`).join('.')
|
||||
else if ( field instanceof QuerySafeValue ) columnString = field.toString()
|
||||
else if ( typeof field.field === 'string' ) columnString = field.field.split('.').map(x => `"${x}"`).join('.')
|
||||
else columnString = field.field.toString()
|
||||
if ( typeof field === 'string' ) {
|
||||
columnString = field.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
} else if ( field instanceof QuerySafeValue ) {
|
||||
columnString = field.toString()
|
||||
} else if ( typeof field.field === 'string' ) {
|
||||
columnString = field.field.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
} else {
|
||||
columnString = field.field.toString()
|
||||
}
|
||||
|
||||
let aliasString = ''
|
||||
if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) aliasString = ` AS "${field.alias}"`
|
||||
if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) {
|
||||
aliasString = ` AS "${field.alias}"`
|
||||
}
|
||||
|
||||
return `${columnString}${aliasString}`
|
||||
})
|
||||
}
|
||||
|
||||
public renderSelect(builder: AbstractBuilder<any>): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('')
|
||||
.join(' ') + item
|
||||
const queryLines = [
|
||||
`SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`
|
||||
`SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`,
|
||||
]
|
||||
|
||||
// Add fields
|
||||
// FIXME error if no fields
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
const fields = this.renderFields(builder).map(x => indent(x))
|
||||
.join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
|
||||
@ -91,7 +103,8 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
queryLines.push('FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
@ -105,7 +118,8 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
// Add group by
|
||||
if ( builder.appliedGroupings?.length ) {
|
||||
const grouping = builder.appliedGroupings.map(group => {
|
||||
return indent(group.split('.').map(x => `"${x}"`).join('.'))
|
||||
return indent(group.split('.').map(x => `"${x}"`)
|
||||
.join('.'))
|
||||
}).join(',\n')
|
||||
|
||||
queryLines.push('GROUP BY')
|
||||
@ -114,7 +128,8 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
|
||||
// Add order by
|
||||
if ( builder.appliedOrder?.length ) {
|
||||
const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(x => '"' + x + '"').join('.')} ${x.direction}`)).join(',\n')
|
||||
const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(y => '"' + y + '"')
|
||||
.join('.')} ${x.direction}`)).join(',\n')
|
||||
queryLines.push('ORDER BY')
|
||||
queryLines.push(ordering)
|
||||
}
|
||||
@ -132,14 +147,14 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
|
||||
// TODO support FROM, RETURNING
|
||||
public renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
@ -166,17 +181,21 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
|
||||
// FIXME: subquery support here and with select
|
||||
public renderInsert(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[] = []): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('')
|
||||
.join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
if ( !Array.isArray(data) ) data = [data]
|
||||
if ( !Array.isArray(data) ) {
|
||||
data = [data]
|
||||
}
|
||||
const columns = Object.keys(data[0])
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
queryLines.push('INSERT INTO ' + (typeof source === 'string' ? table : `${table} AS "${source.alias}"`)
|
||||
+ (columns.length ? ` (${columns.join(', ')})` : ''))
|
||||
}
|
||||
@ -187,9 +206,9 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
queryLines.push('VALUES')
|
||||
|
||||
const valueString = data.map(row => {
|
||||
const values = columns.map(x => this.escape(row[x]))
|
||||
return indent(`(${values.join(', ')})`)
|
||||
})
|
||||
const values = columns.map(x => this.escape(row[x]))
|
||||
return indent(`(${values.join(', ')})`)
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
queryLines.push(valueString)
|
||||
@ -198,7 +217,8 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
// Add return fields
|
||||
if ( builder.appliedFields?.length ) {
|
||||
queryLines.push('RETURNING')
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
const fields = this.renderFields(builder).map(x => indent(x))
|
||||
.join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
}
|
||||
@ -207,14 +227,16 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
}
|
||||
|
||||
public renderDelete(builder: AbstractBuilder<any>): string {
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
|
||||
const indent = (item: string, level = 1) => Array(level + 1).fill('')
|
||||
.join(' ') + item
|
||||
const queryLines: string[] = []
|
||||
|
||||
// Add table source
|
||||
const source = builder.querySource
|
||||
if ( source ) {
|
||||
const tableString = typeof source === 'string' ? source : source.table
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
|
||||
const table: string = tableString.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
queryLines.push('DELETE FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
|
||||
}
|
||||
|
||||
@ -229,7 +251,8 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
if ( builder.appliedFields?.length ) {
|
||||
queryLines.push('RETURNING')
|
||||
|
||||
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
|
||||
const fields = this.renderFields(builder).map(x => indent(x))
|
||||
.join(',\n')
|
||||
|
||||
queryLines.push(fields)
|
||||
}
|
||||
@ -237,16 +260,18 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
return queryLines.join('\n')
|
||||
}
|
||||
|
||||
public renderConstraints(constraints: Constraint[]): string {
|
||||
public renderConstraints(allConstraints: Constraint[]): string {
|
||||
const constraintsToSql = (constraints: Constraint[], level = 1): string => {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
let statements = []
|
||||
const indent = Array(level * 2).fill(' ')
|
||||
.join('')
|
||||
const statements = []
|
||||
|
||||
for ( const constraint of constraints ) {
|
||||
if ( isConstraintGroup(constraint) ) {
|
||||
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}(\n${constraintsToSql(constraint.items, level + 1)}\n${indent})`)
|
||||
} else if ( isConstraintItem(constraint) ) {
|
||||
const field: string = constraint.field.split('.').map(x => `"${x}"`).join('.')
|
||||
const field: string = constraint.field.split('.').map(x => `"${x}"`)
|
||||
.join('.')
|
||||
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}${field} ${constraint.operator} ${this.escape(constraint.operand).value}`)
|
||||
}
|
||||
}
|
||||
@ -254,13 +279,15 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
return statements.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
return constraintsToSql(constraints)
|
||||
return constraintsToSql(allConstraints)
|
||||
}
|
||||
|
||||
public renderUpdateSet(data: {[key: string]: EscapeValue}) {
|
||||
public renderUpdateSet(data: {[key: string]: EscapeValue}): string {
|
||||
const sets = []
|
||||
for ( const key in data ) {
|
||||
if ( !data.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(data, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
sets.push(` "${key}" = ${this.escape(data[key])}`)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Constraint} from "../types";
|
||||
import {AbstractBuilder} from "../builder/AbstractBuilder";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {Constraint} from '../types'
|
||||
import {AbstractBuilder} from '../builder/AbstractBuilder'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
|
||||
/**
|
||||
* A value which can be escaped to be interpolated into an SQL query.
|
||||
@ -18,15 +18,15 @@ export type EscapeValueObject = { [field: string]: EscapeValue }
|
||||
export class QuerySafeValue {
|
||||
constructor(
|
||||
/** The unescaped value. */
|
||||
public readonly originalValue: any,
|
||||
public readonly originalValue: unknown,
|
||||
|
||||
/** The query-safe sanitized value. */
|
||||
public readonly value: any,
|
||||
public readonly value: unknown,
|
||||
) { }
|
||||
|
||||
/** Cast the value to a query-safe string. */
|
||||
toString() {
|
||||
return this.value
|
||||
toString(): string {
|
||||
return String(this.value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ export class QuerySafeValue {
|
||||
* This is dangerous and should NEVER be used to wrap user input.
|
||||
* @param value
|
||||
*/
|
||||
export function raw(value: any) {
|
||||
export function raw(value: unknown): QuerySafeValue {
|
||||
return new QuerySafeValue(value, value)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Collection} from "../../util";
|
||||
import {FieldType} from "../types";
|
||||
import {Collection} from '../../util'
|
||||
import {FieldType} from '../types'
|
||||
|
||||
/** The reflection metadata key containing information about the model's fields. */
|
||||
export const EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY = 'extollo:orm:Field.ts'
|
||||
@ -17,8 +17,8 @@ export interface ModelField {
|
||||
* Retrieve a collection of ModelField metadata from the given model.
|
||||
* @param model
|
||||
*/
|
||||
export function getFieldsMeta(model: any): Collection<ModelField> {
|
||||
const fields = Reflect.getMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, model.constructor)
|
||||
export function getFieldsMeta(model: unknown): Collection<ModelField> {
|
||||
const fields = Reflect.getMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, (model as any).constructor)
|
||||
if ( !(fields instanceof Collection) ) {
|
||||
return new Collection<ModelField>()
|
||||
}
|
||||
@ -31,8 +31,8 @@ export function getFieldsMeta(model: any): Collection<ModelField> {
|
||||
* @param model
|
||||
* @param fields
|
||||
*/
|
||||
export function setFieldsMeta(model: any, fields: Collection<ModelField>) {
|
||||
Reflect.defineMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, fields, model.constructor)
|
||||
export function setFieldsMeta(model: unknown, fields: Collection<ModelField>): void {
|
||||
Reflect.defineMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, fields, (model as any).constructor)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,7 +57,9 @@ export function setFieldsMeta(model: any, fields: Collection<ModelField>) {
|
||||
*/
|
||||
export function Field(type: FieldType, databaseKey?: string): PropertyDecorator {
|
||||
return (target, modelKey) => {
|
||||
if ( !databaseKey ) databaseKey = String(modelKey)
|
||||
if ( !databaseKey ) {
|
||||
databaseKey = String(modelKey)
|
||||
}
|
||||
const fields = getFieldsMeta(target)
|
||||
|
||||
const existingField = fields.firstWhere('modelKey', '=', modelKey)
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {ModelKey, QueryRow, QuerySource} from "../types";
|
||||
import {Container, Inject} from "../../di";
|
||||
import {DatabaseService} from "../DatabaseService";
|
||||
import {ModelBuilder} from "./ModelBuilder";
|
||||
import {getFieldsMeta, ModelField} from "./Field";
|
||||
import {deepCopy, BehaviorSubject, Pipe, Collection} from "../../util";
|
||||
import {EscapeValueObject} from "../dialect/SQLDialect";
|
||||
import {AppClass} from "../../lifecycle/AppClass";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {ModelKey, QueryRow, QuerySource} from '../types'
|
||||
import {Container, Inject} from '../../di'
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {getFieldsMeta, ModelField} from './Field'
|
||||
import {deepCopy, BehaviorSubject, Pipe, Collection} from '../../util'
|
||||
import {EscapeValueObject} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {Connection} from '../connection/Connection'
|
||||
|
||||
/**
|
||||
* Base for classes that are mapped to tables in a database.
|
||||
@ -19,7 +20,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* The name of the connection this model should run through.
|
||||
* @type string
|
||||
*/
|
||||
protected static connection: string = 'default'
|
||||
protected static connection = 'default'
|
||||
|
||||
/**
|
||||
* The name of the table this model is stored in.
|
||||
@ -36,7 +37,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* If false (default), the primary key will be excluded from INSERTs.
|
||||
*/
|
||||
protected static populateKeyOnInsert: boolean = false
|
||||
protected static populateKeyOnInsert = false
|
||||
|
||||
/**
|
||||
* Optionally, the timestamp field set on creation.
|
||||
@ -74,7 +75,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* The original row fetched from the database.
|
||||
* @protected
|
||||
*/
|
||||
protected _original?: QueryRow
|
||||
protected originalSourceRow?: QueryRow
|
||||
|
||||
/**
|
||||
* Behavior subject that fires after the model is populated.
|
||||
@ -124,7 +125,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* Get the table name for this model.
|
||||
*/
|
||||
public static tableName() {
|
||||
public static tableName(): string {
|
||||
return this.table
|
||||
}
|
||||
|
||||
@ -144,15 +145,16 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* Get the name of the connection where this model's table is found.
|
||||
*/
|
||||
public static connectionName() {
|
||||
public static connectionName(): string {
|
||||
return this.connection
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database connection instance for this model's connection.
|
||||
*/
|
||||
public static getConnection() {
|
||||
return Container.getContainer().make<DatabaseService>(DatabaseService).get(this.connectionName());
|
||||
public static getConnection(): Connection {
|
||||
return Container.getContainer().make<DatabaseService>(DatabaseService)
|
||||
.get(this.connectionName())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,14 +166,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* const user = await UserModel.query<UserModel>().where('name', 'LIKE', 'John Doe').first()
|
||||
* ```
|
||||
*/
|
||||
public static query<T2 extends Model<T2>>() {
|
||||
public static query<T2 extends Model<T2>>(): ModelBuilder<T2> {
|
||||
const builder = <ModelBuilder<T2>> Container.getContainer().make<ModelBuilder<T2>>(ModelBuilder, this)
|
||||
const source: QuerySource = this.querySource()
|
||||
|
||||
builder.connection(this.getConnection())
|
||||
|
||||
if ( typeof source === 'string' ) builder.from(source)
|
||||
else builder.from(source.table, source.alias)
|
||||
if ( typeof source === 'string' ) {
|
||||
builder.from(source)
|
||||
} else {
|
||||
builder.from(source.table, source.alias)
|
||||
}
|
||||
|
||||
getFieldsMeta(this.prototype).each(field => {
|
||||
builder.field(field.databaseKey)
|
||||
@ -185,7 +190,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* Pre-fill the model's properties from the given values.
|
||||
* Calls `boot()` under the hood.
|
||||
*/
|
||||
values?: {[key: string]: any}
|
||||
values?: {[key: string]: any},
|
||||
) {
|
||||
super()
|
||||
this.boot(values)
|
||||
@ -199,7 +204,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @param values
|
||||
*/
|
||||
public boot(values?: any) {
|
||||
public boot(values?: {[key: string]: unknown}): void {
|
||||
if ( values ) {
|
||||
getFieldsMeta(this).each(field => {
|
||||
this.setFieldFromObject(field.modelKey, String(field.modelKey), values)
|
||||
@ -216,8 +221,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @param row
|
||||
*/
|
||||
public async assumeFromSource(row: QueryRow) {
|
||||
this._original = row
|
||||
public async assumeFromSource(row: QueryRow): Promise<this> {
|
||||
this.originalSourceRow = row
|
||||
|
||||
getFieldsMeta(this).each(field => {
|
||||
this.setFieldFromObject(field.modelKey, field.databaseKey, row)
|
||||
@ -236,7 +241,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @param object
|
||||
*/
|
||||
public async assume(object: { [key: string]: any }) {
|
||||
public async assume(object: { [key: string]: any }): Promise<this> {
|
||||
getFieldsMeta(this).each(field => {
|
||||
if ( field.modelKey in object ) {
|
||||
this.setFieldFromObject(field.modelKey, String(field.modelKey), object)
|
||||
@ -249,26 +254,24 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* Get the value of the primary key of this model, if it exists.
|
||||
*/
|
||||
public key() {
|
||||
public key(): string {
|
||||
const ctor = this.constructor as typeof Model
|
||||
|
||||
const field = getFieldsMeta(this)
|
||||
.firstWhere('databaseKey', '=', ctor.key)
|
||||
|
||||
if ( field ) {
|
||||
// @ts-ignore
|
||||
return this[field.modelKey]
|
||||
return (this as any)[field.modelKey]
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return this[ctor.key]
|
||||
return (this as any)[ctor.key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this instance's record has been persisted into the database.
|
||||
*/
|
||||
public exists() {
|
||||
return !!this._original && !!this.key()
|
||||
public exists(): boolean {
|
||||
return Boolean(this.originalSourceRow) && Boolean(this.key())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,11 +287,13 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
const timestamps: { updated?: Date, created?: Date } = {}
|
||||
|
||||
if ( ctor.timestamps ) {
|
||||
// @ts-ignore
|
||||
if ( ctor.CREATED_AT ) timestamps.created = this[ctor.CREATED_AT]
|
||||
if ( ctor.CREATED_AT ) {
|
||||
timestamps.created = (this as any)[ctor.CREATED_AT]
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if ( ctor.UPDATED_AT ) timestamps.updated = this[ctor.UPDATED_AT]
|
||||
if ( ctor.UPDATED_AT ) {
|
||||
timestamps.updated = (this as any)[ctor.UPDATED_AT]
|
||||
}
|
||||
}
|
||||
|
||||
return timestamps
|
||||
@ -312,8 +317,11 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
|
||||
builder.connection(ModelClass.getConnection())
|
||||
|
||||
if ( typeof source === 'string' ) builder.from(source)
|
||||
else builder.from(source.table, source.alias)
|
||||
if ( typeof source === 'string' ) {
|
||||
builder.from(source)
|
||||
} else {
|
||||
builder.from(source.table, source.alias)
|
||||
}
|
||||
|
||||
getFieldsMeta(this).each(field => {
|
||||
builder.field(field.databaseKey)
|
||||
@ -343,15 +351,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* Get an array of all instances of this model.
|
||||
*/
|
||||
public async all() {
|
||||
return this.query().get().all()
|
||||
public async all(): Promise<T[]> {
|
||||
return this.query().get()
|
||||
.all()
|
||||
}
|
||||
|
||||
/**
|
||||
* Count all instances of this model in the database.
|
||||
*/
|
||||
public async count(): Promise<number> {
|
||||
return this.query().get().count()
|
||||
return this.query().get()
|
||||
.count()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -365,7 +375,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @param column
|
||||
*/
|
||||
public qualify(column: string) {
|
||||
public qualify(column: string): string {
|
||||
const ctor = this.constructor as typeof Model
|
||||
return `${ctor.tableName()}.${column}`
|
||||
}
|
||||
@ -384,7 +394,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* a.qualifyKey() // => 'table_a.a_id'
|
||||
* ```
|
||||
*/
|
||||
public qualifyKey() {
|
||||
public qualifyKey(): string {
|
||||
const ctor = this.constructor as typeof Model
|
||||
return this.qualify(ctor.key)
|
||||
}
|
||||
@ -400,7 +410,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @param column
|
||||
*/
|
||||
public static qualify(column: string) {
|
||||
public static qualify(column: string): string {
|
||||
return `${this.tableName()}.${column}`
|
||||
}
|
||||
|
||||
@ -417,7 +427,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* A.qualifyKey() // => 'table_a.a_id'
|
||||
* ```
|
||||
*/
|
||||
public static qualifyKey() {
|
||||
public static qualifyKey(): string {
|
||||
return this.qualify(this.key)
|
||||
}
|
||||
|
||||
@ -426,7 +436,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* return the unqualified name of the database column it corresponds to.
|
||||
* @param modelKey
|
||||
*/
|
||||
public static propertyToColumn(modelKey: string) {
|
||||
public static propertyToColumn(modelKey: string): string {
|
||||
return getFieldsMeta(this)
|
||||
.firstWhere('modelKey', '=', modelKey)?.databaseKey || modelKey
|
||||
}
|
||||
@ -434,7 +444,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
/**
|
||||
* Get the unqualified name of the column corresponding to the primary key of this model.
|
||||
*/
|
||||
public keyName() {
|
||||
public keyName(): string {
|
||||
const ctor = this.constructor as typeof Model
|
||||
return ctor.key
|
||||
}
|
||||
@ -446,11 +456,10 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* Only fields with `@Field()` annotations will be included.
|
||||
*/
|
||||
public toQueryRow(): QueryRow {
|
||||
const row = {}
|
||||
const row: QueryRow = {}
|
||||
|
||||
getFieldsMeta(this).each(field => {
|
||||
// @ts-ignore
|
||||
row[field.databaseKey] = this[field.modelKey]
|
||||
row[field.databaseKey] = (this as any)[field.modelKey]
|
||||
})
|
||||
|
||||
return row
|
||||
@ -462,13 +471,12 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* the record was fetched from the database or created.
|
||||
*/
|
||||
public dirtyToQueryRow(): QueryRow {
|
||||
const row = {}
|
||||
const row: QueryRow = {}
|
||||
|
||||
getFieldsMeta(this)
|
||||
.filter(this._isDirty)
|
||||
.filter(this.isDirtyCheck)
|
||||
.each(field => {
|
||||
// @ts-ignore
|
||||
row[field.databaseKey] = this[field.modelKey]
|
||||
row[field.databaseKey] = (this as any)[field.modelKey]
|
||||
})
|
||||
|
||||
return row
|
||||
@ -478,13 +486,13 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* Get an object of the database field => value mapping that was originally
|
||||
* fetched from the database. Excludes changes to model properties.
|
||||
*/
|
||||
public getOriginalValues() {
|
||||
return deepCopy(this._original)
|
||||
public getOriginalValues(): QueryRow | undefined {
|
||||
return deepCopy(this.originalSourceRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object of only the given properties on this model.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* Assume `a` is an instance of some model `A` with the given fields.
|
||||
* ```typescript
|
||||
@ -492,15 +500,14 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* a.only('field1', 'id) // => {field1: 'field1 value', id: 123}
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @param fields
|
||||
*/
|
||||
public only(...fields: string[]) {
|
||||
const row = {}
|
||||
public only(...fields: string[]): QueryRow {
|
||||
const row: QueryRow = {}
|
||||
|
||||
for ( const field of fields ) {
|
||||
// @ts-ignore
|
||||
row[field] = this[field]
|
||||
row[field] = (this as any)[field]
|
||||
}
|
||||
|
||||
return row
|
||||
@ -512,8 +519,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* Only fields with `@Field()` annotations are checked.
|
||||
*/
|
||||
public isDirty() {
|
||||
return getFieldsMeta(this).some(this._isDirty)
|
||||
public isDirty(): boolean {
|
||||
return getFieldsMeta(this).some(this.isDirtyCheck)
|
||||
}
|
||||
|
||||
|
||||
@ -523,7 +530,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* Only fields with `@Field()` annotations are checked.
|
||||
*/
|
||||
public isClean() {
|
||||
public isClean(): boolean {
|
||||
return !this.isDirty()
|
||||
}
|
||||
|
||||
@ -532,18 +539,25 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* the database, or if the given field never existed in the database.
|
||||
* @param field
|
||||
*/
|
||||
public wasChanged(field: string) {
|
||||
// @ts-ignore
|
||||
return getFieldsMeta(this).pluck('modelKey').includes(field) && this[field] !== this._original[field]
|
||||
public wasChanged(field: string): boolean {
|
||||
return (
|
||||
getFieldsMeta(this)
|
||||
.pluck('modelKey')
|
||||
.includes(field)
|
||||
&& (
|
||||
!this.originalSourceRow
|
||||
|| (this as any)[field] !== this.originalSourceRow[field]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of MODEL fields that have been modified since this record
|
||||
* was fetched from the database or created.
|
||||
*/
|
||||
public getDirtyFields() {
|
||||
public getDirtyFields(): string[] {
|
||||
return getFieldsMeta(this)
|
||||
.filter(this._isDirty)
|
||||
.filter(this.isDirtyCheck)
|
||||
.pluck('modelKey')
|
||||
.toArray()
|
||||
}
|
||||
@ -554,17 +568,15 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* If the model doesn't yet exist, set the CREATED_AT date. Always
|
||||
* sets the UPDATED_AT date.
|
||||
*/
|
||||
public touch() {
|
||||
public touch(): this {
|
||||
const constructor = (this.constructor as typeof Model)
|
||||
if ( constructor.timestamps ) {
|
||||
if ( constructor.UPDATED_AT ) {
|
||||
// @ts-ignore
|
||||
this[constructor.UPDATED_AT] = new Date()
|
||||
(this as any)[constructor.UPDATED_AT] = new Date()
|
||||
}
|
||||
|
||||
if ( !this.exists() && constructor.CREATED_AT ) {
|
||||
// @ts-ignore
|
||||
this[constructor.CREATED_AT] = new Date()
|
||||
(this as any)[constructor.CREATED_AT] = new Date()
|
||||
}
|
||||
}
|
||||
return this
|
||||
@ -587,8 +599,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
await this.updating$.next(this)
|
||||
|
||||
if ( !withoutTimestamps && ctor.timestamps && ctor.UPDATED_AT ) {
|
||||
// @ts-ignore
|
||||
this[ctor.UPDATED_AT] = new Date()
|
||||
(this as any)[ctor.UPDATED_AT] = new Date()
|
||||
}
|
||||
|
||||
const result = await this.query()
|
||||
@ -602,7 +613,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
}
|
||||
|
||||
const data = result.rows.firstWhere(this.keyName(), '=', this.key())
|
||||
if ( data ) await this.assumeFromSource(data)
|
||||
if ( data ) {
|
||||
await this.assumeFromSource(data)
|
||||
}
|
||||
|
||||
await this.updated$.next(this)
|
||||
} else if ( !this.exists() ) {
|
||||
@ -610,17 +623,15 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
|
||||
if ( !withoutTimestamps ) {
|
||||
if ( ctor.timestamps && ctor.CREATED_AT ) {
|
||||
// @ts-ignore
|
||||
this[ctor.CREATED_AT] = new Date()
|
||||
(this as any)[ctor.CREATED_AT] = new Date()
|
||||
}
|
||||
|
||||
if ( ctor.timestamps && ctor.UPDATED_AT ) {
|
||||
// @ts-ignore
|
||||
this[ctor.UPDATED_AT] = new Date()
|
||||
(this as any)[ctor.UPDATED_AT] = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
const row = this._buildInsertFieldObject()
|
||||
const row = this.buildInsertFieldObject()
|
||||
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
|
||||
|
||||
const result = await this.query()
|
||||
@ -633,7 +644,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
}
|
||||
|
||||
const data = result.rows.first()
|
||||
if ( data ) await this.assumeFromSource(result)
|
||||
if ( data ) {
|
||||
await this.assumeFromSource(result)
|
||||
}
|
||||
await this.created$.next(this)
|
||||
}
|
||||
|
||||
@ -646,22 +659,19 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* Only fields with `@Field()` annotations are included.
|
||||
*/
|
||||
public toObject(): { [key: string]: any } {
|
||||
public toObject(): QueryRow {
|
||||
const ctor = this.constructor as typeof Model
|
||||
const obj = {}
|
||||
const obj: QueryRow = {}
|
||||
|
||||
getFieldsMeta(this).each(field => {
|
||||
// @ts-ignore
|
||||
obj[field.modelKey] = this[field.modelKey]
|
||||
obj[String(field.modelKey)] = (this as any)[field.modelKey]
|
||||
})
|
||||
|
||||
ctor.appends.forEach(field => {
|
||||
// @ts-ignore
|
||||
obj[field] = this[field]
|
||||
obj[field] = (this as any)[field]
|
||||
})
|
||||
|
||||
ctor.masks.forEach(field => {
|
||||
// @ts-ignore
|
||||
delete obj[field]
|
||||
})
|
||||
|
||||
@ -673,8 +683,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* Only fields with `@Field()` annotations are included.
|
||||
*/
|
||||
public toJSON(): string {
|
||||
return JSON.stringify(this.toObject())
|
||||
public toJSON(): QueryRow {
|
||||
return this.toObject()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -696,7 +706,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* Overwrites any un-persisted changes in the current instance.
|
||||
*/
|
||||
public async refresh() {
|
||||
public async refresh(): Promise<void> {
|
||||
const results = this.query()
|
||||
.clearFields()
|
||||
.fields(...this.getLoadedDatabaseFields())
|
||||
@ -705,7 +715,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
.get()
|
||||
|
||||
const row = await results.first()
|
||||
if ( row ) await this.assumeFromSource(row)
|
||||
if ( row ) {
|
||||
await this.assumeFromSource(row)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -766,10 +778,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected get _isDirty() {
|
||||
protected get isDirtyCheck(): (field: ModelField) => boolean {
|
||||
return (field: ModelField) => {
|
||||
// @ts-ignore
|
||||
return this[field.modelKey] !== this._original[field.databaseKey]
|
||||
return !this.originalSourceRow || (this as any)[field.modelKey] !== this.originalSourceRow[field.databaseKey]
|
||||
}
|
||||
}
|
||||
|
||||
@ -778,15 +789,18 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
* @protected
|
||||
*/
|
||||
protected getLoadedDatabaseFields(): string[] {
|
||||
if ( !this._original ) return []
|
||||
return Object.keys(this._original).map(String)
|
||||
if ( !this.originalSourceRow ) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.keys(this.originalSourceRow).map(String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an object mapping database fields to the values that should be inserted for them.
|
||||
* @private
|
||||
*/
|
||||
private _buildInsertFieldObject(): EscapeValueObject {
|
||||
private buildInsertFieldObject(): EscapeValueObject {
|
||||
const ctor = this.constructor as typeof Model
|
||||
|
||||
return getFieldsMeta(this)
|
||||
@ -795,19 +809,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
|
||||
return fields.where('modelKey', '!=', this.keyName())
|
||||
})
|
||||
.get()
|
||||
// @ts-ignore
|
||||
.keyMap('databaseKey', inst => this[inst.modelKey])
|
||||
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a property on `this` to the value of a given property in `object`.
|
||||
* @param this_field_name
|
||||
* @param object_field_name
|
||||
* @param thisFieldName
|
||||
* @param objectFieldName
|
||||
* @param object
|
||||
* @protected
|
||||
*/
|
||||
protected setFieldFromObject(this_field_name: string | symbol, object_field_name: string, object: { [key: string]: any }) {
|
||||
// @ts-ignore
|
||||
this[this_field_name] = object[object_field_name]
|
||||
protected setFieldFromObject(thisFieldName: string | symbol, objectFieldName: string, object: QueryRow): void {
|
||||
(this as any)[thisFieldName] = object[objectFieldName]
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {Model} from "./Model";
|
||||
import {AbstractBuilder} from "../builder/AbstractBuilder";
|
||||
import {AbstractResultIterable} from "../builder/result/AbstractResultIterable";
|
||||
import {Instantiable} from "../../di";
|
||||
import {ModelResultIterable} from "./ModelResultIterable";
|
||||
import {Model} from './Model'
|
||||
import {AbstractBuilder} from '../builder/AbstractBuilder'
|
||||
import {AbstractResultIterable} from '../builder/result/AbstractResultIterable'
|
||||
import {Instantiable} from '../../di'
|
||||
import {ModelResultIterable} from './ModelResultIterable'
|
||||
|
||||
/**
|
||||
* Implementation of the abstract builder whose results yield instances of a given Model, `T`.
|
||||
@ -10,16 +10,16 @@ import {ModelResultIterable} from "./ModelResultIterable";
|
||||
export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
||||
constructor(
|
||||
/** The model class that is created for results of this query. */
|
||||
protected readonly ModelClass: Instantiable<T>
|
||||
protected readonly ModelClass: Instantiable<T>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
public getNewInstance(): AbstractBuilder<T> {
|
||||
return this.app().make<ModelBuilder<T>>(ModelBuilder)
|
||||
}
|
||||
|
||||
public getResultIterable(): AbstractResultIterable<T> {
|
||||
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this, this._connection, this.ModelClass)
|
||||
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this, this.registeredConnection, this.ModelClass)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Model} from "./Model";
|
||||
import {AbstractResultIterable} from "../builder/result/AbstractResultIterable";
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {ModelBuilder} from "./ModelBuilder";
|
||||
import {Container, Instantiable} from "../../di";
|
||||
import {QueryRow} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
import {Model} from './Model'
|
||||
import {AbstractResultIterable} from '../builder/result/AbstractResultIterable'
|
||||
import {Connection} from '../connection/Connection'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {Container, Instantiable} from '../../di'
|
||||
import {QueryRow} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
|
||||
/**
|
||||
* Implementation of the result iterable that returns query results as instances of the defined model.
|
||||
@ -14,14 +14,16 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
public readonly builder: ModelBuilder<T>,
|
||||
public readonly connection: Connection,
|
||||
/** The model that should be instantiated for each row. */
|
||||
protected readonly ModelClass: Instantiable<T>
|
||||
) { super(builder, connection) }
|
||||
protected readonly ModelClass: Instantiable<T>,
|
||||
) {
|
||||
super(builder, connection)
|
||||
}
|
||||
|
||||
public get selectSQL() {
|
||||
public get selectSQL(): string {
|
||||
return this.connection.dialect().renderSelect(this.builder)
|
||||
}
|
||||
|
||||
async at(i: number) {
|
||||
async at(i: number): Promise<T | undefined> {
|
||||
const query = this.connection.dialect().renderRangedSelect(this.selectSQL, i, i + 1)
|
||||
const row = (await this.connection.query(query)).rows.first()
|
||||
|
||||
@ -35,7 +37,7 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
return (await this.connection.query(query)).rows.promiseMap<T>(row => this.inflateRow(row))
|
||||
}
|
||||
|
||||
async count() {
|
||||
async count(): Promise<number> {
|
||||
const query = this.connection.dialect().renderCount(this.selectSQL)
|
||||
const result = (await this.connection.query(query)).rows.first()
|
||||
return result?.extollo_render_count ?? 0
|
||||
@ -52,10 +54,11 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
* @protected
|
||||
*/
|
||||
protected async inflateRow(row: QueryRow): Promise<T> {
|
||||
return Container.getContainer().make<T>(this.ModelClass).assumeFromSource(row)
|
||||
return Container.getContainer().make<T>(this.ModelClass)
|
||||
.assumeFromSource(row)
|
||||
}
|
||||
|
||||
clone() {
|
||||
clone(): ModelResultIterable<T> {
|
||||
return new ModelResultIterable(this.builder, this.connection, this.ModelClass)
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {Inject, Singleton} from "../../di";
|
||||
import {DatabaseService} from "../DatabaseService";
|
||||
import {PostgresConnection} from "../connection/PostgresConnection";
|
||||
import {ErrorWithContext} from "../../util";
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {Config} from "../../service/Config";
|
||||
import {Logging} from "../../service/Logging";
|
||||
import {Inject, Singleton} from '../../di'
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {PostgresConnection} from '../connection/PostgresConnection'
|
||||
import {ErrorWithContext} from '../../util'
|
||||
import {Unit} from '../../lifecycle/Unit'
|
||||
import {Config} from '../../service/Config'
|
||||
import {Logging} from '../../service/Logging'
|
||||
|
||||
/**
|
||||
* Application unit responsible for loading and creating database connections from config.
|
||||
@ -24,12 +24,15 @@ export class Database extends Unit {
|
||||
* Load the `database.connections` config and register Connection instances for each config.
|
||||
* Automatically initializes the connections.
|
||||
*/
|
||||
public async up() {
|
||||
public async up(): Promise<void> {
|
||||
const connections = this.config.get('database.connections')
|
||||
const promises = []
|
||||
|
||||
for ( const key in connections ) {
|
||||
if ( !connections.hasOwnProperty(key) ) continue
|
||||
if ( !Object.prototype.hasOwnProperty.call(connections, key) ) {
|
||||
continue
|
||||
}
|
||||
|
||||
const config = connections[key]
|
||||
|
||||
this.logging.info(`Initializing database connection: ${key}`)
|
||||
@ -55,7 +58,7 @@ export class Database extends Unit {
|
||||
/**
|
||||
* Close the configured connections cleanly before exit.
|
||||
*/
|
||||
public async down() {
|
||||
public async down(): Promise<void> {
|
||||
await Promise.all(this.dbService.names()
|
||||
.map(name => this.dbService.get(name).close()))
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {Model} from "../model/Model";
|
||||
import {Instantiable, Singleton, Inject} from "../../di";
|
||||
import {CommandLine} from "../../cli";
|
||||
import {model_template} from "../template/model";
|
||||
import {CanonicalStatic} from "../../service/CanonicalStatic";
|
||||
import {CanonicalDefinition} from "../../service/Canonical";
|
||||
import {Model} from '../model/Model'
|
||||
import {Instantiable, Singleton, Inject} from '../../di'
|
||||
import {CommandLine} from '../../cli'
|
||||
import {templateModel} from '../template/model'
|
||||
import {CanonicalStatic} from '../../service/CanonicalStatic'
|
||||
import {CanonicalDefinition} from '../../service/Canonical'
|
||||
|
||||
/**
|
||||
* Canonical unit responsible for loading the model classes defined by the application.
|
||||
@ -14,12 +14,14 @@ export class Models extends CanonicalStatic<Model<any>, Instantiable<Model<any>>
|
||||
protected readonly cli!: CommandLine
|
||||
|
||||
protected appPath = ['models']
|
||||
|
||||
protected canonicalItem = 'model'
|
||||
|
||||
protected suffix = '.model.js'
|
||||
|
||||
public async up() {
|
||||
public async up(): Promise<void> {
|
||||
await super.up()
|
||||
this.cli.registerTemplate(model_template)
|
||||
this.cli.registerTemplate(templateModel)
|
||||
}
|
||||
|
||||
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<Model<any>>> {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user