import EqualStack from "../EqualStack"; import { UnknownTypeError, ReservedWordForPrimaryKeyError, InvalidValidationError, InvalidKeyValidationError } from "../errors"; import {invalidValuesAsString} from "../utils"; const FORBIDDEN_PRIMARY_KEYS = [ "row", "primaryKey", "primaryValue" ]; export type TInstanceOrT = ( T extends new (...args: any) => any ? InstanceType : T ); interface IObjectWithAnyKey { "*": any; } // output type outputValue = ( TInstanceOrT extends {TOutput: any} ? TInstanceOrT["TOutput"] : never ); type outputData = { readonly [key in keyof T]?: outputValue< T[key] >; }; interface IOutputAnyData { readonly [key: string]: outputValue< T >; } export type OutputType = ( T extends IObjectWithAnyKey ? IOutputAnyData< T["*"] > & outputData< Omit< T, "*" > > : outputData< T > ); // input type inputValue = ( TInstanceOrT extends {TInput: any} ? TInstanceOrT["TInput"] : never ); type inputData = { [key in keyof T]?: inputValue< T[key] >; }; interface IInputAnyData { [key: string]: inputValue< T >; } export type InputType = ( T extends IObjectWithAnyKey ? IInputAnyData< T["*"] > & inputData< Omit< T, "*" > > : inputData< T > ); // json type jsonValue = ( TInstanceOrT extends {TJson: any} ? TInstanceOrT["TJson"] : never ); interface IJsonAnyData { [key: string]: jsonValue< T >; } type jsonData = { [key in keyof T]?: jsonValue< T[key] >; }; export type JsonType = ( T extends IObjectWithAnyKey ? IJsonAnyData< T["*"] > & jsonData< Omit< T, "*" > > : jsonData< T > ); export interface IType { TOutput: any; TInput: any; TJson: any; } const Types: any = {}; export interface ITypeParams { key?: ((key: string) => boolean) | RegExp; type?: string; required?: boolean; primary?: boolean; prepare?: (value: any, key: string, model: any) => any; toJSON?: (value: any, stack: any) => any; clone?: (value: any, stack: any) => any; equal?: (selfValue: any, anotherValue: any, stack: any) => boolean; validate?: ((value: any, key: string) => boolean) | RegExp ; enum?: any[]; default?: any | (() => any); const?: boolean; } export class Type { static Model: any; static registerType(name: string, SomeType: (new (...args: any) => Type)) { Types[name] = SomeType; } // create type by params static create(description: any, key: string) { const isTypeHelper = ( typeof description === "function" && description.isTypeHelper === true ); if ( isTypeHelper ) { description = description(); } const isPlainDescription = ( description && description.type ); // structure: {prop: "number"} // or // structure: {prop: ["number"]} // or // structure: {prop: SomeModel} // or // structure: {prop: {}} if ( !isPlainDescription ) { description = { type: description }; } // prepare description: ["string"] // to { type: "array", element: {type: "string"} } for (const typeName in Types) { const SomeType = Types[ typeName ]; // CustomType can use some variations for declare structure SomeType.prepareDescription( description, key ); } // find CustomType by name const CustomType = Types[ description.type ]; if ( !CustomType ) { throw new UnknownTypeError({ key, type: description.type }); } if ( description.primary ) { const Model = Type.Model; const isReserved = ( key in Model.prototype || FORBIDDEN_PRIMARY_KEYS.includes( key ) ); if ( isReserved ) { throw new ReservedWordForPrimaryKeyError({key}); } } try { description = new CustomType( description ); } catch (err) { err.message = key + ": " + err.message; throw err; } // structure must be static Object.freeze( description ); return description; } // default behavior static prepareDescription(description: any, key: string) { // redefine me return description; } primary?: boolean; required: boolean; type: string; const: boolean = false; enum?: any[]; constructor(params: ITypeParams) { if ( params.primary ) { this.primary = true; } this.type = params.type as string; this.required = params.required || params.primary || false; if ( Array.isArray( params.enum ) ) { this.enum = params.enum; } // default can be: false, 0, or function if ( "default" in params ) { if ( typeof params.default === "function" ) { this.default = params.default; } else { this.default = () => params.default; } } // custom prepare not null value, after default prepare if ( typeof params.prepare === "function" ) { const prepareByType = this.prepare.bind(this); const customPrepare = params.prepare; this.prepare = (value, key, model) => { value = prepareByType( value, key, model ); if ( value != null ) { value = customPrepare( value, key, model ); } return value; }; } if ( typeof params.toJSON === "function" ) { this.toJSON = params.toJSON; } // custom validate by RegExp or function if ( "validate" in params ) { // validate by required, enum, "unique" (ArrayType) const validateByType = this.validate.bind(this); if ( typeof params.validate === "function" ) { const customValidate = params.validate; this.validate = (value, modelKey) => { return ( validateByType( value, modelKey ) && ( value == null || customValidate( value, modelKey ) ) ); }; } else if ( params.validate instanceof RegExp ) { const regExp = params.validate; this.validate = (value, modelKey) => { return ( validateByType( value, modelKey ) && ( value == null || regExp.test( value ) ) ); }; } else { throw new InvalidValidationError({ invalidValue: invalidValuesAsString( params.validate ) }); } } // validate key for models with "*" if ( "key" in params ) { const customValidateKey = params.key; if ( customValidateKey instanceof RegExp ) { this.validateKey = (modelKey) => { return customValidateKey.test( modelKey ); }; } else if ( typeof customValidateKey === "function" ) { this.validateKey = customValidateKey; } else { throw new InvalidKeyValidationError({ invalidValue: invalidValuesAsString( customValidateKey ) }); } } // don't change value if ( params.const ) { this.const = true; } if ( params.clone ) { this.clone = params.clone; } if ( params.equal ) { this.equal = params.equal; } } default() { return null; } validateKey(key: string) { return true; } validate(value: any, key: string): boolean { if ( this.enum ) { if ( value != null ) { return this.enum.includes( value ); } } return true; } prepare(value: any, key: string, model: any): any { return value; } toJSON(value: any, stack: any): any { return value; } clone(value: any, stack?: EqualStack, parentModel?: any): any { return this.toJSON( value, stack ); } typeAsString(): string { return this.type; } equal(selfValue: any, otherValue: any, stack: any): boolean { return selfValue === otherValue; } }