UNPKG

@tsed/json-mapper

Version:
269 lines 11.4 kB
import { classOf, hasJsonMethod, isArray, isBoolean, isClassObject, isCollection, isDate, isMomentObject, isMongooseObject, isNil, isObject, isPrimitive, nameOf } from "@tsed/core"; import { getPropertiesStores, JsonEntityStore } from "@tsed/schema"; import { alterOnSerialize } from "../hooks/alterOnSerialize.js"; import { getObjectProperties } from "../utils/getObjectProperties.js"; import { JsonMapperCompiler } from "./JsonMapperCompiler.js"; import { JsonMapperSettings } from "./JsonMapperSettings.js"; import { getJsonMapperTypes } from "./JsonMapperTypesContainer.js"; import { Writer } from "./Writer.js"; const getCollectionType = (input) => { return isArray(input) ? "Array" : input instanceof Set ? "Set" : input instanceof Map ? "Map" : undefined; }; function getBestType(type, obj) { const dataType = classOf(obj); if (dataType && !isClassObject(dataType) && !isCollection(obj)) { return dataType; } return type || Object; } function varKey(k) { return `__${k}`; } export class JsonSerializer extends JsonMapperCompiler { constructor() { super(); this.addTypeMapper(Object, this.mapObject.bind(this)); this.addTypeMapper(Array, this.mapArray.bind(this)); this.addTypeMapper(Map, this.mapMap.bind(this)); this.addTypeMapper(Set, this.mapSet.bind(this)); this.addGlobal("mapJSON", this.mapJSON.bind(this)); this.addTypeMapper("ObjectId", (value) => String(value)); } map(input, options = {}) { if (isNil(input)) { return input; } options = this.mapOptions(options); const model = getBestType(options.type, input); const mapper = this.compile(model, options.groups); return mapper.fn(input, this.createContext(options)); } alterValue(schemaId, value, options) { return alterOnSerialize(this.schemes[schemaId], value, options); } createMapper(model, id, groups) { const entity = JsonEntityStore.from(model); const properties = new Set(); const schemaProperties = [...getPropertiesStores(entity).values()]; const writer = new Writer().arrow("input", "options"); writer.if("isNil(input)").return("input"); // prepare options writer.const("obj", "{}"); writer.set("options", "{...options, self: input}"); // detect some special case writer.add(this.mapPrecondition(id)); if (!schemaProperties.length) { return writer.return("isObject(input) ? {...input} : input").root().toString(); } // properties writer.add(...schemaProperties.flatMap((propertyStore) => { properties.add(propertyStore.propertyKey); if ((propertyStore.schema?.$ignore && isBoolean(propertyStore.schema?.$ignore)) || (propertyStore.schema?.$hooks?.has("groups") && this.alterGroups(propertyStore.schema, groups))) { return; } return this.mapProperty(propertyStore, id, groups); })); // discriminator writer.add(this.mapDiscriminatorKeyValue(entity)); // additional properties writer.add(this.mapAdditionalProperties(entity, properties)); return writer.return("obj").root().toString(); } mapOptions({ groups, useAlias = true, types, ...options }) { const customMappers = {}; types = types || getJsonMapperTypes(); types.forEach((mapper, model) => { if (![Array, Set, Map].includes(model)) { this.addTypeMapper(model, (value, options) => mapper.serialize(value, { ...options, type: model })); } }); const strictGroups = options.strictGroups ?? JsonMapperSettings.strictGroups; return { ...options, groups: groups === undefined ? (strictGroups ? [] : false) : groups || false, useAlias, customMappers }; } mapProperty(propertyStore, id, groups) { const key = String(propertyStore.propertyKey); const aliasKey = String(propertyStore.parent.schema.getAliasOf(key) || key); const schemaId = this.getSchemaId(id, key); const format = propertyStore.itemSchema.get("format"); const formatOpts = format && `options: {format: '${format}'}`; let writer = new Writer().add(`// Map ${key} ${id} ${groups || ""}`); // ignore hook (deprecated) if (propertyStore.schema?.$hooks?.has("ignore")) { this.schemes[schemaId] = propertyStore.schema; writer = writer.if(`!alterIgnore('${schemaId}', {...options, self: input})`); } // pre hook const hasSerializer = propertyStore.schema?.$hooks?.has("onSerialize"); let getter = `input.${key}`; if (hasSerializer) { this.schemes[schemaId] = propertyStore.schema; const opts = Writer.options(formatOpts); getter = `alterValue('${schemaId}', input.${key}, ${opts})`; } writer = writer.set(`let ${varKey(key)}`, getter).if(`${varKey(key)} !== undefined`); const fill = this.getPropertyFiller(propertyStore, key, groups, formatOpts); if (hasSerializer) { fill(writer.if(`${varKey(key)} === input.${key}`)); } else { fill(writer); } if (aliasKey !== key) { writer.set(`obj[options.useAlias ? '${aliasKey}' : '${key}']`, varKey(key)); } else { writer.set(`obj.${key}`, varKey(key)); } return writer.root(); } getPropertyFiller(propertyStore, key, groups, formatOpts) { const isGeneric = propertyStore.itemSchema.isGeneric; const hasDiscriminator = propertyStore.itemSchema.hasDiscriminator; if (propertyStore.isCollection) { const type = propertyStore.getBestType(); let nestedMapper; if (hasDiscriminator) { const targetName = propertyStore.parent.targetName; nestedMapper = this.compile(`Discriminator:${targetName}:${key}`, groups, { mapper: () => this.createDiscriminatorMapper(propertyStore, groups) }); } else { nestedMapper = isGeneric ? { id: "" } : this.compile(type, groups); } return (writer) => writer.callMapper(nameOf(propertyStore.collectionType), varKey(key), `id: '${nestedMapper.id}'`, formatOpts); } if (isGeneric) { return (writer) => writer.set(varKey(key), `compileAndMap(${varKey(key)}, options)`); } let nestedMapper; if (hasDiscriminator) { const targetName = propertyStore.parent.targetName; nestedMapper = this.compile(`Discriminator:${targetName}:${key}`, groups, { mapper: () => this.createDiscriminatorMapper(propertyStore, groups) }); } else { nestedMapper = this.compile(propertyStore.getBestType(), groups); } return (writer) => writer.callMapper(nestedMapper.id, varKey(key), formatOpts); } createDiscriminatorMapper(propertyStore, groups) { const discriminator = propertyStore.itemSchema.discriminator(); const writer = new Writer().arrow("input", "options"); const sw = writer.switch(`nameOf(classOf(input))`); discriminator.values.forEach((value, kind) => { const nestedMapper = this.compile(value, groups); sw.case(`'${nameOf(value)}'`).returnCallMapper(nestedMapper.id, "input"); }); return writer.root().toString(); } mapPrecondition(id) { const writer = new Writer(); writer.if("input && isCollection(input)").return(Writer.mapperFrom("input", `{...options, id: '${id}'}`)); writer.if("hasJsonMethod(input)").return(`mapJSON(input, {...options, id: '${id}'})`); return writer; } mapDiscriminatorKeyValue(entity) { if (entity.discriminatorAncestor) { const discriminator = entity.discriminatorAncestor.schema.discriminator(); const type = discriminator.getDefaultValue(entity.target); if (type) { const writer = new Writer(); writer.if(`!obj.${discriminator.propertyName}`).set(`obj.${discriminator.propertyName}`, `'${type}'`); return writer; } } } mapAdditionalProperties(entity, properties) { const additionalProperties = !!entity.schema.get("additionalProperties"); if (additionalProperties) { const exclude = [...properties.values()].map((key) => `'${key}'`).join(", "); const writer = new Writer(); writer.add("// add additional properties"); writer.each("objectKeys(input)", ["key"]).if(`![${exclude}].includes(key)`).set("obj[key]", "input[key]"); return writer; } } mapObject(input, { type, ...options }) { if ((input && isPrimitive(input)) || !input) { // prevent mongoose mapping error return input; } if (input && isCollection(input)) { return this.execMapper(getCollectionType(input), input, options); } if (hasJsonMethod(input)) { return this.mapJSON(input, options); } return getObjectProperties(input) .filter(([, value]) => value !== undefined) .reduce((obj, [key, value]) => { if (isNil(value)) { return { ...obj, [key]: value }; } const mapper = this.compile(classOf(value), options.groups); return { ...obj, [key]: mapper.fn(value, options) }; }, {}); } mapSet(input, options) { if (isNil(input)) { return input; } return [...input.values()].map((item) => { return this.mapItem(item, options); }); } mapArray(input, options) { if (isNil(input)) { return input; } return [].concat(input).map((item) => { return this.mapItem(item, options); }); } mapMap(input, options) { if (isNil(input)) { return input; } return [...input.entries()].reduce((obj, [key, item]) => { return { ...obj, [key]: this.mapItem(item, options) }; }, {}); } mapItem(input, { id, ...options }) { if (!id && input) { return this.compile(classOf(input), options.groups).fn(input, options); } return id ? this.execMapper(id, input, options) : input; } mapJSON(input, { id, ...options }) { if (isMongooseObject(input)) { return input.toJSON({ ...options, id }); } id = id || nameOf(classOf(input)); if (this.mappers[id] && (isDate(input) || isMomentObject(input))) { return this.execMapper(id, input, options); } input = input.toJSON(); return isObject(input) ? this.execMapper(id, input, options) : input; } } //# sourceMappingURL=JsonSerializer.js.map