json-api-serializer
Version:
Framework agnostic JSON API serializer.
1,028 lines (901 loc) • 35.9 kB
JavaScript
require('setimmediate');
const {
pick,
isEmpty,
omit,
isPlainObject,
isObjectLike,
transform,
get,
set,
toCamelCase,
toKebabCase,
toSnakeCase,
LRU,
} = require('./helpers');
const { validateOptions, validateDynamicTypeOptions, validateError } = require('./validator');
/**
* JSONAPISerializer class.
*
* @example
* const JSONAPISerializer = require('json-api-serializer');
*
* // Create an instance of JSONAPISerializer with default settings
* const serializer = new JSONAPISerializer();
* @class JSONAPISerializer
* @param {Options} [opts] Global options.
*/
module.exports = class JSONAPISerializer {
constructor(opts) {
this.opts = opts || {};
this.schemas = {};
// Size of cache used for convertCase, 0 results in an infinitely sized cache
const { convertCaseCacheSize = 5000 } = this.opts;
// Cache of strings to convert to their converted values per conversion type
this.convertCaseMap = {
camelCase: new LRU(convertCaseCacheSize),
kebabCase: new LRU(convertCaseCacheSize),
snakeCase: new LRU(convertCaseCacheSize),
};
}
/**
* Register a resource with its type, schema name, and configuration options.
*
* @function JSONAPISerializer#register
* @param {string} type resource's type.
* @param {string|Options} [schema='default'] schema name.
* @param {Options} [options] options.
*/
register(type, schema, options) {
if (typeof schema === 'object') {
options = schema;
schema = 'default';
}
schema = schema || 'default';
options = { ...this.opts, ...options };
this.schemas[type] = this.schemas[type] || {};
this.schemas[type][schema] = validateOptions(options);
}
/**
* Serialze input data to a JSON API compliant response.
* Input data can be a simple object or an array of objects.
*
* @see {@link http://jsonapi.org/format/#document-top-level}
* @function JSONAPISerializer#serialize
* @param {string|DynamicTypeOptions} type resource's type as string or a dynamic type options as object.
* @param {object|object[]} data input data.
* @param {string|object} [schema='default'] resource's schema name.
* @param {object} [extraData] additional data that can be used in topLevelMeta options.
* @param {boolean} [excludeData] boolean that can be set to exclude the `data` property in serialized data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {object|object[]} serialized data.
*/
serialize(type, data, schema, extraData, excludeData, overrideSchemaOptions = {}) {
// Support optional arguments (schema)
if (arguments.length === 3) {
if (typeof schema === 'object') {
extraData = schema;
schema = 'default';
}
}
schema = schema || 'default';
extraData = extraData || {};
const included = new Map();
const isDynamicType = typeof type === 'object';
const options = this._getSchemaOptions(type, schema, overrideSchemaOptions);
let dataProperty;
if (excludeData) {
dataProperty = undefined;
} else if (isDynamicType) {
dataProperty = this.serializeMixedResource(
options,
data,
included,
extraData,
overrideSchemaOptions
);
} else {
dataProperty = this.serializeResource(
type,
data,
options,
included,
extraData,
overrideSchemaOptions
);
}
return {
jsonapi: options.jsonapiObject ? { version: '1.0' } : undefined,
meta: this.processOptionsValues(data, extraData, options.topLevelMeta, 'extraData'),
links: this.processOptionsValues(data, extraData, options.topLevelLinks, 'extraData'),
data: dataProperty,
included: included.size ? [...included.values()] : undefined,
};
}
/**
* Asynchronously serialize input data to a JSON API compliant response.
* Input data can be a simple object or an array of objects.
*
* @see {@link http://jsonapi.org/format/#document-top-level}
* @function JSONAPISerializer#serializeAsync
* @param {string|DynamicTypeOptions} type resource's type as string or a dynamic type options as object.
* @param {object|object[]} data input data.
* @param {string} [schema='default'] resource's schema name.
* @param {object} [extraData] additional data that can be used in topLevelMeta options.
* @param {boolean} [excludeData] boolean that can be set to exclude the `data` property in serialized data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {Promise} resolves with serialized data.
*/
serializeAsync(type, data, schema, extraData, excludeData, overrideSchemaOptions = {}) {
// Support optional arguments (schema)
if (arguments.length === 3) {
if (typeof schema === 'object') {
extraData = schema;
schema = 'default';
}
}
schema = schema || 'default';
extraData = extraData || {};
const included = new Map();
const isDataArray = Array.isArray(data);
const isDynamicType = typeof type === 'object';
const arrayData = isDataArray ? data : [data];
const serializedData = [];
const that = this;
let i = 0;
const options = this._getSchemaOptions(type, schema, overrideSchemaOptions);
return new Promise((resolve, reject) => {
/**
* Non-blocking serialization using the immediate queue.
*
* @see {@link https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/}
*/
function next() {
setImmediate(() => {
if (excludeData) {
return resolve();
}
if (i >= arrayData.length) {
return resolve(serializedData);
}
try {
// Serialize a single item of the data-array.
const serializedItem = isDynamicType
? that.serializeMixedResource(
type,
arrayData[i],
included,
extraData,
overrideSchemaOptions
)
: that.serializeResource(
type,
arrayData[i],
options,
included,
extraData,
overrideSchemaOptions
);
if (serializedItem !== null) {
serializedData.push(serializedItem);
}
i += 1;
return next();
} catch (e) {
return reject(e);
}
});
}
next();
}).then((result) => {
let dataProperty;
if (typeof result === 'undefined') {
dataProperty = undefined;
} else if (isDataArray) {
dataProperty = result;
} else {
dataProperty = result[0] || null;
}
return {
jsonapi: options.jsonapiObject ? { version: '1.0' } : undefined,
meta: this.processOptionsValues(data, extraData, options.topLevelMeta, 'extraData'),
links: this.processOptionsValues(data, extraData, options.topLevelLinks, 'extraData'),
// If the source data was an array, we just pass the serialized data array.
// Otherwise we try to take the first (and only) item of it or pass null.
data: dataProperty,
included: included.size ? [...included.values()] : undefined,
};
});
}
/**
* Deserialize JSON API document data.
* Input data can be a simple object or an array of objects.
*
* @function JSONAPISerializer#deserialize
* @param {string|DynamicTypeOptions} type resource's type as string or a dynamic type options as object.
* @param {object} data JSON API input data.
* @param {string} [schema='default'] resource's schema name.
* @returns {object} deserialized data.
*/
deserialize(type, data, schema) {
schema = schema || 'default';
if (typeof type === 'object') {
type = validateDynamicTypeOptions(type);
} else {
if (!this.schemas[type]) {
throw new Error(`No type registered for ${type}`);
}
if (schema && !this.schemas[type][schema]) {
throw new Error(`No schema ${schema} registered for ${type}`);
}
}
let deserializedData = {};
if (data.data) {
deserializedData = Array.isArray(data.data)
? data.data.map((resource) =>
this.deserializeResource(type, resource, schema, data.included)
)
: this.deserializeResource(type, data.data, schema, data.included);
}
return deserializedData;
}
/**
* Asynchronously Deserialize JSON API document data.
* Input data can be a simple object or an array of objects.
*
* @function JSONAPISerializer#deserializeAsync
* @param {string|DynamicTypeOptions} type resource's type as string or a dynamic type options as object.
* @param {object} data JSON API input data.
* @param {string} [schema='default'] resource's schema name.
* @returns {Promise} resolves with serialized data.
*/
deserializeAsync(type, data, schema) {
schema = schema || 'default';
if (typeof type === 'object') {
type = validateDynamicTypeOptions(type);
} else {
if (!this.schemas[type]) {
throw new Error(`No type registered for ${type}`);
}
if (schema && !this.schemas[type][schema]) {
throw new Error(`No schema ${schema} registered for ${type}`);
}
}
const isDataArray = Array.isArray(data.data);
let i = 0;
const arrayData = isDataArray ? data.data : [data.data];
const deserializedData = [];
const that = this;
return new Promise((resolve, reject) => {
/**
* Non-blocking deserialization using the immediate queue.
*
* @see {@link https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/}
*/
function next() {
setImmediate(() => {
if (i >= arrayData.length) {
return resolve(isDataArray ? deserializedData : deserializedData[0]);
}
try {
// Serialize a single item of the data-array.
const deserializedItem = that.deserializeResource(
type,
arrayData[i],
schema,
data.included
);
deserializedData.push(deserializedItem);
i += 1;
return next();
} catch (e) {
return reject(e);
}
});
}
next();
});
}
/**
* Serialize any error into a JSON API error document.
* Input data can be:
* - An `Error` or an array of `Error` instances.
* - A JSON API error object or an array of JSON API error objects.
*
* @see {@link http://jsonapi.org/format/#errors}
* @function JSONAPISerializer#serializeError
* @param {Error|Error[]|object|object[]} error an Error, an array of Error, a JSON API error object, an array of JSON API error object.
* @returns {object} resolves with serialized error.
*/
serializeError(error) {
return {
errors: Array.isArray(error)
? error.map((err) => validateError(err))
: [validateError(error)],
};
}
/**
* Deserialize a single JSON API resource.
* Input data must be a simple object.
*
* @function JSONAPISerializer#deserializeResource
* @private
* @param {string|DynamicTypeOptions} type resource's type as string or an object with a dynamic type resolved from data.
* @param {object} data JSON API resource data.
* @param {string} [schema='default'] resource's schema name.
* @param {Map<string, object>} included Included resources.
* @param {string[]} lineage resource identifiers already deserialized to prevent circular references.
* @returns {object} deserialized data.
*/
deserializeResource(type, data, schema = 'default', included, lineage = []) {
if (typeof type === 'object') {
type = typeof type.type === 'function' ? type.type(data) : get(data, type.type);
}
if (!type) {
throw new Error(`No type can be resolved from data: ${JSON.stringify(data)}`);
}
if (!this.schemas[type]) {
throw new Error(`No type registered for ${type}`);
}
const options = this.schemas[type][schema];
let deserializedData = {};
if (data.id !== undefined) {
deserializedData[options.id] = data.id;
}
if (data.attributes && options.whitelistOnDeserialize.length) {
data.attributes = pick(data.attributes, options.whitelistOnDeserialize);
}
if (data.attributes && options.blacklistOnDeserialize.length) {
data.attributes = omit(data.attributes, options.blacklistOnDeserialize);
}
Object.assign(deserializedData, data.attributes);
// Deserialize relationships
if (data.relationships) {
Object.keys(data.relationships).forEach((relationshipProperty) => {
const relationship = data.relationships[relationshipProperty];
const relationshipType = this._getRelationshipDataType(relationship.data);
const relationshipKey = options.unconvertCase
? this._convertCase(relationshipProperty, options.unconvertCase)
: relationshipProperty;
const relationshipOptions =
options.relationships[relationshipKey] || this.schemas[relationshipType];
const deserializeFunction = (relationshipData) => {
if (relationshipOptions && relationshipOptions.deserialize) {
return relationshipOptions.deserialize(relationshipData);
}
return relationshipData.id;
};
if (relationship.data !== undefined) {
if (relationship.data === null) {
// null data
set(
deserializedData,
(relationshipOptions && relationshipOptions.alternativeKey) || relationshipKey,
null
);
} else {
if ((relationshipOptions && relationshipOptions.alternativeKey) || !included) {
set(
deserializedData,
(relationshipOptions && relationshipOptions.alternativeKey) || relationshipKey,
Array.isArray(relationship.data)
? relationship.data.map((d) => deserializeFunction(d))
: deserializeFunction(relationship.data)
);
}
if (included) {
const deserializeIncludedRelationship = (relationshipData) => {
const lineageCopy = [...lineage];
// Prevent circular relationships
const lineageKey = `${relationshipData.type}-${relationshipData.id}`;
const isCircular = lineageCopy.includes(lineageKey);
if (isCircular) {
return deserializeFunction(relationshipData);
}
lineageCopy.push(lineageKey);
return this.deserializeIncluded(
relationshipData.type,
relationshipData.id,
relationshipOptions,
included,
lineageCopy,
deserializeFunction
);
};
const deserializedIncludedRelationship = Array.isArray(relationship.data)
? relationship.data.map((d) => deserializeIncludedRelationship(d))
: deserializeIncludedRelationship(relationship.data);
// not set to deserializedData if alternativeKey is set and relationship data is not in the included array (value is the same as alternativeKey value)
if (
!(
relationshipOptions &&
relationshipOptions.alternativeKey &&
deserializedIncludedRelationship.toString() ===
get(deserializedData, relationshipOptions.alternativeKey).toString()
)
) {
set(deserializedData, relationshipKey, deserializedIncludedRelationship);
}
}
}
}
});
}
if (options.unconvertCase) {
deserializedData = this._convertCase(deserializedData, options.unconvertCase);
}
if (data.links) {
deserializedData.links = data.links;
}
if (data.meta) {
deserializedData.meta = data.meta;
}
if (options.afterDeserialize) {
return options.afterDeserialize(deserializedData);
}
return deserializedData;
}
/**
* Deserialize included
*
* @function JSONAPISerializer#deserializeIncluded
* @private
* @param {string} type resource's type as string or an object with a dynamic type resolved from data.
* @param {string} id identifier of the resource.
* @param {RelationshipOptions} relationshipOpts relationship option.
* @param {Map<string, object>} included Included resources.
* @param {string[]} lineage resource identifiers already deserialized to prevent circular references.
* @param {Function} deserializeFunction a deserialize function
* @returns {object} deserialized data.
*/
deserializeIncluded(type, id, relationshipOpts, included, lineage, deserializeFunction) {
const includedResource = included.find(
(resource) => resource.type === type && resource.id === id
);
if (!includedResource) {
return deserializeFunction({ type, id });
}
if (!relationshipOpts) {
throw new Error(`No type registered for ${type}`);
}
return this.deserializeResource(
type,
includedResource,
relationshipOpts.schema,
included,
lineage
);
}
/**
* Serialize resource objects.
*
* @see {@link http://jsonapi.org/format/#document-resource-objects}
* @function JSONAPISerializer#serializeDocument
* @private
* @param {string} type resource's type.
* @param {object|object[]} data input data.
* @param {Options} options resource's configuration options.
* @param {Map<string, object>} [included] Included resources.
* @param {object} [extraData] additional data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {object|object[]} serialized data.
*/
serializeResource(type, data, options, included, extraData, overrideSchemaOptions = {}) {
if (isEmpty(data)) {
// Return [] or null
return Array.isArray(data) ? data : null;
}
if (Array.isArray(data)) {
return data.map((d) =>
this.serializeResource(type, d, options, included, extraData, overrideSchemaOptions)
);
}
if (options.beforeSerialize) {
data = options.beforeSerialize(data);
}
return {
type,
id: data[options.id] ? data[options.id].toString() : undefined,
attributes: this.serializeAttributes(data, options),
relationships: this.serializeRelationships(
data,
options,
included,
extraData,
overrideSchemaOptions
),
meta: this.processOptionsValues(data, extraData, options.meta),
links: this.processOptionsValues(data, extraData, options.links),
};
}
/**
* Serialize mixed resource object with a dynamic type resolved from data
*
* @see {@link http://jsonapi.org/format/#document-resource-objects}
* @function JSONAPISerializer#serializeMixedResource
* @private
* @param {DynamicTypeOptions} typeOption a dynamic type options.
* @param {object|object[]} data input data.
* @param {Map<string, object>} [included] Included resources.
* @param {object} [extraData] additional data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {object|object[]} serialized data.
*/
serializeMixedResource(typeOption, data, included, extraData, overrideSchemaOptions = {}) {
if (isEmpty(data)) {
// Return [] or null
return Array.isArray(data) ? data : null;
}
if (Array.isArray(data)) {
return data.map((d) =>
this.serializeMixedResource(typeOption, d, included, extraData, overrideSchemaOptions)
);
}
// Resolve type from data (can be a string or a function deriving a type-string from each data-item)
const type =
typeof typeOption.type === 'function' ? typeOption.type(data) : get(data, typeOption.type);
if (!type) {
throw new Error(`No type can be resolved from data: ${JSON.stringify(data)}`);
}
if (!this.schemas[type]) {
throw new Error(`No type registered for ${type}`);
}
const options = this._getSchemaOptions(type, 'default', overrideSchemaOptions);
return this.serializeResource(type, data, options, included, extraData, overrideSchemaOptions);
}
/**
* Serialize 'attributes' key of resource objects: an attributes object representing some of the resource's data.
*
* @see {@link http://jsonapi.org/format/#document-resource-object-attributes}
* @function JSONAPISerializer#serializeAttributes
* @private
* @param {object|object[]} data input data.
* @param {Options} options resource's configuration options.
* @returns {object} serialized attributes.
*/
serializeAttributes(data, options) {
if (options.whitelist && options.whitelist.length) {
data = pick(data, options.whitelist);
}
// Support alternativeKey options for relationships
const alternativeKeys = [];
Object.keys(options.relationships).forEach((key) => {
const rOptions = options.relationships[key];
if (rOptions.alternativeKey) {
alternativeKeys.push(rOptions.alternativeKey);
}
});
// Remove unwanted keys
let serializedAttributes = omit(data, [
options.id,
...Object.keys(options.relationships),
...alternativeKeys,
...options.blacklist,
]);
if (options.convertCase) {
serializedAttributes = this._convertCase(serializedAttributes, options.convertCase);
}
return Object.keys(serializedAttributes).length ? serializedAttributes : undefined;
}
/**
* Serialize 'relationships' key of resource objects: a relationships object describing relationships between the resource and other JSON API resources.
*
* @see {@link http://jsonapi.org/format/#document-resource-object-relationships}
* @function JSONAPISerializer#serializeRelationships
* @private
* @param {object|object[]} data input data.
* @param {Options} options resource's configuration options.
* @param {Map<string, object>} [included] Included resources.
* @param {object} [extraData] additional data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {object} serialized relationships.
*/
serializeRelationships(data, options, included, extraData, overrideSchemaOptions = {}) {
const serializedRelationships = {};
Object.keys(options.relationships).forEach((relationship) => {
const relationshipOptions = options.relationships[relationship];
// Support alternativeKey options for relationships
let relationshipKey = relationship;
if (!data[relationship] && relationshipOptions.alternativeKey) {
relationshipKey = relationshipOptions.alternativeKey;
}
const serializeRelationship = {
links: this.processOptionsValues(data, extraData, relationshipOptions.links),
meta: this.processOptionsValues(data, extraData, relationshipOptions.meta),
data: this.serializeRelationship(
relationshipOptions.type,
relationshipOptions.schema,
get(data, relationshipKey),
included,
data,
extraData,
overrideSchemaOptions
),
};
if (
serializeRelationship.data !== undefined ||
serializeRelationship.links !== undefined ||
serializeRelationship.meta !== undefined
) {
// Convert case
relationship = options.convertCase
? this._convertCase(relationship, options.convertCase)
: relationship;
serializedRelationships[relationship] = serializeRelationship;
}
});
return Object.keys(serializedRelationships).length ? serializedRelationships : undefined;
}
/**
* Serialize 'data' key of relationship's resource objects.
*
* @see {@link http://jsonapi.org/format/#document-resource-object-linkage}
* @function JSONAPISerializer#serializeRelationship
* @private
* @param {string|Function} rType the relationship's type.
* @param {string} rSchema the relationship's schema
* @param {object|object[]} rData relationship's data.
* @param {Map<string, object>} [included] Included resources.
* @param {object} [data] the entire resource's data.
* @param {object} [extraData] additional data.
* @param {object} [overrideSchemaOptions={}] additional schema options, a map of types with options to override.
* @returns {object|object[]} serialized relationship data.
*/
serializeRelationship(
rType,
rSchema,
rData,
included,
data,
extraData,
overrideSchemaOptions = {}
) {
included = included || new Map();
const schema = rSchema || 'default';
// No relationship data
if (rData === undefined || rData === null) {
return rData;
}
if (typeof rData === 'object' && isEmpty(rData)) {
// Return [] or null
return Array.isArray(rData) ? [] : null;
}
if (Array.isArray(rData)) {
return rData.map((d) =>
this.serializeRelationship(
rType,
schema,
d,
included,
data,
extraData,
overrideSchemaOptions
)
);
}
// Resolve relationship type
const type = typeof rType === 'function' ? rType(rData, data) : rType;
if (!type) {
throw new Error(`No type can be resolved from relationship's data: ${JSON.stringify(rData)}`);
}
if (!this.schemas[type]) {
throw new Error(`No type registered for "${type}"`);
}
if (!this.schemas[type][schema]) {
throw new Error(`No schema "${schema}" registered for type "${type}"`);
}
let rOptions = this.schemas[type][schema];
if (overrideSchemaOptions[type]) {
// Merge default (registered) options and extra options into new options object
rOptions = { ...rOptions, ...overrideSchemaOptions[type] };
}
const serializedRelationship = { type };
// Support for unpopulated relationships (an id, or array of ids)
if (!isObjectLike(rData)) {
serializedRelationship.id = rData.toString();
} else {
const serializedIncluded = this.serializeResource(
type,
rData,
rOptions,
included,
extraData,
overrideSchemaOptions
);
serializedRelationship.id = serializedIncluded.id;
const identifier = `${type}-${serializedRelationship.id}`;
// Not include relationship object which only contains an id
if (
(serializedIncluded.attributes && Object.keys(serializedIncluded.attributes).length) ||
(serializedIncluded.relationships && Object.keys(serializedIncluded.relationships).length)
) {
// Merge relationships data if already included
if (included.has(identifier)) {
const alreadyIncluded = included.get(identifier);
if (serializedIncluded.relationships) {
alreadyIncluded.relationships = {
...alreadyIncluded.relationships,
...serializedIncluded.relationships,
};
included.set(identifier, alreadyIncluded);
}
} else {
included.set(identifier, serializedIncluded);
}
}
}
return serializedRelationship;
}
/**
* Process options values.
* Allows options to be an object or a function with 1 or 2 arguments
*
* @function JSONAPISerializer#processOptionsValues
* @private
* @param {object} data data passed to functions options.
* @param {object} extraData additional data passed to functions options.
* @param {object} options configuration options.
* @param {string} [fallbackModeIfOneArg] fallback mode if only one argument is passed to function.
* Avoid breaking changes with issue https://github.com/danivek/json-api-serializer/issues/27.
* @returns {object} processed options.
*/
processOptionsValues(data, extraData, options, fallbackModeIfOneArg) {
let processedOptions = {};
if (options && typeof options === 'function') {
// Backward compatible with functions with one 'extraData' argument
processedOptions =
fallbackModeIfOneArg === 'extraData' && options.length === 1
? options(extraData)
: options(data, extraData);
} else {
Object.keys(options).forEach((key) => {
let processedValue = {};
if (options[key] && typeof options[key] === 'function') {
// Backward compatible with functions with one 'extraData' argument
processedValue =
fallbackModeIfOneArg === 'extraData' && options[key].length === 1
? options[key](extraData)
: options[key](data, extraData);
} else {
processedValue = options[key];
}
Object.assign(processedOptions, { [key]: processedValue });
});
}
return processedOptions && Object.keys(processedOptions).length ? processedOptions : undefined;
}
/**
* Get the schema options for the given type and optional schema.
*
* @function JSONAPISerializer#_getSchemaOptions
* @private
* @param {string|object} [type] the type to get schema options for.
* @param {schema} [schema] the schema name to get options for.
* @param {object} [overrideSchemaOptions] optional options to override schema options.
* @returns {object} the schema options for the given type.
*/
_getSchemaOptions(type, schema, overrideSchemaOptions = {}) {
const isDynamicType = typeof type === 'object';
const overrideType = isDynamicType ? type.type : type;
const overrideOptions = { ...(overrideSchemaOptions[overrideType] || {}) };
if (isDynamicType) {
return validateDynamicTypeOptions(type);
}
if (!this.schemas[type]) {
throw new Error(`No type registered for ${type}`);
}
if (schema && !this.schemas[type][schema]) {
throw new Error(`No schema ${schema} registered for ${type}`);
}
return { ...this.schemas[type][schema], ...overrideOptions };
}
_getRelationshipDataType(data) {
if (data === null || typeof data === 'undefined') {
return null;
}
if (Array.isArray(data)) {
return get(data[0], 'type');
}
return data.type;
}
/**
* Recursively convert object keys case
*
* @function JSONAPISerializer#_convertCase
* @private
* @param {object|object[]|string} data to convert
* @param {string} convertCaseOptions can be snake_case', 'kebab-case' or 'camelCase' format.
* @returns {object} Object with it's keys converted as per the convertCaseOptions
*/
_convertCase(data, convertCaseOptions) {
if (Array.isArray(data)) {
return data.map((item) => {
if (item && (Array.isArray(item) || isPlainObject(item))) {
return this._convertCase(item, convertCaseOptions);
}
return item;
});
}
if (isPlainObject(data)) {
return transform(
data,
(result, value, key) => {
let converted;
if (value && (Array.isArray(value) || isPlainObject(value))) {
converted = this._convertCase(value, convertCaseOptions);
} else {
converted = value;
}
result[this._convertCase(key, convertCaseOptions)] = converted;
return result;
},
{}
);
}
if (typeof data === 'string') {
let converted;
switch (convertCaseOptions) {
case 'snake_case':
converted = this.convertCaseMap.snakeCase.get(data);
if (!converted) {
converted = toSnakeCase(data);
this.convertCaseMap.snakeCase.set(data, converted);
}
break;
case 'kebab-case':
converted = this.convertCaseMap.kebabCase.get(data);
if (!converted) {
converted = toKebabCase(data);
this.convertCaseMap.kebabCase.set(data, converted);
}
break;
case 'camelCase':
converted = this.convertCaseMap.camelCase.get(data);
if (!converted) {
converted = toCamelCase(data);
this.convertCaseMap.camelCase.set(data, converted);
}
break;
default: // Do nothing
}
return converted;
}
return data;
}
};
/**
* @typedef {object} Options
* @property {string} [id='id'] the key to use as the reference. Default = 'id'
* @property {string[]} [blacklist=[]] an array of blacklisted attributes. Default = []
* @property {string[]} [whitelist=[]] an array of whitelisted attributes. Default = []
* @property {boolean} [jsonapiObject=true] enable/Disable JSON API Object. Default = true
* @property {Function|object} [links] describes the links inside data
* @property {Function|object} [topLevelLinks] describes the top-level links
* @property {Function|object} [topLevelMeta] describes the top-level meta
* @property {Function|object} [meta] describes resource-level meta
* @property {object.<string, RelationshipOptions>} [relationships] an object defining some relationships
* @property {string[]} [blacklistOnDeserialize=[]] an array of blacklisted attributes. Default = []
* @property {string[]} [whitelistOnDeserialize=[]] an array of whitelisted attributes. Default = []
* @property {('kebab-case'|'snake_case'|'camelCase')} [convertCase] case conversion for serializing data
* @property {('kebab-case'|'snake_case'|'camelCase')} [unconvertCase] case conversion for deserializing data
* @property {number} [convertCaseCacheSize=5000] When using convertCase, a LRU cache is utilized for optimization. The default size of the cache is 5000 per conversion type.
* @property {Function} [beforeSerialize] a function to transform data before serialization.
* @property {Function} [afterDeserialize] a function to transform data after deserialization.
*/
/**
* @typedef {object} RelationshipOptions
* @property {string|Function} type a string or a function for the type to use for serializing the relationship (type need to be register)
* @property {string} [alternativeKey] an alternative key (string or path) to use if relationship key not exist (example: 'author_id' as an alternative key for 'author' relationship)
* @property {string} [schema] a custom schema for serializing the relationship. If no schema define, it use the default one.
* @property {Function|object} [links] describes the links for the relationship
* @property {Function|object} [meta] describes meta that contains non-standard meta-information about the relationship
* @property {Function} [deserialize] describes the function which should be used to deserialize a related property which is not included in the JSON:API document
*/
/**
*
* @typedef {object} DynamicTypeOptions
* @property {string} id a string for the path to the key to use to determine type or a function deriving a type-string from each data-item.
* @property {boolean} [jsonapiObject=true] enable/Disable JSON API Object.
* @property {Function|object} [topLevelLinks] describes the top-level links
* @property {Function|object} [topLevelMeta] describes the top-level meta.
*/