UNPKG

@creamapi/cream

Version:

Concise REST API Maker - An extension library for express to create REST APIs faster

272 lines (271 loc) 10.1 kB
"use strict"; /* * Copyright 2024 Raul Radu * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BootstrapSerializer = exports.Serializer = exports.SerializerCommon = void 0; const Serializable_1 = require("./Serializable"); const SerializerMetaInfo_1 = require("./SerializerMetaInfo"); const Transform_1 = require("./Transform"); /** * This namespace defines Common information * for serializers */ var SerializerCommon; (function (SerializerCommon) { /** * This namespace defines the common attributes to all serializers */ let Attributes; (function (Attributes) { /** * This attribute is used to declare an object as an array */ Attributes.Array = 'common:array'; /** * This attribute is used to automatically serialize everything in the object */ Attributes.AutoSerialize = 'common:autoserialize'; })(Attributes = SerializerCommon.Attributes || (SerializerCommon.Attributes = {})); })(SerializerCommon || (exports.SerializerCommon = SerializerCommon = {})); /** * This base class that implements the complex logic for handling * serialization of objects. It gives a framework to easily implement * complex serializers. */ class Serializer { targetName; contextStack = []; constructor(targetName, baseContext) { this.targetName = targetName; this.contextStack = [baseContext]; } /** * This method is used to handle undefined data. * By default undefined is returned as an empty string * @param dataLabel (unused) the label of the data. * @returns empty string */ async serializeUndefined(_dataLabel) { return ''; } /** * This method is used as the default behaviour for serializing Dates * it uses the Date.toISOString method for serializing it. * @param dataLabel (unused) the label of the data * @param data the date to be serialized * @returns the serialized string in ISO format */ async serializeDate(_dataLabel, data) { return data.toISOString(); } /** * This method is used to serialize a piece of data * @param dataLabel the label of the data * @param data the actual data * @returns a string representing the serialized object */ async serialize(dataLabel, data) { if (typeof data === 'number') { return this.serializeNumber(dataLabel, data); } if (typeof data === 'string') { return this.serializeString(dataLabel, data); } if (typeof data === 'boolean') { return this.serializeBoolean(dataLabel, data); } if (data === null) { return this.serializeNull(dataLabel); } if (data === undefined) { return this.serializeUndefined(dataLabel); } if (data instanceof Date) { return this.serializeDate(dataLabel, data); } return this.serializeAnyObject(dataLabel, data); } /** * This method is used to serialize anything that is not * a base type * @param dataLabel the data label that should be used when rendering * @param data the data that should be serialized * @returns the string representing the serialized object */ async serializeAnyObject(dataLabel, data) { let serializer = Reflect.getMetadata(Serializable_1.SERIALIZER_METADATA_KEY, data) || this; let targetName = serializer.targetName; let metaInfo = this.fetchMetaInfoForObject(dataLabel); if (Array.isArray(data)) { metaInfo.addAttribute(SerializerCommon.Attributes.Array); this.autoMapArray(data); targetName = dataLabel; } if (metaInfo.hasAttribute(SerializerCommon.Attributes.AutoSerialize)) { Serializer.makeSerializable(data); } let dataStream = this.streamify(data); serializer.pushContext(data); let preObjectStream = this.preObject(targetName, dataStream, metaInfo); let postObjectStream = this.postObject(targetName, dataStream, metaInfo); let outStream = (await Promise.all([ preObjectStream, serializer.handleSerializationStream(targetName, dataStream, metaInfo), postObjectStream, ])).join(''); serializer.popContext(); return outStream; } /** * Gets the current context * @returns the current context */ getContext() { return this.contextStack[this.contextStack.length - 1]; } /** * removes the current context from the stack */ popContext() { this.contextStack.pop(); } /** * This is used to push a context to the stack. * This context is used to infer data ownership * @param context the context that should be pushed */ pushContext(context) { this.contextStack.push(context); } /** * This static method makes any object serializable by iterating through the object * this can break when recursive references are taken in place * @param data the data to be serialized * @returns the decorated data */ static makeSerializable(data) { for (let key in data) { (0, Serializable_1.AutoMap)(data, key); (0, SerializerMetaInfo_1.Meta)(SerializerCommon.Attributes.AutoSerialize)(data, key); } return data; } /** * This method is called before handling a custom object * @param dataLabel the label of the object * @param data the object * @param metaInfo any information useful for serialization * @returns a string that should be appended before the serialization of the object */ async preObject(dataLabel, data, metaInfo) { return ''; } /** * This method is called after handling a custom object * @param dataLabel the label of the object * @param data the object * @param metaInfo any information useful for serialization * @returns a string that should be appended after the serialization of the object */ async postObject(dataLabel, data, metaInfo) { return ''; } /** * This method is used to create a serialization of * the array in data by putting the index as the * field name * @param data the array that should be mapped */ autoMapArray(data) { let serialMap = []; for (let index in data) { serialMap.push({ fieldName: index, outName: index, }); } Reflect.defineMetadata(Serializable_1.SERIAL_MAP_METADATA_KEY, serialMap, data); } /** * This method is used to streamify the data from the object given as input * @param data the data that should be streamified * @returns the stream of SerialBites that will be later handled by the serializer */ streamify(data) { let streamBuffer = []; let serialMap = Reflect.getMetadata(Serializable_1.SERIAL_MAP_METADATA_KEY, data) || []; let accessibleData = data; for (let serialItem of serialMap) { let datum = accessibleData[serialItem.fieldName]; let datumMetaInfo = Reflect.getMetadata(SerializerMetaInfo_1.SERIALIZER_META_INFO_METADATA_KEY, data, serialItem.fieldName); let transformPipeline = Reflect.getMetadata(Transform_1.SERIALIZER_TRANSFORM_METADATA_KEY, data, serialItem.fieldName) || []; let transformResult = datum; for (let transform of transformPipeline) { transformResult = transform(transformResult); } streamBuffer.push({ data: transformResult, dataLabel: serialItem.outName, metaInfo: datumMetaInfo, }); } return streamBuffer; } /** * This method will return meta information for the current context based on the * dataLabel * @param dataLabel the name of the field that the metadata should be retrieved from * @returns the metadata associated with the dataLabel */ fetchMetaInfoForObject(dataLabel) { let serialMap = Reflect.getMetadata(Serializable_1.SERIAL_MAP_METADATA_KEY, this.getContext()); let oldName = serialMap?.find((pair) => { return pair.outName == dataLabel; })?.fieldName; return (Reflect.getMetadata(SerializerMetaInfo_1.SERIALIZER_META_INFO_METADATA_KEY, this.getContext(), oldName || dataLabel) || new SerializerMetaInfo_1.SerializerMetaInfo()); } } exports.Serializer = Serializer; /** * @internal * This class is only used to bootstrap serialization * It will also handle base types when no complex object is returned * The serialization of those types is language agnostic */ class BootstrapSerializer extends Serializer { async serializeNull(_dataLabel) { return 'null'; } constructor() { super('', {}); } async start(data) { return this.serialize('', data); } async serializeNumber(_, num) { return Number(num).toString(); } async serializeString(_, data) { return data; } async serializeBoolean(_, bool) { return bool ? 'true' : 'false'; } async handleSerializationStream(_) { throw Error('Trying to serialize an object that is not serializable'); } } exports.BootstrapSerializer = BootstrapSerializer;