UNPKG

kitsu-core

Version:

Simple, lightweight & framework agnostic JSON:API (de)serialsation components

344 lines (329 loc) 12.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.kitsuCore = {})); })(this, (function (exports) { 'use strict'; const camel = input => input.replace(/[-_][a-z\u00E0-\u00F6\u00F8-\u00FE]/g, match => match.slice(1).toUpperCase()); function deattribute(data) { if (typeof data === 'object' && data !== null) { if (Array.isArray(data)) data.map(el => deattribute(el));else if (typeof data.attributes === 'object' && !Array.isArray(data.attributes) && data.attributes !== null) { for (const key of Object.keys(data.attributes)) { if (!data.attributes.attributes) { data[key] = data.attributes[key]; } } if (data.attributes.attributes) { data.attributes = data.attributes.attributes; } else { delete data.attributes; } } } return data; } function error(Error) { if (Error.response) { const e = Error.response.data; if (e?.errors) Error.errors = e.errors; } throw Error; } function filterIncludes(included, { id, type }) { try { if (id && type) { const filtered = included.filter(el => { return el.id === id && el.type === type; })[0] || { id, type }; return Object.assign({}, filtered); } else { return {}; } } catch (E) { error(E); } } const isDeepEqual = (left, right) => { if (!left || !right) { return left === right; } const leftKeys = Object.keys(left); const rightKeys = Object.keys(right); if (leftKeys.length !== rightKeys.length) return false; for (const key of leftKeys) { const leftValue = left[key]; const rightValue = right[key]; const isObjects = isObject(leftValue) && isObject(rightValue); if (isObjects && !isDeepEqual(leftValue, rightValue) || !isObjects && leftValue !== rightValue) { return false; } } return true; }; const isObject = object => { return object != null && typeof object === 'object'; }; function link({ id, type }, included, previouslyLinked, relationshipCache) { const filtered = filterIncludes(included, { id, type }); previouslyLinked[`${type}#${id}`] = filtered; if (filtered.relationships) { linkRelationships(filtered, included, previouslyLinked, relationshipCache); } return deattribute(filtered); } function linkArray(data, included, key, previouslyLinked, relationshipCache) { data[key] = {}; if (data.relationships[key].links) data[key].links = data.relationships[key].links; if (data.relationships[key].meta) data[key].meta = data.relationships[key].meta; data[key].data = []; for (const resource of data.relationships[key].data) { const cache = previouslyLinked[`${resource.type}#${resource.id}`]; let relationship = cache || link(resource, included, previouslyLinked, relationshipCache); if (resource.meta || relationship.meta) { relationship = { ...relationship, meta: { ...relationship.meta, ...resource.meta } }; } data[key].data.push(relationship); } delete data.relationships[key]; } function linkObject(data, included, key, previouslyLinked, relationshipCache) { data[key] = {}; const resource = data.relationships[key].data; const cache = previouslyLinked[`${resource.type}#${resource.id}`]; if (cache) { let resourceCache = null; if (!isDeepEqual(cache.meta, resource.meta)) { resourceCache = { ...cache, meta: { ...cache.meta, ...resource.meta } }; } else { resourceCache = cache; } data[key].data = resourceCache; } else { data[key].data = link(resource, included, previouslyLinked, relationshipCache); } if (resource.meta || data[key].data.meta) { data[key].data = { ...data[key].data, meta: { ...data[key].data.meta, ...resource.meta } }; } const cacheKey = `${data.type}#${data.id}#${key}`; const relationships = relationshipCache[cacheKey] || data.relationships[key]; if (!relationshipCache[cacheKey]) relationshipCache[cacheKey] = relationships; if (relationships?.links) data[key].links = relationships.links; if (relationships?.meta) data[key].meta = relationships.meta; delete data.relationships[key]; } function linkAttr(data, key) { data[key] = {}; if (data.relationships[key].links) data[key].links = data.relationships[key].links; if (data.relationships[key].meta) data[key].meta = data.relationships[key].meta; delete data.relationships[key]; } function linkRelationships(data, included = [], previouslyLinked = {}, relationshipCache = {}) { const { relationships } = data; for (const key in relationships) { if (Array.isArray(relationships[key]?.data)) { linkArray(data, included, key, previouslyLinked, relationshipCache); } else if (relationships[key].data) { linkObject(data, included, key, previouslyLinked, relationshipCache); } else { linkAttr(data, key); } } if (Object.keys(relationships || []).length === 0 && typeof relationships === 'object' && !Array.isArray(relationships) && relationships !== null) { delete data.relationships; } return data; } function deserialiseArray(response) { const previouslyLinked = {}; const relationshipCache = {}; for (let value of response.data) { const included = [...response.data.map(item => ({ ...item, relationships: { ...item.relationships } })), ...(response.included || [])]; value = linkRelationships(value, included, previouslyLinked, relationshipCache); if (value.attributes) value = deattribute(value); response.data[response.data.indexOf(value)] = value; } return response; } function deserialise(response) { if (!response) return; if (Array.isArray(response.data)) response = deserialiseArray(response);else if (response.included) response.data = linkRelationships(response.data, response.included);else if (typeof response.data === 'object' && response.data !== null) response.data = linkRelationships(response.data); delete response.included; if (response.data?.attributes) response.data = deattribute(response.data); return response; } const kebab = input => input.charAt(0).toLowerCase() + input.slice(1).replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, match => '-' + match.toLowerCase()); function queryFormat(value, key, traditional) { if (traditional && value !== null && Array.isArray(value)) return value.map(v => queryFormat(v, key, traditional)).join('&'); if (!traditional && value !== null && Array.isArray(value)) return value.map(v => queryFormat(v, `${key}[]`, traditional)).join('&');else if (value !== null && typeof value === 'object') return query(value, key, traditional);else return encodeURIComponent(key) + '=' + encodeURIComponent(value); } function paramKeyName(param) { if (param === null || param === undefined) return ''; if (['[]', ']['].includes(param.slice(-2))) { return `[${param.slice(0, -2)}][]`; } return `[${param}]`; } function query(params, prefix = undefined, traditional = true) { const str = []; for (const param in params) { str.push(queryFormat(params[param], prefix ? `${prefix}${paramKeyName(param)}` : param, traditional)); } return str.join('&'); } function isValid(isArray, type, payload, method) { const requireID = new Error(`${method} requires an ID for the ${type} type`); if (type === undefined) { throw new Error(`${method} requires a resource type`); } if (isArray) { if (method !== 'POST' && payload.length > 0) { for (const resource of payload) { if (!resource.id) throw requireID; } } } else { if (typeof payload !== 'object' || method !== 'POST' && Object.keys(payload).length === 0) { throw new Error(`${method} requires an object or array body`); } if (method !== 'POST' && !payload.id) { throw requireID; } } } function serialiseRelationOne(node, nodeType) { if (node === null) return node; let relation = {}; for (const prop of Object.keys(node)) { if (['id', 'type'].includes(prop)) relation[prop] = node[prop];else relation = serialiseAttr(node[prop], prop, relation); } if (!relation.type) relation.type = nodeType; return relation; } function serialiseRelationMany(node, nodeType) { const relation = []; for (const prop of node) { relation.push(serialiseRelationOne(prop, nodeType)); } return relation; } function serialiseRelation(node, nodeType, key, data) { if (!data.relationships) data.relationships = {}; data.relationships[key] = { data: Array.isArray(node.data) ? serialiseRelationMany(node.data, nodeType) : serialiseRelationOne(node.data, nodeType) }; if (node?.links?.self || node?.links?.related) data.relationships[key].links = node.links; if (node?.meta) data.relationships[key].meta = node.meta; return data; } function serialiseAttr(node, key, data) { if (!data.attributes) data.attributes = {}; if (key === 'links' && (typeof node.self === 'string' || typeof node.related === 'string')) data.links = node;else if (key === 'meta' && typeof node === 'object' && !Array.isArray(node) && node !== null) data.meta = node;else data.attributes[key] = node; return data; } function hasID(node) { if (node?.data === null || Array.isArray(node?.data) && node?.data?.length === 0) return true; if (!node.data) return false; const nodeData = Array.isArray(node.data) ? node.data[0] : node.data; return Object.prototype.hasOwnProperty.call(nodeData, 'id'); } function serialiseRootArray(type, payload, method, options) { isValid(true, type, payload, method); const data = []; for (const resource of payload) { data.push(serialiseRootObject(type, resource, method, options).data); } return { data }; } function serialiseRootObject(type, payload, method, options) { isValid(false, type, payload, method); type = options.pluralTypes(options.camelCaseTypes(type)); let data = { type }; if (payload?.id) data.id = String(payload.id); for (const key in payload) { const node = payload[key]; const nodeType = options.pluralTypes(options.camelCaseTypes(key)); if (typeof node === 'object' && !Array.isArray(node) && node !== null && hasID(node)) { data = serialiseRelation(node, nodeType, key, data); } else if (key !== 'id' && key !== 'type') { data = serialiseAttr(node, key, data); } } return { data }; } function serialise(type, data = {}, method = 'POST', options = {}) { try { if (!options.camelCaseTypes) options.camelCaseTypes = s => s; if (!options.pluralTypes) options.pluralTypes = s => s; if (data === null || Array.isArray(data) && data.length === 0) return { data }; if (Array.isArray(data) && data?.length > 0) return serialiseRootArray(type, data, method, options);else return serialiseRootObject(type, data, method, options); } catch (E) { throw error(E); } } const snake = input => input.charAt(0).toLowerCase() + input.slice(1).replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, match => '_' + match.toLowerCase()); function splitModel(url, options = {}) { if (!options.pluralModel) options.pluralModel = s => s; if (!options.resourceCase) options.resourceCase = s => s; const urlSegments = url.split('/'); const resourceModel = urlSegments.pop() || ''; urlSegments.push(options.pluralModel(options.resourceCase(resourceModel))); const newUrl = urlSegments.join('/'); return [resourceModel, newUrl]; } exports.camel = camel; exports.deattribute = deattribute; exports.deserialise = deserialise; exports.error = error; exports.filterIncludes = filterIncludes; exports.kebab = kebab; exports.linkRelationships = linkRelationships; exports.paramKeyName = paramKeyName; exports.query = query; exports.serialise = serialise; exports.snake = snake; exports.splitModel = splitModel; }));