Import other modules into monorepo
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2021-06-01 20:59:40 -05:00
parent 26d54033af
commit 9be9c44a32
138 changed files with 11544 additions and 139 deletions

0
src/forms/.gitkeep Normal file
View File

60
src/forms/FormRequest.ts Normal file
View File

@@ -0,0 +1,60 @@
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() {
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
}
}

104
src/forms/Validator.ts Normal file
View File

@@ -0,0 +1,104 @@
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: any,
/** 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: any): 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: any): Promise<Messages> {
const messages = new Messages()
const params: ValidatorFunctionParams = { data }
for ( const key in this.rules ) {
if ( !this.rules.hasOwnProperty(key) ) continue;
// This walks over all of the values in the data structure using the nested
// key notation. It's not type-safe, but neither is the original input object
// yet, so it's useful here.
for ( const walkEntry of dataWalkUnsafe<any>(data, key) ) {
let [entry, dataKey] = walkEntry
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)
}
if ( result.stopValidation ) {
break // move on to the next field
}
}
}
}
return messages
}
}

9
src/forms/index.ts Normal file
View File

@@ -0,0 +1,9 @@
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 Normal file
View File

@@ -0,0 +1,34 @@
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
}
}
}

130
src/forms/rules/arrays.ts Normal file
View File

@@ -0,0 +1,130 @@
import {ValidationResult, ValidatorFunction} from "./types";
export namespace Arr {
/** Requires the input value to be an array. */
export function is(fieldName: string, inputValue: any): ValidationResult {
if ( Array.isArray(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be an array'
}
}
/** Requires the values in the input value array to be distinct. */
export function distinct(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( (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
*/
export function includes(value: any): ValidatorFunction {
return function includes(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( inputValue.includes(value) ) {
return { valid: true }
}
return {
valid: false,
message: `must include ${value}`
}
}
}
/**
* Builds a validator function that requires the input array NOT to contain the given value.
* @param value
*/
export function excludes(value: any): ValidatorFunction {
return function excludes(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( !inputValue.includes(value) ) {
return { valid: true }
}
return {
valid: false,
message: `must not include ${value}`
}
}
}
/**
* Builds a validator function that requires the input array to have exactly `len` many entries.
* @param len
*/
export function length(len: number): ValidatorFunction {
return function length(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( inputValue.length === len ) {
return { valid: true }
}
return {
valid: false,
message: `must be exactly of length ${len}`
}
}
}
/**
* Builds a validator function that requires the input array to have at least `len` many entries.
* @param len
*/
export function lengthMin(len: number): ValidatorFunction {
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( inputValue.length >= len ) {
return { valid: true }
}
return {
valid: false,
message: `must be at least length ${len}`
}
}
}
/**
* Builds a validator function that requires the input array to have at most `len` many entries.
* @param len
*/
export function lengthMax(len: number): ValidatorFunction {
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
const arr = is(fieldName, inputValue)
if ( !arr.valid ) return arr
if ( inputValue.length <= len ) {
return { valid: true }
}
return {
valid: false,
message: `must be at most length ${len}`
}
}
}
}

View File

@@ -0,0 +1,70 @@
import {infer as inferUtil} from '../../util'
import {ValidationResult} from "./types";
export namespace Cast {
/** Attempt to infer the native type of a string value. */
export function infer(fieldName: string, inputValue: any): ValidationResult {
return {
valid: true,
castValue: typeof inputValue === 'string' ? inferUtil(inputValue) : inputValue,
}
}
/**
* Casts the input value to a boolean.
* Note that this assumes the value may be boolish. The strings "true", "True",
* "TRUE", and "1" evaluate to `true`, while "false", "False", "FALSE", and "0"
* evaluate to `false`.
* @param fieldName
* @param inputValue
*/
export function boolean(fieldName: string, inputValue: any): ValidationResult {
let castValue = !!inputValue
if ( ['true', 'True', 'TRUE', '1'].includes(inputValue) ) castValue = true
if ( ['false', 'False', 'FALSE', '0'].includes(inputValue) ) castValue = false
return {
valid: true,
castValue,
}
}
/** Casts the input value to a string. */
export function string(fieldName: string, inputValue: any): ValidationResult {
return {
valid: true,
castValue: String(inputValue),
}
}
/** Casts the input value to a number, if it is numerical. Fails otherwise. */
export function numeric(fieldName: string, inputValue: any): ValidationResult {
if ( !isNaN(parseFloat(inputValue)) ) {
return {
valid: true,
castValue: parseFloat(inputValue)
}
}
return {
valid: false,
message: 'must be numeric',
}
}
/** Casts the input value to an integer. Fails otherwise. */
export function integer(fieldName: string, inputValue: any): ValidationResult {
if ( !isNaN(parseInt(inputValue)) ) {
return {
valid: true,
castValue: parseInt(inputValue)
}
}
return {
valid: false,
message: 'must be an integer',
}
}
}

197
src/forms/rules/numeric.ts Normal file
View File

@@ -0,0 +1,197 @@
import {ValidationResult, ValidatorFunction} from "./types";
export namespace Num {
/**
* Builds a validator function that requires the input value to be greater than some value.
* @param value
*/
export function greaterThan(value: number): ValidatorFunction {
return function greaterThan(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue > value ) {
return { valid: true }
}
return {
valid: false,
message: `must be greater than ${value}`
}
}
}
/**
* Builds a validator function that requires the input value to be at least some value.
* @param value
*/
export function atLeast(value: number): ValidatorFunction {
return function atLeast(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue >= value ) {
return { valid: true }
}
return {
valid: false,
message: `must be at least ${value}`
}
}
}
/**
* Builds a validator function that requires the input value to be less than some value.
* @param value
*/
export function lessThan(value: number): ValidatorFunction {
return function lessThan(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue < value ) {
return { valid: true }
}
return {
valid: false,
message: `must be less than ${value}`
}
}
}
/**
* Builds a validator function that requires the input value to be at most some value.
* @param value
*/
export function atMost(value: number): ValidatorFunction {
return function atMost(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue <= value ) {
return { valid: true }
}
return {
valid: false,
message: `must be at most ${value}`
}
}
}
/**
* Builds a validator function that requires the input value to have exactly `num` many digits.
* @param num
*/
export function digits(num: number): ValidatorFunction {
return function digits(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).replace('.', '').length === num ) {
return { valid: true }
}
return {
valid: false,
message: `must have exactly ${num} digits`
}
}
}
/**
* Builds a validator function that requires the input value to have at least `num` many digits.
* @param num
*/
export function digitsMin(num: number): ValidatorFunction {
return function digitsMin(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).replace('.', '').length >= num ) {
return { valid: true }
}
return {
valid: false,
message: `must have at least ${num} digits`
}
}
}
/**
* Builds a validator function that requires the input value to have at most `num` many digits.
* @param num
*/
export function digitsMax(num: number): ValidatorFunction {
return function digitsMax(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).replace('.', '').length <= num ) {
return { valid: true }
}
return {
valid: false,
message: `must have at most ${num} digits`
}
}
}
/**
* Builds a validator function that requires the input value to end with the given number sequence.
* @param num
*/
export function ends(num: number): ValidatorFunction {
return function ends(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).endsWith(String(num)) ) {
return { valid: true }
}
return {
valid: false,
message: `must end with "${num}"`
}
}
}
/**
* Builds a validator function that requires the input value to begin with the given number sequence.
* @param num
*/
export function begins(num: number): ValidatorFunction {
return function begins(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).startsWith(String(num)) ) {
return { valid: true }
}
return {
valid: false,
message: `must begin with "${num}"`
}
}
}
/**
* Builds a validator function that requires the input value to be a multiple of the given number.
* @param num
*/
export function multipleOf(num: number): ValidatorFunction {
return function multipleOf(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue % num === 0 ) {
return { valid: true }
}
return {
valid: false,
message: `must be a multiple of ${num}`
}
}
}
/** Requires the input value to be even. */
export function even(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue % 2 === 0 ) {
return { valid: true }
}
return {
valid: false,
message: 'must be even',
}
}
/** Requires the input value to be odd. */
export function odd(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue % 2 === 0 ) {
return { valid: true }
}
return {
valid: false,
message: 'must be odd',
}
}
}

