UNPKG

@tsed/json-mapper

Version:
248 lines (196 loc) 5.67 kB
import { ancestorsOf, classOf, getRandomId, hasJsonMethod, isClass, isCollection, isDate, isMomentObject, isMongooseObject, isNil, isObject, isObjectID, isString, nameOf, objectKeys, Type } from "@tsed/core"; import {JsonSchema} from "@tsed/schema"; export type JsonMapperCallback<Options> = (input: any, options?: Options) => any; export type CachedJsonMapper<Options> = { id: string; fn: JsonMapperCallback<Options>; }; export type CachedGroupsJsonMapper<Options> = Map<string, CachedJsonMapper<Options>>; export abstract class JsonMapperCompiler<Options extends Record<string, any> = any> { /** * Cached mappers metadata * @protected */ protected cache = new Map<Type<any> | string, CachedGroupsJsonMapper<Options>>(); /** * Cached executable mappers by his id * @protected */ protected mappers: Record<string, JsonMapperCallback<Options>> = {}; /** * Cached schemas * @protected */ protected schemes: Record<string, any> = {}; /** * Cached classes by his id * @protected */ protected constructors: Record<string, Type<any>> = {}; /** * Global variables available in the mapper * @protected */ protected globals: Record<string, any> = { isCollection, isClass, isObject, classOf, nameOf, hasJsonMethod, isMongooseObject, isNil, isDate, objectKeys, isMomentObject }; constructor() { this.addGlobal("alterIgnore", this.alterIgnore.bind(this)); this.addGlobal("alterValue", this.alterValue.bind(this)); this.addGlobal("execMapper", this.execMapper.bind(this)); this.addGlobal("compileAndMap", this.map.bind(this)); } addTypeMapper(model: Type<any> | string, fn: any) { const id = nameOf(model); this.cache.set( model, new Map().set("typeMapper", { id, fn }) ); this.mappers[id] = fn; return this; } removeTypeMapper(model: Type<any> | string) { const store = this.cache.get(model); if (store) { const {id} = store.get("typeMapper")!; delete this.mappers[id]; this.cache.delete(model); } } addGlobal(key: string, value: any) { this.globals[key] = value; return this; } eval(mapper: string, {id, groupsId, model}: {id: string; model: Type<any> | string; groupsId: string}) { this.addGlobal("cache", this.cache); const {globals, schemes} = this; const injectGlobals = Object.keys(globals) .map((name) => { return `const ${name} = globals.${name};`; }) .join("\n"); eval(`${injectGlobals}; cache.get(model).set(groupsId, { id: '${id}', fn: ${mapper} })`); const store = this.cache.get(model)!.get(groupsId)!; this.mappers[id] = store.fn; return store; } createContext(options: Options) { const {cache} = this; return { ...options, cache }; } compile( model: Type<any> | string, groups: false | string[], opts: { mapper?: any; } = {} ): CachedJsonMapper<Options> { const token = isString(model) ? model : this.getType(model); const groupsId = this.getGroupsId(groups); let storeGroups = this.cache.get(token) || this.cache.get(nameOf(token)); if (!storeGroups) { storeGroups = new Map(); this.cache.set(token, storeGroups); } if (storeGroups.has("typeMapper")) { return storeGroups.get("typeMapper")!; } // generate mapper for the given groups if (!storeGroups.has(groupsId)) { const id = this.getId(token, groupsId); // prevent circular dependencies storeGroups.set(groupsId, { id } as any); const mapper = opts.mapper ? opts.mapper(id, groups) : this.createMapper(token as Type<any>, id, groups); try { return this.eval(mapper, {id, model: token, groupsId}); } catch (err) { throw new Error(`Fail to compile mapper for ${nameOf(model)}. See the error above: ${err.message}.\n${mapper}`); } } return storeGroups!.get(groupsId)!; } protected execMapper(id: string, value: any, options: Options) { if (isObjectID(value)) { return value.toString(); } return this.mappers[id || nameOf(classOf(value))](value, options); } protected abstract map(input: any, options: Options): any; protected abstract alterValue(schemaId: string, value: any, options: Options): any; protected abstract createMapper(model: Type<any>, id: string, groups: false | string[]): string; protected getType(model: Type<any>) { if (!model) { return Object; } if (isClass(model) && !isCollection(model)) { const type = [Array, Map, Set].find((t) => ancestorsOf(model).includes(t)); if (type) { return type; } } return model; } protected alterIgnore(id: string, options: Options) { let result = this.schemes[id]?.$hooks?.alter("ignore", false, [options]); if (result) { return result; } } protected alterGroups(schema: JsonSchema, groups: false | string[]) { if (groups !== false) { return schema.$hooks.alter("groups", false, [groups]); } return false; } protected getGroupsId(groups: false | string[]) { if (groups === false) { return "default"; } if (groups.length === 0) { return "-"; } return groups.join(","); } protected getId(model: Type<any> | string, groupsId: string) { return `${isString(model) ? model : nameOf(model)}:${getRandomId()}:${groupsId}`; } protected getSchemaId(id: string, propertyKey: string) { return `${id}:${propertyKey}`; } }