Browse Source

Start routing and pipeline rewrite

orm-types
Garrett Mills 6 months ago
parent
commit
8cf19792a6
  1. 4
      src/auth/index.ts
  2. 143
      src/auth/ui/basic/BasicLoginController.ts
  3. 24
      src/auth/ui/basic/BasicLoginFormRequest.ts
  4. 25
      src/auth/ui/basic/BasicRegisterFormRequest.ts
  5. 11
      src/di/constructable.ts
  6. 1
      src/di/index.ts
  7. 0
      src/forms/.gitkeep
  8. 62
      src/forms/FormRequest.ts
  9. 109
      src/forms/Validator.ts
  10. 9
      src/forms/index.ts
  11. 34
      src/forms/middleware.ts
  12. 150
      src/forms/rules/arrays.ts
  13. 80
      src/forms/rules/inference.ts
  14. 210
      src/forms/rules/numeric.ts
  15. 191
      src/forms/rules/presence.ts
  16. 31
      src/forms/rules/provided/DateValidator.ts
  17. 5
      src/forms/rules/rules.ts
  18. 264
      src/forms/rules/strings.ts
  19. 85
      src/forms/rules/types.ts
  20. 44
      src/forms/templates/form.ts
  21. 18
      src/forms/unit/Forms.ts
  22. 14
      src/http/routing/Route.ts
  23. 209
      src/http/routing/Route2.ts
  24. 11
      src/orm/model/Model.ts
  25. 52
      src/orm/schema/PostgresSchema.ts
  26. 35
      src/orm/schema/TableBuilder.ts
  27. 6
      src/service/Routing.ts
  28. 17
      src/util/collection/AsyncCollection.ts
  29. 11
      src/util/collection/Collection.ts
  30. 13
      src/util/support/Messages.ts
  31. 136
      src/util/support/Pipe.ts
  32. 15
      src/util/support/path.ts
  33. 26
      src/util/support/types.ts
  34. 75
      src/validation/Validator.ts

4
src/auth/index.ts

@ -14,8 +14,4 @@ export * from './event/UserFlushedEvent'
export * from './repository/orm/ORMUser'
export * from './repository/orm/ORMUserRepository'
export * from './ui/basic/BasicRegisterFormRequest'
export * from './ui/basic/BasicLoginFormRequest'
export * from './ui/basic/BasicLoginController'
export * from './config'

143
src/auth/ui/basic/BasicLoginController.ts

