ts-japi
Version:
A highly-modular (typescript-friendly)-framework agnostic library for serializing data to the JSON:API specification
282 lines • 10.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const error_model_1 = __importDefault(require("../models/error.model"));
const resource_model_1 = __importDefault(require("../models/resource.model"));
const resource_identifier_model_1 = __importDefault(require("../models/resource-identifier.model"));
const merge_1 = __importDefault(require("../utils/merge"));
const serializer_utils_1 = require("../utils/serializer.utils");
const cache_1 = __importDefault(require("./cache"));
/**
* The {@link Serializer} class is the main class used to serializer data
* (you can use the {@link ErrorSerializer} class to serialize errors).
*
* Example:
* ```typescript
* [[include:serializer.example.ts]]
* ```
*/
class Serializer {
/**
* Default options. Can be edited to change default options globally.
*/
static defaultOptions = {
idKey: "id",
version: "1.0",
onlyIdentifier: false,
nullData: false,
asIncluded: false,
onlyRelationship: false,
cache: false,
depth: 0,
include: 0,
projection: null,
linkers: {},
metaizers: {},
};
/**
* The name to use for the type.
*/
collectionName;
/**
* The set of options for the serializer.
*/
options;
/**
* The set of helper functions for the serializer
*/
helpers;
/**
* Caching
*/
cache = new cache_1.default();
/**
* Creates a {@link Serializer}.
*
* @param collectionName - The name of the collection of objects.
* @param options - Options for the serializer.
*/
constructor(collectionName, options = {}) {
// Setting default options.
this.options = (0, merge_1.default)({}, Serializer.defaultOptions, options);
this.helpers = new serializer_utils_1.Helpers(this.options);
if (this.options.cache && this.options.cache instanceof cache_1.default) {
this.cache = this.options.cache;
}
// Setting type name.
this.collectionName = collectionName;
}
/**
* Gets the idKey (ie. name of the id field for the given model)
*/
getIdKeyFieldName() {
return this.options.idKey;
}
/**
* Gets the {@link Relator}s associated with this serializer
*/
getRelators() {
return this.helpers.relators;
}
/**
* Sets the {@link Relator}s associated with this serializer
*/
setRelators(relators) {
this.options.relators = relators;
this.helpers = new serializer_utils_1.Helpers(this.options);
}
/** @internal Generates a `ResourceIdentifier`. */
createIdentifier(data, options) {
// Get options
const resolvedOptions = options ?? this.options;
const identifierOptions = {};
if (resolvedOptions.metaizers.resource) {
identifierOptions.meta = resolvedOptions.metaizers.resource.metaize(data);
}
return new resource_identifier_model_1.default(data[resolvedOptions.idKey], this.collectionName, identifierOptions);
}
/** @internal Generates a `Resource`. */
async createResource(data, options, helpers, relatorDataCache) {
// Get options
const resolvedOptions = options ?? this.options;
const resolvedHelpers = helpers ?? this.helpers;
if (!resolvedOptions.idKey) {
throw new error_model_1.default({
detail: "options must provide a value for `idKey`",
});
}
const resourceOptions = {};
// Get ID before projections.
const id = data[resolvedOptions.idKey];
const type = this.collectionName;
// Get attributes
resourceOptions.attributes = resolvedHelpers.projectAttributes(data);
// Handling relators
if (resolvedHelpers.relators) {
const relationships = {};
await Promise.all(Object.entries(resolvedHelpers.relators).map(async ([name, relator]) => {
let relatedDataCache;
if (relatorDataCache) {
relatedDataCache = relatorDataCache.get(relator) || [];
relatorDataCache.set(relator, relatedDataCache);
}
const relationship = await relator.getRelationship(data, relatedDataCache);
if (relationship) {
relationships[name] = relationship;
}
}));
resourceOptions.relationships = relationships;
}
// Handling links
if (resolvedOptions.linkers?.resource) {
resourceOptions.links = {
self: resolvedOptions.linkers.resource.link(data),
};
}
if (resolvedOptions.metaizers?.resource) {
resourceOptions.meta = resolvedOptions.metaizers.resource.metaize(data);
}
return new resource_model_1.default(id, type, resourceOptions);
}
/**
* The actual serialization function.
*
* @param data - Data to serialize.
* @param options - Options to use at runtime.
*/
async serialize(data, options) {
// Merge options.
let o = this.options;
let h = this.helpers;
if (options !== undefined) {
o = (0, merge_1.default)({}, o, options);
h = new serializer_utils_1.Helpers(o);
}
const cache = o.cache instanceof cache_1.default ? o.cache : this.cache;
if (o.cache) {
const storedDocument = cache.get(data, options);
if (storedDocument) {
return storedDocument;
}
}
// Construct initial document and included data
const document = {};
// Document versioning
if (o.version) {
document.jsonapi = { ...document.jsonapi, version: o.version };
}
if (o.metaizers.jsonapi) {
document.jsonapi = {
...document.jsonapi,
meta: o.metaizers.jsonapi.metaize(),
};
}
// Cache data fetched during resource creation
const relatorDataCache = new Map();
const keys = [];
let wasSingle = false;
let dto;
let createIdentifier;
let createResource;
let relators;
// Check if only a relationship is desired
if (o.onlyRelationship) {
// Validate options.
if (h.relators === undefined) {
throw new TypeError(`"relators" must be defined when using "onlyRelationship"`);
}
if (!data || Array.isArray(data)) {
throw new TypeError(`Cannot serialize multiple primary datum using "onlyRelationship"`);
}
const relator = h.relators[o.onlyRelationship];
if (relator === undefined) {
throw new TypeError(`"onlyRelationship" is not the name of any collection name among the relators listed in "relators"`);
}
// Handle related data
const relatedData = await relator.getRelatedData(data);
// Handle related links
const links = relator.getRelatedLinks(data, relatedData);
if (links) {
document.links = links;
}
// Handle related meta
const meta = relator.getRelatedMeta(data, relatedData);
if (meta) {
document.meta = meta;
}
createIdentifier = (datum) => relator.getRelatedIdentifier(datum);
createResource = async (datum) => {
const resource = await relator.getRelatedResource(datum);
keys.push(resource.getKey());
return resource;
};
relators = relator.getRelatedRelators();
dto = relatedData;
}
else {
// Handle meta
if (o.metaizers.document) {
document.meta = o.metaizers.document.metaize(data);
}
// Handle links
if (o.linkers.document) {
document.links = {
...document.links,
self: o.linkers.document.link(data),
};
}
// Handle pagination links
if (o.linkers.paginator) {
const pagination = o.linkers.paginator.paginate(data);
if (pagination) {
document.links = { ...document.links, ...pagination };
}
}
createIdentifier = (datum) => this.createIdentifier(datum, o);
createResource = async (datum) => {
const resource = await this.createResource(datum, o, h, relatorDataCache);
keys.push(resource.getKey());
return resource;
};
relators = h.relators;
dto = data;
}
if (dto === undefined) {
return cache.set(data, document, options);
}
if (o.nullData || dto === null) {
document.data = null;
return cache.set(data, document, options);
}
// Handle `onlyIdentifier` option
if (o.onlyIdentifier) {
document.data = Array.isArray(dto)
? dto.map(createIdentifier)
: createIdentifier(dto);
return cache.set(data, document, options);
}
if (!Array.isArray(dto)) {
wasSingle = true;
dto = [dto];
}
if (o.asIncluded) {
document.data = dto.map(createIdentifier);
document.included = await Promise.all(dto.map(createResource));
}
else {
document.data = await Promise.all(dto.map(createResource));
}
const include = o.include || o.depth;
if (relators && include) {
document.included = (document.included || []).concat(await (0, serializer_utils_1.recurseRelators)(dto, relators, include, keys, relatorDataCache));
}
if (wasSingle) {
document.data = document.data[0];
}
return cache.set(data, document, options);
}
}
exports.default = Serializer;
//# sourceMappingURL=serializer.js.map