175
src/forms/rules/presence.ts Normal file
View File

@@ -0,0 +1,175 @@
import {ValidationResult, ValidatorFunction} from "./types";
import {UniversalPath} from '../../util'
export namespace Is {
/** Requires the given input value to be some form of affirmative boolean. */
export function accepted(fieldName: string, inputValue: any): ValidationResult {
if ( ['yes', 'Yes', 'YES', 1, true, 'true', 'True', 'TRUE'].includes(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be accepted'
}
}
/** Requires the given input value to be some form of boolean. */
export function boolean(fieldName: string, inputValue: any): ValidationResult {
const boolish = ['true', 'True', 'TRUE', '1', 'false', 'False', 'FALSE', '0', true, false, 1, 0]
if ( boolish.includes(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be true or false'
}
}
/** Requires the input value to be of type string. */
export function string(fieldName: string, inputValue: any): ValidationResult {
if ( typeof inputValue === 'string' ) {
return { valid: true }
}
return {
valid: false,
message: 'must be a string'
}
}
/** Requires the given input value to be present and non-nullish. */
export function required(fieldName: string, inputValue: any): ValidationResult {
if ( typeof inputValue !== 'undefined' && inputValue !== null && inputValue !== '' ) {
return { valid: true }
}
return {
valid: false,
message: 'is required',
stopValidation: true,
}
}
/** Alias of required(). */
export function present(fieldName: string, inputValue: any): ValidationResult {
return required(fieldName, inputValue)
}
/** Alias of required(). */
export function filled(fieldName: string, inputValue: any): ValidationResult {
return required(fieldName, inputValue)
}
/** Requires the given input value to be absent or nullish. */
export function prohibited(fieldName: string, inputValue: any): ValidationResult {
if ( typeof inputValue === 'undefined' || inputValue === null || inputValue === '' ) {
return { valid: true }
}
return {
valid: false,
message: 'is not allowed',
stopValidation: true,
}
}
/** Alias of prohibited(). */
export function absent(fieldName: string, inputValue: any): ValidationResult {
return prohibited(fieldName, inputValue)
}
/** Alias of prohibited(). */
export function empty(fieldName: string, inputValue: any): ValidationResult {
return prohibited(fieldName, inputValue)
}
/**
* Builds a validator function that requires the given input to be found in an array of values.
* @param values
*/
export function foundIn(values: any[]): ValidatorFunction {
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
if ( values.includes(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: `must be one of: ${values.join(', ')}`
}
}
}
/**
* Builds a validator function that requires the given input NOT to be found in an array of values.
* @param values
*/
export function notFoundIn(values: any[]): ValidatorFunction {
return function foundIn(fieldName: string, inputValue: any): ValidationResult {
if ( values.includes(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: `must be one of: ${values.join(', ')}`
}
}
}
/** Requires the input value to be number-like. */
export function numeric(fieldName: string, inputValue: any): ValidationResult {
if ( !isNaN(parseFloat(inputValue)) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be numeric',
}
}
/** Requires the given input value to be integer-like. */
export function integer(fieldName: string, inputValue: any): ValidationResult {
if ( !isNaN(parseInt(inputValue)) && parseInt(inputValue) === parseFloat(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be an integer',
}
}
/** Requires the given input value to be a UniversalPath. */
export function file(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue instanceof UniversalPath ) {
return { valid: true }
}
return {
valid: false,
message: 'must be a file'
}
}
/**
* A special validator function that marks a field as optional.
* If the value of the field is nullish, no further validation rules will be applied.
* If it is non-nullish, validation will continue.
* @param fieldName
* @param inputValue
*/
export function optional(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue ?? true ) {
return {
valid: true,
stopValidation: true,
}
}
return { valid: true }
}
}

View File

@@ -0,0 +1,31 @@
/*
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 Normal file
View File

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

224
src/forms/rules/strings.ts Normal file
View File

@@ -0,0 +1,224 @@
import {ValidationResult, ValidatorFunction} from "./types";
import {isJSON} from '../../util'
/**
* String-related validation rules.
*/
export namespace Str {
const regexes: {[key: string]: RegExp} = {
'string.is.alpha': /[a-zA-Z]*/,
'string.is.alpha_num': /[a-zA-Z0-9]*/,
'string.is.alpha_dash': /[a-zA-Z\-]*/,
'string.is.alpha_score': /[a-zA-Z_]*/,
'string.is.alpha_num_dash_score': /[a-zA-Z\-_0-9]*/,
'string.is.email': /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])/,
'string.is.ip': /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/,
'string.is.ip.v4': /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/,
'string.is.ip.v6': /(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))/,
'string.is.mime': /^(?=[-a-z]{1,127}\/[-.a-z0-9]{1,127}$)[a-z]+(-[a-z]+)*\/[a-z0-9]+([-.][a-z0-9]+)*$/,
'string.is.url': /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=+$,\w]+@)?[A-Za-z0-9.\-]+|(?:www\.|[\-;:&=+$,\w]+@)[A-Za-z0-9.\-]+)((?:\/[+~%\/.\w\-_]*)?\??(?:[\-+=&;%@.\w_]*)#?(?:[.!\/\\\w]*))?)/,
'string.is.uuid': /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/,
}
function validateRex(key: string, inputValue: any, message: string): ValidationResult {
if ( regexes[key].test(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message
}
}
/** Requires the input value to be alphabetical characters only. */
export function alpha(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.alpha', inputValue, 'must be alphabetical only')
}
/** Requires the input value to be alphanumeric characters only. */
export function alphaNum(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.alpha_num', inputValue, 'must be alphanumeric only')
}
/** Requires the input value to be alphabetical characters or the "-" character only. */
export function alphaDash(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.alpha_dash', inputValue, 'must be alphabetical and dashes only')
}
/** Requires the input value to be alphabetical characters or the "_" character only. */
export function alphaScore(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.alpha_score', inputValue, 'must be alphabetical and underscores only')
}
/** Requires the input value to be alphabetical characters, numeric characters, "-", or "_" only. */
export function alphaNumDashScore(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.alpha_num_dash_score', inputValue, 'must be alphanumeric, dashes, and underscores only')
}
/** Requires the input value to be a valid RFC email address format. */
export function email(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.email', inputValue, 'must be an email address')
}
/** Requires the input value to be a valid IPv4 or IPv6 address. */
export function ip(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.ip', inputValue, 'must be a valid IP address')
}
/** Requires the input value to be a valid IPv4 address. */
export function ipv4(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.ip.v4', inputValue, 'must be a valid IP version 4 address')
}
/** Requires the input value to be a valid IPv6 address. */
export function ipv6(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.ip.v6', inputValue, 'must be a valid IP version 6 address')
}
/** Requires the input value to be a valid file MIME type. */
export function mime(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.mime', inputValue, 'must be a valid MIME-type')
}
/** Requires the input value to be a valid RFC URL format. */
export function url(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.url', inputValue, 'must be a valid URL')
}
/** Requires the input value to be a valid RFC UUID format. */
export function uuid(fieldName: string, inputValue: any): ValidationResult {
return validateRex('string.is.uuid', inputValue, 'must be a valid UUID')
}
/**
* Builds a validation function that requires the input value to match the given regex.
* @param rex
*/
export function regex(rex: RegExp): ValidatorFunction {
return function regex(fieldName: string, inputValue: any): ValidationResult {
if ( rex.test(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'is not valid'
}
}
}
/**
* Builds a validation function that requires the input to NOT match the given regex.
* @param rex
*/
export function notRegex(rex: RegExp): ValidatorFunction {
return function notRegex(fieldName: string, inputValue: any): ValidationResult {
if ( !rex.test(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'is not valid'
}
}
}
/**
* Builds a validation function that requires the given input to end with the substring.
* @param substr
*/
export function ends(substr: string): ValidatorFunction {
return function ends(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).endsWith(substr) ) {
return { valid: true }
}
return {
valid: false,
message: `must end with "${substr}"`
}
}
}
/**
* Builds a validation function that requires the given input to begin with the substring.
* @param substr
*/
export function begins(substr: string): ValidatorFunction {
return function begins(fieldName: string, inputValue: any): ValidationResult {
if ( String(inputValue).startsWith(substr) ) {
return { valid: true }
}
return {
valid: false,
message: `must begin with "${substr}"`
}
}
}
/** Requires the input value to be a valid JSON string. */
export function json(fieldName: string, inputValue: any): ValidationResult {
if ( isJSON(inputValue) ) {
return { valid: true }
}
return {
valid: false,
message: 'must be valid JSON'
}
}
/**
* Builds a validator function that requires the input value to have exactly len many characters.
* @param len
*/
export function length(len: number): ValidatorFunction {
return function length(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue.length === len ) {
return { valid: true }
}
return {
valid: false,
message: `must be exactly of length ${len}`
}
}
}
/**
* Builds a validator function that requires the input value to have at least len many characters.
* @param len
*/
export function lengthMin(len: number): ValidatorFunction {
return function lengthMin(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue.length >= len ) {
return { valid: true }
}
return {
valid: false,
message: `must be at least length ${len}`
}
}
}
/**
* Builds a validator function that requires the input value to have at most len many characters.
* @param len
*/
export function lengthMax(len: number): ValidatorFunction {
return function lengthMax(fieldName: string, inputValue: any): ValidationResult {
if ( inputValue.length <= len ) {
return { valid: true }
}
return {
valid: false,
message: `must be at most length ${len}`
}
}
}
}

85
src/forms/rules/types.ts Normal file
View File

@@ -0,0 +1,85 @@
/**
* 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

View File

@@ -0,0 +1,46 @@
import {UniversalPath} from '../../util'
import {Template} from '../../cli'
const form_template: Template = {
name: 'form',
fileSuffix: '.form.ts',
description: 'Create a new form request validator',
baseAppPath: ['http', 'forms'],
render(name: string, fullCanonicalName: string, targetFilePath: UniversalPath) {
return `import {FormRequest, ValidationRules, Rule} from '@extollo/forms'
import {Injectable} from '@extollo/di'
/**
* ${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 { form_template }

18
src/forms/unit/Forms.ts Normal file
View File

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