34 changed files with 468 additions and 1652 deletions
@ -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, |
|||
}) |
|||
} |
|||
} |
@ -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), |
|||
], |
|||
} |
|||
} |
|||
} |
@ -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, |
|||
], |
|||
} |
|||
} |
|||
} |
@ -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,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 |
|||
} |
|||
} |
@ -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 |
|||
} |
|||
} |
@ -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' |
@ -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 |
|||
} |
|||
} |
|||
} |
@ -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, |
|||
} |
@ -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, |
|||
} |
@ -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, |
|||
} |
@ -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, |
|||
} |
@ -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 } |
|||
} |
|||
} |
|||
*/ |
@ -1,5 +0,0 @@ |
|||
export { Arr } from './arrays' |
|||
export { Cast } from './inference' |
|||
export { Num } from './numeric' |
|||
export { Is } from './presence' |
|||
export { Str } from './strings' |
@ -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, |
|||
} |
@ -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 |
@ -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 } |
@ -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) |
|||
} |
|||
} |