@ -1,143 +0,0 @@
import {Controller} from '../../../http/Controller'
import {Inject, Injectable} from '../../../di'
import {SecurityContext} from '../../context/SecurityContext'
import {Session} from '../../../http/session/Session'
import {ResponseFactory} from '../../../http/response/ResponseFactory'
import {redirectToGet, view} from '../../../http/response/ViewResponseFactory'
import {Routing} from '../../../service/Routing'
import {ResponseObject, Route} from '../../../http/routing/Route'
import {BasicLoginFormData, BasicLoginFormRequest} from './BasicLoginFormRequest'
import {Valid, ValidationError} from '../../../forms'
import {BasicRegisterFormData, BasicRegisterFormRequest} from './BasicRegisterFormRequest'
import {AuthenticatableAlreadyExistsError} from '../../AuthenticatableAlreadyExistsError'
import {Request} from '../../../http/lifecycle/Request'
@Injectable()
export class BasicLoginController extends Controller {
public static routes({ enableRegistration = true } = {}): void {
const controller = (request: Request) => {
return <BasicLoginController> request.make(BasicLoginController)
}
Route.group('auth', () => {
Route.get('login', (request: Request) =>
controller(request).getLogin())
.pre('@auth:guest')
.alias('@auth.login')
Route.post('login', (request: Request) =>
controller(request).attemptLogin())
.pre('@auth:guest')
.alias('@auth.login.attempt')
Route.any('logout', (request: Request) =>
controller(request).attemptLogout())
.pre('@auth:required')
.alias('@auth.logout')
if ( enableRegistration ) {
Route.get('register', (request: Request) =>
controller(request).getRegistration())
.pre('@auth:guest')
.alias('@auth.register')
Route.post('register', (request: Request) =>
controller(request).attemptRegister())
.pre('@auth:guest')
.alias('@auth.register.attempt')
}
}).pre('@auth:web')
}
@Inject()
protected readonly security!: SecurityContext
@Inject()
protected readonly session!: Session
@Inject()
protected readonly routing!: Routing
public getLogin(): ResponseFactory {
return this.getLoginView()
}
public getRegistration(): ResponseFactory {
if ( !this.security.repository.supportsRegistration() ) {
return redirectToGet('/')
}
return this.getRegistrationView()
}
public async attemptLogin(): Promise<ResponseObject> {
const form = <BasicLoginFormRequest> this.request.make(BasicLoginFormRequest)
try {
const data: Valid<BasicLoginFormData> = await form.get()
const user = await this.security.repository.getByIdentifier(data.username)
if ( user && (await user.validateCredential(data.password)) ) {
await this.security.authenticate(user)
const intention = this.session.get('@extollo:auth.intention', '/')
this.session.forget('@extollo:auth.intention')
return redirectToGet(intention)
}
return this.getLoginView(['Invalid username/password.'])
} catch (e: unknown) {
if ( e instanceof ValidationError ) {
return this.getLoginView(e.errors.all())
}
throw e
}
}
public async attemptRegister(): Promise<ResponseObject> {
const form = <BasicRegisterFormRequest> this.request.make(BasicRegisterFormRequest)
try {
const data: Valid<BasicRegisterFormData> = await form.get()
const user = await this.security.repository.createFromCredentials(data.username, data.password)
await this.security.authenticate(user)
const intention = this.session.get('@extollo:auth.intention', '/')
this.session.forget('@extollo:auth.intention')
return redirectToGet(intention)
} catch (e: unknown) {
if ( e instanceof ValidationError ) {
return this.getRegistrationView(e.errors.all())
} else if ( e instanceof AuthenticatableAlreadyExistsError ) {
return this.getRegistrationView(['A user with that username already exists.'])
}
throw e
}
}
public async attemptLogout(): Promise<ResponseObject> {
await this.security.flush()
return this.getMessageView('You have been logged out.')
}
protected getRegistrationView(errors?: string[]): ResponseFactory {
return view('@extollo:auth:register', {
formAction: this.routing.getNamedPath('@auth.register.attempt').toRemote,
errors,
})
}
protected getLoginView(errors?: string[]): ResponseFactory {
return view('@extollo:auth:login', {
formAction: this.routing.getNamedPath('@auth.login.attempt').toRemote,
errors,
})
}
protected getMessageView(message: string): ResponseFactory {
return view('@extollo:auth:message', {
message,
})
}
}

24
src/auth/ui/basic/BasicLoginFormRequest.ts

@ -1,24 +0,0 @@
import {FormRequest, ValidationRules} from '../../../forms'
import {Is, Str} from '../../../forms/rules/rules'
import {Singleton} from '../../../di'
export interface BasicLoginFormData {
username: string,
password: string,
}
@Singleton()
export class BasicLoginFormRequest extends FormRequest<BasicLoginFormData> {
protected getRules(): ValidationRules {
return {
username: [
Is.required,
Str.lengthMin(1),
],
password: [
Is.required,
Str.lengthMin(1),
],
}
}
}

25
src/auth/ui/basic/BasicRegisterFormRequest.ts

@ -1,25 +0,0 @@
import {FormRequest, ValidationRules} from '../../../forms'
import {Is, Str} from '../../../forms/rules/rules'
import {Singleton} from '../../../di'
export interface BasicRegisterFormData {
username: string,
password: string,
}
@Singleton()
export class BasicRegisterFormRequest extends FormRequest<BasicRegisterFormData> {
protected getRules(): ValidationRules {
return {
username: [
Is.required,
Str.lengthMin(1),
],
password: [
Is.required,
Str.lengthMin(1),
Str.confirmed,
],
}
}
}

11
src/di/constructable.ts

