@creamapi/cream
Version:
Concise REST API Maker - An extension library for express to create REST APIs faster
272 lines (271 loc) • 10.1 kB
JavaScript
;
/*
* 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;