@tsed/json-mapper
Version:
Json mapper module for Ts.ED Framework
167 lines (166 loc) • 5.41 kB
JavaScript
import { ancestorsOf, classOf, getRandomId, hasJsonMethod, isClass, isCollection, isDate, isMomentObject, isMongooseObject, isNil, isObject, isObjectID, isString, nameOf, objectKeys } from "@tsed/core";
/**
* Base compiler that turns schema metadata into executable (de)serialization functions.
* Subclasses supply `map`, `alterValue`, and `createMapper` implementations to specialize
* the pipeline for serialization or deserialization.
*/
export class JsonMapperCompiler {
constructor() {
/**
* Cached mappers metadata
* @protected
*/
this.cache = new Map();
/**
* Cached executable mappers by his id
* @protected
*/
this.mappers = {};
/**
* Cached schemas
* @protected
*/
this.schemes = {};
/**
* Cached classes by his id
* @protected
*/
this.constructors = {};
/**
* Global variables available in the mapper
* @protected
*/
this.globals = {
isCollection,
isClass,
isObject,
classOf,
nameOf,
hasJsonMethod,
isMongooseObject,
isNil,
isDate,
objectKeys,
isMomentObject
};
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, fn) {
const id = nameOf(model);
this.cache.set(model, new Map().set("typeMapper", {
id,
fn
}));
this.mappers[id] = fn;
return this;
}
removeTypeMapper(model) {
const store = this.cache.get(model);
if (store) {
const { id } = store.get("typeMapper");
delete this.mappers[id];
this.cache.delete(model);
}
}
addGlobal(key, value) {
this.globals[key] = value;
return this;
}
compileMapper(mapper, { id, groupsId, storeGroups }) {
const { globals, schemes } = this;
const injectGlobals = Object.keys(globals)
.map((name) => {
return `const ${name} = globals.${name};`;
})
.join("\n");
const compileMapper = new Function("globals", "storeGroups", "schemes", "groupsId", "id", `${injectGlobals}
storeGroups.set(groupsId, {id, fn: (${mapper})});
return storeGroups.get(groupsId);`);
const store = compileMapper(globals, storeGroups, schemes, groupsId, id);
this.mappers[id] = store.fn;
return store;
}
createContext(options) {
const { cache } = this;
return {
...options,
cache
};
}
compile(model, groups, opts = {}) {
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
});
const mapper = opts.mapper ? opts.mapper(id, groups) : this.createMapper(token, id, groups);
try {
return this.compileMapper(mapper, { id, groupsId, model: token, storeGroups });
}
catch (err) {
throw new Error(`Fail to compile mapper for ${nameOf(model)}. See the error above: ${err.message}.\n${mapper}`);
}
}
return storeGroups.get(groupsId);
}
execMapper(id, value, options) {
if (isObjectID(value)) {
return value.toString();
}
return this.mappers[id || nameOf(classOf(value))](value, options);
}
getType(model) {
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;
}
alterIgnore(id, options) {
let result = this.schemes[id]?.$hooks?.alter("ignore", false, [options]);
if (result) {
return result;
}
}
alterGroups(schema, groups) {
if (groups !== false) {
return schema.$hooks.alter("groups", false, [groups]);
}
return false;
}
getGroupsId(groups) {
if (groups === false) {
return "default";
}
if (groups.length === 0) {
return "-";
}
return groups.join(",");
}
getId(model, groupsId) {
return `${isString(model) ? model : nameOf(model)}:${getRandomId()}:${groupsId}`;
}
getSchemaId(id, propertyKey) {
return `${id}:${propertyKey}`;
}
}