@ -0,0 +1,11 @@
import {Container} from './Container'
import {TypedDependencyKey} from './types'
import {Pipeline} from '../util'
export type Constructable<T> = Pipeline<Container, T>
export function constructable<T>(key: TypedDependencyKey<T>): Constructable<T> {
return new Pipeline<Container, T>(
container => container.make(key),
)
}

1
src/di/index.ts

@ -14,3 +14,4 @@ export * from './types'
export * from './decorator/injection'
export * from './InjectionAware'
export * from './constructable'

0
src/forms/.gitkeep

62
src/forms/FormRequest.ts

@ -1,62 +0,0 @@
import {Container, Injectable, InjectParam} from '../di'
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'
/**
* Base class for defining reusable validators for request routes.
* If instantiated with a container, it must be a request-level container,
* but the type interface allows any data-container to be used when creating
* manually.
*
* You should mark implementations of this class as singleton to avoid
* re-validating the input data every time it is accessed.
*
* @example
* ```ts
* // Instantiate with the request:
* const data = <MyFormRequest> request.make(MyFormRequest)
*
* // Instantiate with some container:
* const data = new MyFormRequest(someDataContainer)
* ```
*/
@Injectable()
export abstract class FormRequest<T> extends AppClass {
/** The cached validation result. */
protected cachedResult?: Valid<T>
constructor(
@InjectParam(Request)
protected readonly data: DataContainer,
) {
super()
}
protected container(): Container {
return (this.data as unknown) as Container
}
/**
* The validation rules that should be applied to the request to guarantee
* that it contains the given data type.
* @protected
*/
protected abstract getRules(): ValidationRules | Promise<ValidationRules>
/**
* Validate and get the request input. Throws a validation error on fail.
* Internally, caches the result after the first validation. So, singleton
* validators will avoid re-processing their rules every time.
*/
public async get(): Promise<Valid<T>> {
if ( !this.cachedResult ) {
const validator = <Validator<T>> this.make(Validator, await this.getRules())
this.cachedResult = await validator.validate(this.data.input())
}
return this.cachedResult
}
}

109
src/forms/Validator.ts

@ -1,109 +0,0 @@
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.
*/
export class ValidationError<T> extends ErrorWithContext {
constructor(
/** The original input data. */
public readonly data: unknown,
/** The validator instance used. */
public readonly validator: Validator<T>,
/** Validation error messages, by field. */
public readonly errors: Messages,
) {
super('One or more fields were invalid.', { data,
messages: errors.all() })
}
}
/**
* A class to validate arbitrary data using functional rules.
*/
export class Validator<T> {
constructor(
/** The rules used to validate input objects. */
protected readonly rules: ValidationRules,
) {}
/**
* Attempt to validate the input data.
* If it is valid, it is type aliased as Valid<T>.
* If it is invalid, a ValidationError is thrown.
* @param data
*/
public async validate(data: unknown): Promise<Valid<T>> {
const messages = await this.validateAndGetErrors(data)
if ( messages.any() ) {
throw new ValidationError<T>(data, this, messages)
}
return data as Valid<T>
}
/**
* Returns true if the given data is valid and type aliases it as Valid<T>.
* @param data
*/
public async isValid(data: unknown): Promise<boolean> {
return !(await this.validateAndGetErrors(data)).any()
}
/**
* Apply the validation rules to the data object and return any error messages.
* @param data
* @protected
*/
protected async validateAndGetErrors(data: unknown): Promise<Messages> {
const messages = new Messages()
const params: ValidatorFunctionParams = { data }
for ( const key in this.rules ) {
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 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 ) {
const result: ValidationResult = await rule(dataKey, entry, params)
if ( !result.valid ) {
let errors = ['is invalid']
if ( Array.isArray(result.message) && result.message.length ) {
errors = result.message
} else if ( !Array.isArray(result.message) && result.message ) {
errors = [result.message]
}
for ( const error of errors ) {
if ( !messages.has(dataKey, error) ) {
messages.put(dataKey, error)
}
}
}
if ( result.valid && result.castValue ) {
entry = result.castValue
data = dataSetUnsafe(dataKey, entry, data as any)
}
if ( result.stopValidation ) {
break // move on to the next field
}
}
}
}
return messages
}
}

