model-layer
Version:
365 lines (294 loc) • 9.46 kB
text/typescript
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> = (
T extends new (...args: any) => any ?
InstanceType<T> :
T
);
interface IObjectWithAnyKey {
"*": any;
}
// output
type outputValue<T extends any> = (
TInstanceOrT<T> extends {TOutput: any} ?
TInstanceOrT<T>["TOutput"] :
never
);
type outputData<T> = {
readonly [key in keyof T]?: outputValue< T[key] >;
};
interface IOutputAnyData<T> {
readonly [key: string]: outputValue< T >;
}
export type OutputType<T> = (
T extends IObjectWithAnyKey ?
IOutputAnyData< T["*"] > & outputData< Omit< T, "*" > > :
outputData< T >
);
// input
type inputValue<T extends any> = (
TInstanceOrT<T> extends {TInput: any} ?
TInstanceOrT<T>["TInput"] :
never
);
type inputData<T> = {
[key in keyof T]?: inputValue< T[key] >;
};
interface IInputAnyData<T> {
[key: string]: inputValue< T >;
}
export type InputType<T> = (
T extends IObjectWithAnyKey ?
IInputAnyData< T["*"] > & inputData< Omit< T, "*" > > :
inputData< T >
);
// json
type jsonValue<T extends any> = (
TInstanceOrT<T> extends {TJson: any} ?
TInstanceOrT<T>["TJson"] :
never
);
interface IJsonAnyData<T> {
[key: string]: jsonValue< T >;
}
type jsonData<T> = {
[key in keyof T]?: jsonValue< T[key] >;
};
export type JsonType<T> = (
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;
}
}