Start routing and pipeline rewrite
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
9b8333295f
commit
8cf19792a6
@ -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'
|
||||
|
@ -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,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
11
src/di/constructable.ts
Normal file
11
src/di/constructable.ts
Normal file
@ -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),
|
||||
)
|
||||
}
|
@ -14,3 +14,4 @@ export * from './types'
|
||||
|
||||
export * from './decorator/injection'
|
||||
export * from './InjectionAware'
|
||||
export * from './constructable'
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 nested groups and resolving handlers.
|
||||
*
|
||||
* This function attempts to resolve the route handlers ahead of time to cache
|
||||
@ -215,6 +216,8 @@ export class Route extends AppClass {
|
||||
/** Pre-compiled route handler for the main route handler for this route. */
|
||||
protected compiledPostflight?: ResolvedRouteHandler[]
|
||||
|
||||
protected validator?: Validator<unknown>
|
||||
|
||||
/** Programmatic aliases of this route. */
|
||||
public aliases: string[] = []
|
||||
|
||||
@ -387,6 +390,15 @@ export class Route extends AppClass {
|
||||
return this
|
||||
}
|
||||
|
||||
input(validator: Validator<any>): this {
|
||||
if ( !this.validator ) {
|
||||
//
|
||||
}
|
||||
|
||||
this.validator = validator
|
||||
return this
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
private prepend(prefix: string): this {
|
||||
if ( !prefix.endsWith('/') ) {
|
||||
|
209
src/http/routing/Route2.ts
Normal file
209
src/http/routing/Route2.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import {Collection, Either, PrefixTypeArray} from '../../util'
|
||||
import {ResponseFactory} from '../response/ResponseFactory'
|
||||
import {HTTPMethod, Request} from '../lifecycle/Request'
|
||||
import {TypedDependencyKey, constructable, Constructable, Instantiable} from '../../di'
|
||||
import {Middleware} from './Middleware'
|
||||
|
||||
/**
|
||||
* Type alias for an item that is a valid response object, or lack thereof.
|
||||
*/
|
||||
export type ResponseObject = ResponseFactory | string | number | void | any | Promise<ResponseObject>
|
||||
|
||||
/**
|
||||
* Type alias for a function that applies a route handler to the request.
|
||||
* The goal is to transform RouteHandlers to ResolvedRouteHandler.
|
||||
*/
|
||||
export type ResolvedRouteHandler = (request: Request) => ResponseObject
|
||||
|
||||
export type ParameterProvidingMiddleware<T> = (request: Request) => Either<ResponseObject, T>
|
||||
|
||||
export interface HandledRoute<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
alias(name: string): this
|
||||
}
|
||||
|
||||
export class Route<TReturn extends ResponseObject, THandlerParams extends unknown[] = []> {
|
||||
protected preflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
protected parameters: Collection<ParameterProvidingMiddleware<unknown>> = new Collection<ParameterProvidingMiddleware<unknown>>()
|
||||
|
||||
protected postflight: Collection<ResolvedRouteHandler> = new Collection<ResolvedRouteHandler>()
|
||||
|
||||
protected aliases: Collection<string> = new Collection<string>()
|
||||
|
||||
protected handler?: Constructable<(...x: THandlerParams) => TReturn>
|
||||
|
||||
constructor(
|
||||
protected method: HTTPMethod | HTTPMethod[],
|
||||
protected route: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set a programmatic name for this route.
|
||||
* @param name
|
||||
*/
|
||||
public alias(name: string): this {
|
||||
this.aliases.push(name)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form of the route.
|
||||
*/
|
||||
public getRoute(): string {
|
||||
return this.route
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string-form methods supported by the route.
|
||||
*/
|
||||
public getMethods(): HTTPMethod[] {
|
||||
if ( !Array.isArray(this.method) ) {
|
||||
return [this.method]
|
||||
}
|
||||
|
||||
return this.method
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preflight middleware for this route.
|
||||
*/
|
||||
public getPreflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.preflight.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get postflight middleware for this route.
|
||||
*/
|
||||
public getPostflight(): Collection<ResolvedRouteHandler> {
|
||||
return this.postflight.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this route matches the given HTTP verb and request path.
|
||||
* @param method
|
||||
* @param potential
|
||||
*/
|
||||
public match(method: HTTPMethod, potential: string): boolean {
|
||||
if ( Array.isArray(this.method) && !this.method.includes(method) ) {
|
||||
return false
|
||||
} else if ( !Array.isArray(this.method) && this.method !== method ) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Boolean(this.extract(potential))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request path, try to extract this route's paramters from the path string.
|
||||
*
|
||||
* @example
|
||||
* For route `/foo/:bar/baz` and input `/foo/bob/baz`, extracts:
|
||||
*
|
||||
* ```typescript
|
||||
* {
|
||||
* bar: 'bob'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param potential
|
||||
*/
|
||||
public extract(potential: string): {[key: string]: string} | undefined {
|
||||
const routeParts = (this.route.startsWith('/') ? this.route.substr(1) : this.route).split('/')
|
||||
const potentialParts = (potential.startsWith('/') ? potential.substr(1) : potential).split('/')
|
||||
|
||||
const params: any = {}
|
||||
let wildcardIdx = 0
|
||||
|
||||
for ( let i = 0; i < routeParts.length; i += 1 ) {
|
||||
const part = routeParts[i]
|
||||
|
||||
if ( part === '**' ) {
|
||||
params[wildcardIdx] = potentialParts.slice(i).join('/')
|
||||
return params
|
||||
}
|
||||
|
||||
if ( (potentialParts.length - 1) < i ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( part === '*' ) {
|
||||
params[wildcardIdx] = potentialParts[i]
|
||||
wildcardIdx += 1
|
||||
} else if ( part.startsWith(':') ) {
|
||||
params[part.substr(1)] = potentialParts[i]
|
||||
} else if ( potentialParts[i] !== part ) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we didn't find a **
|
||||
// So, if the lengths are different, fail
|
||||
if ( routeParts.length !== potentialParts.length ) {
|
||||
return
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
public parameterMiddleware<T>(
|
||||
handler: ParameterProvidingMiddleware<T>,
|
||||
): Route<TReturn, PrefixTypeArray<T, THandlerParams>> {
|
||||
const route = new Route<TReturn, PrefixTypeArray<T, THandlerParams>>(
|
||||
this.method,
|
||||
this.route,
|
||||
)
|
||||
|
||||
route.copyFrom(this)
|
||||
route.parameters.push(handler)
|
||||
return route
|
||||
}
|
||||
|
||||
private copyFrom(other: Route<TReturn, any>) {
|
||||
this.preflight = other.preflight.clone()
|
||||
this.postflight = other.postflight.clone()
|
||||
this.aliases = other.aliases.clone()
|
||||
}
|
||||
|
||||
public calls<TKey>(
|
||||
key: TypedDependencyKey<TKey>,
|
||||
selector: (x: TKey) => (...params: THandlerParams) => TReturn,
|
||||
): HandledRoute<TReturn, THandlerParams> {
|
||||
this.handler = constructable<TKey>(key)
|
||||
.tap(inst => Function.prototype.bind.call(inst as any, selector(inst)) as ((...params: THandlerParams) => TReturn))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public pre(middleware: Instantiable<Middleware>): this {
|
||||
this.preflight.push(request => request.make<Middleware>(middleware).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
public post(middleware: Instantiable<Middleware>): this {
|
||||
this.postflight.push(request => request.make<Middleware>(middleware).apply())
|
||||
return this
|
||||
}
|
||||
|
||||
// validator
|
||||
|
||||
/** Cast the route to an intelligible string. */
|
||||
toString(): string {
|
||||
const method = Array.isArray(this.method) ? this.method : [this.method]
|
||||
return `${method.join('|')} -> ${this.route}`
|
||||
}
|
||||
|
||||
/** Prefix the route's path with the given prefix, normalizing `/` characters. */
|
||||
private prepend(prefix: string): this {
|
||||
if ( !prefix.endsWith('/') ) {
|
||||
prefix = `${prefix}/`
|
||||
}
|
||||
if ( this.route.startsWith('/') ) {
|
||||
this.route = this.route.substring(1)
|
||||
}
|
||||
this.route = `${prefix}${this.route}`
|
||||
return this
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import {Container, Inject, Instantiable, isInstantiable, StaticClass} from '../.
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {getFieldsMeta, ModelField} from './Field'
|
||||
import {deepCopy, Pipe, Collection, Awaitable, uuid4, isKeyof} from '../../util'
|
||||
import {deepCopy, Collection, Awaitable, uuid4, isKeyof, Pipeline} from '../../util'
|
||||
import {EscapeValueObject} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Logging} from '../../service/Logging'
|
||||
@ -828,8 +828,8 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
/**
|
||||
* Creates a new Pipe instance containing this model instance.
|
||||
*/
|
||||
public pipe(): Pipe<this> {
|
||||
return Pipe.wrap(this)
|
||||
public pipe<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||||
return pipeline.apply(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -867,12 +867,11 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
|
||||
this.logging.debug(`buildInsertFieldObject populateKeyOnInsert? ${ctor.populateKeyOnInsert}; keyName: ${this.keyName()}`)
|
||||
|
||||
return getFieldsMeta(this)
|
||||
.pipe()
|
||||
return Pipeline.id<Collection<ModelField>>()
|
||||
.unless(ctor.populateKeyOnInsert, fields => {
|
||||
return fields.where('databaseKey', '!=', this.keyName())
|
||||
})
|
||||
.get()
|
||||
.apply(getFieldsMeta(this))
|
||||
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
|
||||
}
|
||||
|
||||
|
@ -66,14 +66,14 @@ export class PostgresSchema extends Schema {
|
||||
cols.each(col => {
|
||||
table.column(col.column_name)
|
||||
.type(col.data_type)
|
||||
.pipe()
|
||||
.pipe(line => {
|
||||
return line
|
||||
.when(col.is_nullable, builder => {
|
||||
builder.isNullable()
|
||||
return builder
|
||||
return builder.nullable()
|
||||
})
|
||||
.when(col.column_default, builder => {
|
||||
builder.default(raw(col.column_default))
|
||||
return builder
|
||||
return builder.default(raw(col.column_default))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -92,20 +92,18 @@ export class PostgresSchema extends Schema {
|
||||
|
||||
table.constraint(key)
|
||||
.type(ConstraintType.Unique)
|
||||
.pipe()
|
||||
.peek(constraint => {
|
||||
.tap(constraint => {
|
||||
collect<{column_name: string}>(uniques[key]) // eslint-disable-line camelcase
|
||||
.pluck<string>('column_name')
|
||||
.each(column => constraint.field(column))
|
||||
})
|
||||
.get()
|
||||
.flagAsExistingInSchema()
|
||||
}
|
||||
|
||||
// Apply the primary key constraints
|
||||
constraints.where('constraint_type', '=', 'p')
|
||||
.pipe()
|
||||
.when(c => c.count() > 0, pk => {
|
||||
.pipe(line => {
|
||||
return line.when(c => c.count() > 0, pk => {
|
||||
pk.each(constraint => {
|
||||
table.column(constraint.column_name)
|
||||
.primary()
|
||||
@ -113,6 +111,7 @@ export class PostgresSchema extends Schema {
|
||||
|
||||
return pk
|
||||
})
|
||||
})
|
||||
|
||||
// Apply the non-null constraints
|
||||
// Builder columns are non-null by default, so mark the others as nullable
|
||||
@ -158,7 +157,8 @@ export class PostgresSchema extends Schema {
|
||||
}
|
||||
|
||||
table.index(key)
|
||||
.pipe()
|
||||
.pipe(builder => {
|
||||
return builder
|
||||
.peek(idx => {
|
||||
collect<{column_name: string}>(groupedIndexes[key]) // eslint-disable-line camelcase
|
||||
.pluck<string>('column_name')
|
||||
@ -166,7 +166,7 @@ export class PostgresSchema extends Schema {
|
||||
})
|
||||
.when(groupedIndexes[key]?.[0]?.indisprimary, idx => idx.primary())
|
||||
.when(groupedIndexes[key]?.[0]?.indisunique, idx => idx.unique())
|
||||
.get()
|
||||
})
|
||||
.flagAsExistingInSchema()
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {collect, Maybe, ParameterizedCallback, Pipe} from '../../util'
|
||||
import {collect, Maybe, ParameterizedCallback, Pipeline} from '../../util'
|
||||
import {FieldType} from '../types'
|
||||
import {EscapeValue, QuerySafeValue, raw} from '../dialect/SQLDialect'
|
||||
|
||||
@ -33,7 +33,7 @@ export abstract class SchemaBuilderBase {
|
||||
protected existsInSchema = false
|
||||
|
||||
/** If the resource exists in the schema, the unaltered values it has. */
|
||||
public originalFromSchema?: SchemaBuilderBase
|
||||
public originalFromSchema?: this
|
||||
|
||||
constructor(
|
||||
/** The name of the schema item. */
|
||||
@ -44,7 +44,7 @@ export abstract class SchemaBuilderBase {
|
||||
* Clone the properties of this resource to a different instance.
|
||||
* @param newBuilder
|
||||
*/
|
||||
public cloneTo(newBuilder: SchemaBuilderBase): SchemaBuilderBase {
|
||||
public cloneTo(newBuilder: this): this {
|
||||
newBuilder.shouldDrop = this.shouldDrop
|
||||
newBuilder.shouldRenameTo = this.shouldRenameTo
|
||||
newBuilder.shouldSkipIfExists = this.shouldSkipIfExists
|
||||
@ -129,9 +129,14 @@ export abstract class SchemaBuilderBase {
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get a Pipe containing this instance. */
|
||||
pipe(): Pipe<this> {
|
||||
return Pipe.wrap<this>(this)
|
||||
/** Build and apply a pipeline. */
|
||||
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||||
return builder(Pipeline.id()).apply(this)
|
||||
}
|
||||
|
||||
tap(op: (x: this) => unknown): this {
|
||||
op(this)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,8 +168,6 @@ export class ColumnBuilder extends SchemaBuilderBase {
|
||||
/** True if this column should contain distinct values. */
|
||||
protected shouldBeUnique = false
|
||||
|
||||
public originalFromSchema?: ColumnBuilder
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
|
||||
@ -174,7 +177,7 @@ export class ColumnBuilder extends SchemaBuilderBase {
|
||||
super(name)
|
||||
}
|
||||
|
||||
public cloneTo(newBuilder: ColumnBuilder): ColumnBuilder {
|
||||
public cloneTo(newBuilder: this): this {
|
||||
super.cloneTo(newBuilder)
|
||||
newBuilder.targetType = this.targetType
|
||||
newBuilder.shouldBeNullable = this.shouldBeNullable
|
||||
@ -324,7 +327,6 @@ export enum ConstraintType {
|
||||
* Builder to specify the schema of a table constraint.
|
||||
*/
|
||||
export class ConstraintBuilder extends SchemaBuilderBase {
|
||||
public originalFromSchema?: ConstraintBuilder
|
||||
|
||||
/** The fields included in this constraint. */
|
||||
protected fields: Set<string> = new Set<string>()
|
||||
@ -359,7 +361,7 @@ export class ConstraintBuilder extends SchemaBuilderBase {
|
||||
return this.constraintExpression
|
||||
}
|
||||
|
||||
public cloneTo(newBuilder: ConstraintBuilder): ConstraintBuilder {
|
||||
public cloneTo(newBuilder: this): this {
|
||||
super.cloneTo(newBuilder)
|
||||
newBuilder.fields = new Set<string>([...this.fields])
|
||||
newBuilder.constraintType = this.constraintType
|
||||
@ -436,8 +438,6 @@ export class IndexBuilder extends SchemaBuilderBase {
|
||||
/** True if this is a primary key index. */
|
||||
protected shouldBePrimary = false
|
||||
|
||||
public originalFromSchema?: IndexBuilder
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
|
||||
@ -447,7 +447,7 @@ export class IndexBuilder extends SchemaBuilderBase {
|
||||
super(name)
|
||||
}
|
||||
|
||||
public cloneTo(newBuilder: IndexBuilder): IndexBuilder {
|
||||
public cloneTo(newBuilder: this): this {
|
||||
super.cloneTo(newBuilder)
|
||||
newBuilder.fields = new Set<string>([...this.fields])
|
||||
newBuilder.removedFields = new Set<string>([...this.removedFields])
|
||||
@ -555,9 +555,7 @@ export class TableBuilder extends SchemaBuilderBase {
|
||||
*/
|
||||
protected constraints: {[key: string]: ConstraintBuilder} = {}
|
||||
|
||||
public originalFromSchema?: TableBuilder
|
||||
|
||||
public cloneTo(newBuilder: TableBuilder): TableBuilder {
|
||||
public cloneTo(newBuilder: this): this {
|
||||
super.cloneTo(newBuilder)
|
||||
newBuilder.columns = {...this.columns}
|
||||
newBuilder.indexes = {...this.indexes}
|
||||
@ -743,8 +741,7 @@ export class TableBuilder extends SchemaBuilderBase {
|
||||
const name = this.getNextAvailableConstraintName(ConstraintType.Unique)
|
||||
this.constraint(name)
|
||||
.type(ConstraintType.Unique)
|
||||
.pipe()
|
||||
.peek(constraint => {
|
||||
.tap(constraint => {
|
||||
fields.forEach(field => constraint.field(field))
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Inject, Singleton} from '../di'
|
||||
import {Awaitable, Collection, ErrorWithContext, Maybe, Pipe, universalPath, UniversalPath} from '../util'
|
||||
import {Awaitable, Collection, ErrorWithContext, Maybe, Pipeline, universalPath, UniversalPath} from '../util'
|
||||
import {Unit, UnitStatus} from '../lifecycle/Unit'
|
||||
import {Logging} from './Logging'
|
||||
import {Route} from '../http/routing/Route'
|
||||
@ -168,7 +168,7 @@ export class Routing extends Unit {
|
||||
const isSSL = rawHost.startsWith('https://')
|
||||
const port = this.config.get('server.port', 8000)
|
||||
|
||||
return Pipe.wrap<string>(rawHost)
|
||||
return Pipeline.id<string>()
|
||||
.unless(
|
||||
host => host.startsWith('http://') || host.startsWith('https'),
|
||||
host => `http://${host}`,
|
||||
@ -187,7 +187,7 @@ export class Routing extends Unit {
|
||||
},
|
||||
)
|
||||
.tap<UniversalPath>(host => universalPath(host))
|
||||
.get()
|
||||
.apply(rawHost)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from './Collection'
|
||||
import {Iterable, StopIteration} from './Iterable'
|
||||
import {applyWhere, WhereOperator} from './where'
|
||||
import {AsyncPipe, Pipe} from '../support/Pipe'
|
||||
import {AsyncPipe, Pipeline} from '../support/Pipe'
|
||||
type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncCollection<T>
|
||||
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
|
||||
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
|
||||
@ -798,19 +798,16 @@ export class AsyncCollection<T> {
|
||||
return this.storedItems.range(start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the function, passing this collection to it.
|
||||
* @param {AsyncCollectionFunction} func
|
||||
*/
|
||||
pipeTo<T2>(func: AsyncCollectionFunction<T, T2>): any {
|
||||
return func(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new Pipe of this collection.
|
||||
*/
|
||||
pipe(): Pipe<AsyncCollection<T>> {
|
||||
return Pipe.wrap(this)
|
||||
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||||
return pipeline.apply(this)
|
||||
}
|
||||
|
||||
/** Build and apply a pipeline. */
|
||||
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||||
return builder(Pipeline.id()).apply(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {AsyncPipe, Pipe} from '../support/Pipe'
|
||||
import {AsyncPipe, Pipeline} from '../support/Pipe'
|
||||
|
||||
type CollectionItem<T> = T
|
||||
type MaybeCollectionItem<T> = CollectionItem<T> | undefined
|
||||
@ -822,8 +822,13 @@ class Collection<T> {
|
||||
/**
|
||||
* Return a new Pipe of this collection.
|
||||
*/
|
||||
pipe(): Pipe<Collection<T>> {
|
||||
return Pipe.wrap(this)
|
||||
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||||
return pipeline.apply(this)
|
||||
}
|
||||
|
||||
/** Build and apply a pipeline. */
|
||||
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||||
return builder(Pipeline.id()).apply(this)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {collect} from '../collection/Collection'
|
||||
import {InvalidJSONStateError, JSONState, Rehydratable} from './Rehydratable'
|
||||
import {Pipe} from './Pipe'
|
||||
import {Pipeline} from './Pipe'
|
||||
|
||||
/**
|
||||
* A class for building and working with messages grouped by keys.
|
||||
@ -131,9 +131,14 @@ export class Messages implements Rehydratable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new Pipe object wrapping this instance.
|
||||
* Return a new Pipe of this collection.
|
||||
*/
|
||||
pipe(): Pipe<Messages> {
|
||||
return Pipe.wrap<Messages>(this)
|
||||
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||||
return pipeline.apply(this)
|
||||
}
|
||||
|
||||
/** Build and apply a pipeline. */
|
||||
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||||
return builder(Pipeline.id()).apply(this)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
/**
|
||||
* A closure that maps a given pipe item to a different type.
|
||||
*/
|
||||
import {Awaitable} from './types'
|
||||
import {Awaitable, Maybe} from './types'
|
||||
|
||||
export type PipeOperator<T, T2> = (subject: T) => T2
|
||||
|
||||
/**
|
||||
* A closure that maps a given pipe item to an item of the same type.
|
||||
*/
|
||||
export type ReflexivePipeOperator<T> = (subject: T) => T
|
||||
export type ReflexivePipeOperator<T> = (subject: T) => Maybe<T>
|
||||
|
||||
/**
|
||||
* A condition or condition-resolving function for pipe methods.
|
||||
@ -19,48 +19,14 @@ export type PipeCondition<T> = boolean | ((subject: T) => boolean)
|
||||
* A class for writing chained/conditional operations in a data-flow manner.
|
||||
*
|
||||
* This is useful when you need to do a series of operations on an object, perhaps conditionally.
|
||||
*
|
||||
* @example
|
||||
* Say we have a Collection of items, and want to apply some transformations and filtering based on arguments:
|
||||
*
|
||||
* ```typescript
|
||||
* const collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
*
|
||||
* function transform(collection, evensOnly = false, returnEntireCollection = false) {
|
||||
* return Pipe.wrap(collection)
|
||||
* .when(evensOnly, coll => {
|
||||
* return coll.filter(x => !(x % 2))
|
||||
* })
|
||||
* .unless(returnEntireCollection, coll => {
|
||||
* return coll.take(3)
|
||||
* })
|
||||
* .tap(coll => {
|
||||
* return coll.map(x => x * 2))
|
||||
* })
|
||||
* .get()
|
||||
* }
|
||||
*
|
||||
* transform(collection) // => Collection[2, 4, 6]
|
||||
*
|
||||
* transform(collection, true) // => Collection[4, 8, 12]
|
||||
*
|
||||
* transform(collection, false, true) // => Collection[2, 4, 6, 8, 10, 12, 14, 16, 18]
|
||||
* ```
|
||||
*/
|
||||
export class Pipe<T> {
|
||||
/**
|
||||
* Return a new Pipe containing the given subject.
|
||||
* @param subject
|
||||
*/
|
||||
static wrap<subjectType>(subject: subjectType): Pipe<subjectType> {
|
||||
return new Pipe<subjectType>(subject)
|
||||
export class Pipeline<TIn, TOut> {
|
||||
static id<T>(): Pipeline<T, T> {
|
||||
return new Pipeline(x => x)
|
||||
}
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The item being operated on.
|
||||
*/
|
||||
private subject: T,
|
||||
protected readonly factory: (TIn: TIn) => TOut,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -75,17 +41,22 @@ export class Pipe<T> {
|
||||
*
|
||||
* @param op
|
||||
*/
|
||||
tap<T2>(op: PipeOperator<T, T2>): Pipe<T2> {
|
||||
return new Pipe(op(this.subject))
|
||||
tap<T2>(op: PipeOperator<TOut, T2>): Pipeline<TIn, T2> {
|
||||
return new Pipeline((val: TIn) => {
|
||||
return op(this.factory(val))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Like tap, but always returns the original pipe.
|
||||
* Like tap, but always returns the original pipe type.
|
||||
* @param op
|
||||
*/
|
||||
peek<T2>(op: PipeOperator<T, T2>): this {
|
||||
op(this.subject)
|
||||
return this
|
||||
peek<T2>(op: PipeOperator<TOut, T2>): Pipeline<TIn, TOut> {
|
||||
return new Pipeline((val: TIn) => {
|
||||
const nextVal = this.factory(val)
|
||||
op(nextVal)
|
||||
return nextVal
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,14 +66,20 @@ export class Pipe<T> {
|
||||
* @param check
|
||||
* @param op
|
||||
*/
|
||||
when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
||||
if (
|
||||
(typeof check === 'function' && check(this.subject))
|
||||
|| (typeof check !== 'function' && check) ) {
|
||||
return Pipe.wrap(op(this.subject))
|
||||
when(check: PipeCondition<TOut>, op: ReflexivePipeOperator<TOut>): Pipeline<TIn, TOut> {
|
||||
return new Pipeline((val: TIn) => {
|
||||
const nextVal = this.factory(val)
|
||||
if ( this.checkCondition(check, nextVal) ) {
|
||||
const appliedVal = op(nextVal)
|
||||
if ( typeof appliedVal === 'undefined' ) {
|
||||
return nextVal
|
||||
}
|
||||
|
||||
return this
|
||||
return appliedVal
|
||||
}
|
||||
|
||||
return nextVal
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -112,42 +89,32 @@ export class Pipe<T> {
|
||||
* @param check
|
||||
* @param op
|
||||
*/
|
||||
unless(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
||||
if (
|
||||
(typeof check === 'function' && check(this.subject))
|
||||
|| (typeof check !== 'function' && check) ) {
|
||||
return this
|
||||
unless(check: PipeCondition<TOut>, op: ReflexivePipeOperator<TOut>): Pipeline<TIn, TOut> {
|
||||
return new Pipeline((val: TIn) => {
|
||||
const nextVal = this.factory(val)
|
||||
if ( !this.checkCondition(check, nextVal) ) {
|
||||
const appliedVal = op(nextVal)
|
||||
if ( typeof appliedVal === 'undefined' ) {
|
||||
return nextVal
|
||||
}
|
||||
|
||||
return Pipe.wrap(op(this.subject))
|
||||
return appliedVal
|
||||
}
|
||||
|
||||
return nextVal
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of `unless()`.
|
||||
* @param check
|
||||
* @param op
|
||||
* Apply the pipeline to an input.
|
||||
*/
|
||||
whenNot(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
|
||||
return this.unless(check, op)
|
||||
apply(input: TIn): TOut {
|
||||
return this.factory(input)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item in the pipe.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* Pipe.wrap(4).get() // => 4
|
||||
* ```
|
||||
*/
|
||||
get(): T {
|
||||
return this.subject
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an AsyncPipe with the current item in the pipe.
|
||||
*/
|
||||
async(): AsyncPipe<T> {
|
||||
return AsyncPipe.wrap<T>(this.subject)
|
||||
protected checkCondition(check: PipeCondition<TOut>, val: TOut): boolean {
|
||||
return (typeof check === 'function' && check(val))
|
||||
|| (typeof check !== 'function' && check)
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,13 +247,6 @@ export class AsyncPipe<T> {
|
||||
return this.subject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the value and return it in a sync `Pipe` instance.
|
||||
*/
|
||||
async sync(): Promise<Pipe<T>> {
|
||||
return Pipe.wrap<T>(await this.subject())
|
||||
}
|
||||
|
||||
/** Get the transformed value from the pipe. Allows awaiting the pipe directly. */
|
||||
then(): Promise<T> {
|
||||
return this.resolve()
|
||||
|
@ -5,7 +5,7 @@ import * as mime from 'mime-types'
|
||||
import {FileNotFoundError, Filesystem} from './path/Filesystem'
|
||||
import {Collection} from '../collection/Collection'
|
||||
import {Readable, Writable} from 'stream'
|
||||
import {Pipe} from './Pipe'
|
||||
import {Pipeline} from './Pipe'
|
||||
|
||||
/**
|
||||
* An item that could represent a path.
|
||||
@ -533,8 +533,15 @@ export class UniversalPath {
|
||||
return false
|
||||
}
|
||||
|
||||
/** Get a new Pipe instance wrapping this. */
|
||||
toPipe(): Pipe<UniversalPath> {
|
||||
return Pipe.wrap(this)
|
||||
/**
|
||||
* Return a new Pipe of this collection.
|
||||
*/
|
||||
pipeTo<TOut>(pipeline: Pipeline<this, TOut>): TOut {
|
||||
return pipeline.apply(this)
|
||||
}
|
||||
|
||||
/** Build and apply a pipeline. */
|
||||
pipe<TOut>(builder: (pipeline: Pipeline<this, this>) => Pipeline<this, TOut>): TOut {
|
||||
return builder(Pipeline.id()).apply(this)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,28 @@ export type Awaitable<T> = T | Promise<T>
|
||||
/** Type alias for something that may be undefined. */
|
||||
export type Maybe<T> = T | undefined
|
||||
|
||||
export type Either<T1, T2> = Left<T1> | Right<T2>
|
||||
|
||||
export type Left<T> = [T, undefined]
|
||||
|
||||
export type Right<T> = [undefined, T]
|
||||
|
||||
export function isLeft<T1, T2>(what: Either<T1, T2>): what is Left<T1> {
|
||||
return typeof what[1] === 'undefined'
|
||||
}
|
||||
|
||||
export function isRight<T1, T2>(what: Either<T1, T2>): what is Right<T2> {
|
||||
return typeof what[0] === 'undefined'
|
||||
}
|
||||
|
||||
export function left<T>(what: T): Left<T> {
|
||||
return [what, undefined]
|
||||
}
|
||||
|
||||
export function right<T>(what: T): Right<T> {
|
||||
return [undefined, what]
|
||||
}
|
||||
|
||||
/** Type alias for a callback that accepts a typed argument. */
|
||||
export type ParameterizedCallback<T> = ((arg: T) => any)
|
||||
|
||||
@ -30,3 +52,7 @@ export function hasOwnProperty<X extends {}, Y extends PropertyKey>(obj: X, prop
|
||||
export interface TypeTag<S extends string> {
|
||||
readonly __typeTag: S
|
||||
}
|
||||
|
||||
export type PrefixTypeArray<T, TArr extends unknown[]> = [T, ...TArr]
|
||||
export type SuffixTypeArray<TArr extends unknown[], T> = [...TArr, T]
|
||||
export type TypeArraySignature<TArr extends unknown[], TReturn> = (...params: TArr) => TReturn
|
||||
|
@ -1,17 +1,92 @@
|
||||
import { z } from 'zod'
|
||||
import {InjectionAware} from '../di'
|
||||
import {ErrorWithContext, TypeTag} from '../util'
|
||||
import {ZodifyRecipient} from './ZodifyRecipient'
|
||||
import {ZodifyRegistrar} from './ZodifyRegistrar'
|
||||
import {Logging} from '../service/Logging'
|
||||
|
||||
/** Type tag for a validated runtime type. */
|
||||
export type Valid<T> = TypeTag<'@extollo/lib:Valid'> & T
|
||||
|
||||
/**
|
||||
* Error thrown if the schema for a validator cannot be located.
|
||||
*/
|
||||
export class InvalidSchemaMappingError extends Error {
|
||||
constructor(message = 'Unable to resolve schema for validator.') {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface defining validation error messages.
|
||||
*/
|
||||
export interface ValidationFormErrors<T> {
|
||||
formErrors: string[],
|
||||
fieldErrors: {
|
||||
[k in keyof T]: string[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when data validation has failed.
|
||||
*/
|
||||
export class ValidationError<T> extends ErrorWithContext {
|
||||
constructor(
|
||||
public readonly formErrors: ValidationFormErrors<T>,
|
||||
message = 'Invalid form data',
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input data against a schema at runtime.
|
||||
*/
|
||||
export class Validator<T> extends InjectionAware implements ZodifyRecipient {
|
||||
__exZodifiedSchemata: number[] = []
|
||||
|
||||
/**
|
||||
* Parse the input data against the schema.
|
||||
* @throws ValidationError
|
||||
* @param from
|
||||
*/
|
||||
public parse(from: unknown): Valid<T> {
|
||||
try {
|
||||
return this.getZod().parse(from) as Valid<T>
|
||||
} catch (e: unknown) {
|
||||
if ( e instanceof z.ZodError ) {
|
||||
throw new ValidationError<T>(e.formErrors as ValidationFormErrors<T>)
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typeguard for the input schema.
|
||||
* @param what
|
||||
*/
|
||||
public is(what: unknown): what is Valid<T> {
|
||||
try {
|
||||
this.parse(what)
|
||||
return true
|
||||
} catch (e: unknown) {
|
||||
this.log().verbose(`Error during validation: ${e}`)
|
||||
|
||||
if ( e instanceof ValidationError ) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the logging service. */
|
||||
protected log(): Logging {
|
||||
return this.make<Logging>(Logging)
|
||||
}
|
||||
|
||||
/** Get the Zod schema. */
|
||||
protected getZod(): z.ZodType<T> {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
if ( this.__exZodifiedSchemata.length < 1 ) {
|
||||
|
Loading…
Reference in New Issue
Block a user