9
src/forms/index.ts

@ -1,9 +0,0 @@
export * from './rules/types'
export * as Rule from './rules/rules'
export * from './unit/Forms'
export * from './Validator'
export * from './FormRequest'
export * from './middleware'

34
src/forms/middleware.ts

@ -1,34 +0,0 @@
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'
/**
* Builds a middleware function that validates a request's input against
* the given form request class and registers the FormRequest class into
* the request container.
*
* @example
* ```typescript
* Route.group(...).pre(formRequest(MyFormRequestClass))
* ```
*
* @param formRequestClass
*/
export function formRequest<T>(formRequestClass: Instantiable<FormRequest<T>>): RouteHandler {
return async function formRequestRouteHandler(request: Request): Promise<ResponseObject> {
const formRequestInstance = <FormRequest<T>> request.make(formRequestClass)
try {
await formRequestInstance.get()
request.registerSingletonInstance<FormRequest<T>>(formRequestClass, formRequestInstance)
} catch (e: unknown) {
if ( e instanceof ValidationError ) {
return e.errors.toJSON()
}
throw e
}
}
}

150
src/forms/rules/arrays.ts

@ -1,150 +0,0 @@
import {ValidationResult, ValidatorFunction} from './types'
/** Requires the input value to be an array. */
function is(fieldName: string, inputValue: unknown): ValidationResult {
if ( Array.isArray(inputValue) ) {
return { valid: true }
}
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 ( Array.isArray(inputValue) && 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
*/
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,
}

80
src/forms/rules/inference.ts

@ -1,80 +0,0 @@
import {infer as inferUtil} from '../../util'
import {ValidationResult} from './types'
/** 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,
}

210
src/forms/rules/numeric.ts

@ -1,210 +0,0 @@
import {ValidationResult, ValidatorFunction} from './types'
/**
* 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 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,
}

191
src/forms/rules/presence.ts

@ -1,191 +0,0 @@
import {ValidationResult, ValidatorFunction} from './types'
import {UniversalPath} from '../../util'
/** 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,
}

31
src/forms/rules/provided/DateValidator.ts

@ -1,31 +0,0 @@
/*
import {Injectable} from '@extollo/di'
import {Validator} from '../Validator'
import {ValidationResult} from "../types";
@Injectable()
export class DateValidator extends Validator {
protected names: string[] = [
'date',
'date.after',
'date.at_least',
'date.before',
'date.at_most',
'date.equals',
'date.format',
]
public matchName(name: string): boolean {
return this.names.includes(name)
}
validate(fieldName: string, inputValue: any, params: { name: string; params: any }): ValidationResult {
switch ( params.name ) {
}
return { valid: false }
}
}
*/

5
src/forms/rules/rules.ts

@ -1,5 +0,0 @@
export { Arr } from './arrays'
export { Cast } from './inference'
export { Num } from './numeric'
export { Is } from './presence'
export { Str } from './strings'

264
src/forms/rules/strings.ts

@ -1,264 +0,0 @@
import {ValidationResult, ValidatorFunction, ValidatorFunctionParams} from './types'
import {isJSON} from '../../util'
/**
* String-related validation rules.
*/
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 }
}
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: '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}`,
}
}
}
/**
* Validator function that requires the input value to match a `${field}Confirm` field's value.
* @param fieldName
* @param inputValue
* @param params
*/
function confirmed(fieldName: string, inputValue: unknown, params: ValidatorFunctionParams): ValidationResult {
const confirmedFieldName = `${fieldName}Confirm`
if ( inputValue === params.data[confirmedFieldName] ) {
return { valid: true }
}
return {
valid: false,
message: `confirmation does not match`,
}
}
export const Str = {
alpha,
alphaNum,
alphaDash,
alphaScore,
alphaNumDashScore,
email,
ip,
ipv4,
ipv6,
mime,
url,
uuid,
regex,
notRegex,
ends,
begins,
json,
length,
lengthMin,
lengthMax,
confirmed,
}

85
src/forms/rules/types.ts

