@tsed/json-mapper
Version:
Json mapper module for Ts.ED Framework
248 lines (196 loc) • 5.67 kB
text/typescript
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}`;
}
}