@ -1,85 +0,0 @@
/**
* Additional parameters passed to complex validation functions.
*/
export interface ValidatorFunctionParams {
/** The entire original input data. */
data: any,
}
/**
* An interface representing the result of an attempted validation that failed.
*/
export interface ValidationErrorResult {
/** Whether or not the validation succeeded. */
valid: false
/**
* The human-readable error message(s) describing the issue.
*/
message?: string | string[]
/**
* If true, validation of subsequent fields will stop.
*/
stopValidation?: boolean
}
/**
* An interface representing the result of an attempted validation that succeeded.
*/
export interface ValidationSuccessResult {
/** Whether or not the validation succeeded. */
valid: true
/**
* If the value was cast to a different type, or inferred, as a result of this validation,
* provide it here. It will replace the input string as the value of the field in the form.
*/
castValue?: any
/**
* If true, validation of subsequent fields will stop.
*/
stopValidation?: boolean
}
/** All possible results of an attempted validation. */
export type ValidationResult = ValidationErrorResult | ValidationSuccessResult
/** A validator function that takes only the field key and the object value. */
export type SimpleValidatorFunction = (fieldName: string, inputValue: any) => ValidationResult | Promise<ValidationResult>
/** A validator function that takes the field key, the object value, and an object of contextual params. */
export type ComplexValidatorFunction = (fieldName: string, inputValue: any, params: ValidatorFunctionParams) => ValidationResult | Promise<ValidationResult>
/** Useful type alias for all allowed validator function signatures. */
export type ValidatorFunction = SimpleValidatorFunction | ComplexValidatorFunction
/**
* A set of validation rules that are applied to input objects on validators.
*
* The keys of this object are deep-nested keys and can be used to validate
* nested properties.
*
* For example, the key "user.links.*.url" refers to the "url" property of all
* objects in the "links" array on the "user" object on:
*
* ```json
* {
* "user": {
* "links": [
* {
* "url": "..."
* },
* {
* "url": "..."
* }
* ]
* }
* }
* ```
*/
export type ValidationRules = {[key: string]: ValidatorFunction | ValidatorFunction[]}
/** A type alias denoting that a particular type has been validated. */
export type Valid<T> = T

44
src/forms/templates/form.ts

@ -1,44 +0,0 @@
import {Template} from '../../cli'
const templateForm: Template = {
name: 'form',
fileSuffix: '.form.ts',
description: 'Create a new form request validator',
baseAppPath: ['http', 'forms'],
render(name: string) {
return `import {Injectable, FormRequest, ValidationRules, Rule} from '@extollo/lib'
/**
* ${name} object
* ----------------------------
* This is the object interface that is guaranteed when
* all of the validation rules below pass.
*/
export interface ${name}Form {
}
/**
* ${name}Request validator
* ----------------------------
* Request validator that defines the rules needed to guarantee
* that a request's input conforms to the interface defined above.
*/
@Injectable()
export class ${name}FormRequest extends FormRequest<${name}Form> {
/**
* The validation rules that should be applied to the various
* request input fields.
* @protected
*/
protected getRules(): ValidationRules {
return {
}
}
}
`
},
}
export { templateForm }

18
src/forms/unit/Forms.ts

@ -1,18 +0,0 @@
import {Singleton, Inject} from '../../di'
import {CommandLine} from '../../cli'
import {templateForm} from '../templates/form'
import {Unit} from '../../lifecycle/Unit'
import {Logging} from '../../service/Logging'
@Singleton()
export class Forms extends Unit {
@Inject()
protected readonly cli!: CommandLine
@Inject()
protected readonly logging!: Logging
public async up(): Promise<void> {
this.cli.registerTemplate(templateForm)
}
}

14
src/http/routing/Route.ts

@ -11,6 +11,7 @@ import {Controller} from '../Controller'
import {Middlewares} from '../../service/Middlewares'
import {Middleware} from './Middleware'
import {Config} from '../../service/Config'
import {Validator} from '../../validation/Validator'
/**
* Type alias for an item that is a valid response object, or lack thereof.
@ -66,7 +67,7 @@ export class Route extends AppClass {
}
/**
* Load and compile all of the registered routes and their groups, accounting
* Load and compile all the registered routes and their groups, accounting
* for <