UNPKG

@cisstech/nestjs-expand

Version:

A NestJS module to build Dynamic Resource Expansion for APIs

193 lines 7.3 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ Object.defineProperty(exports, "__esModule", { value: true }); exports.handleExpansionErrors = exports.maskObjectWithThree = exports.createExpansionThree = void 0; const MINUS_OPERATOR = '-'; const WILDCARD_OPERATOR = '*'; const expandThreeFromDotNotation = (keys) => { const result = {}; const array = Array.isArray(keys) ? keys : [keys]; const mergedCommaSeparatedKeys = array .map((key) => key.trim().split(',')) .filter(Boolean) .reduce((acc, val) => acc.concat(val), []); mergedCommaSeparatedKeys.forEach((key) => { const wildcards = key.match(/\*/g)?.length ?? 0; if (wildcards > 1) { throw new Error(`NestJsExpand: key "${key}" cannot have more than on occurence of "${WILDCARD_OPERATOR}".`); } if (key.includes(MINUS_OPERATOR) && key.includes(WILDCARD_OPERATOR)) { throw new Error(`NestJsExpand: key "${key}" cannot contains both wildcard "${WILDCARD_OPERATOR}" and minus "${MINUS_OPERATOR}" operators.`); } const parts = key.replace(MINUS_OPERATOR, '').split('.'); let currentLevel = result; parts.forEach((part, index) => { if (typeof currentLevel[part] !== 'object') { // If the property doesn't exist, create an object for it currentLevel[part] = {}; } // If it's the last part of the key, set the value to true if (index === parts.length - 1) { currentLevel[part] = !key.startsWith(MINUS_OPERATOR); } // Move to the next level currentLevel = currentLevel[part]; }); }); return result; }; /** * Creates an ExpansionThree from a string or array of strings. * * @remarks * This function assumes that the object is already an ExpansionThree if it's an object. * @returns An ExpansionThree. */ const createExpansionThree = (object) => { if (!object) return {}; if (typeof object === 'string') { return expandThreeFromDotNotation(object); } if (Array.isArray(object)) { return expandThreeFromDotNotation(object); } return object; }; exports.createExpansionThree = createExpansionThree; /** * Masks an object with a given selection three. * @remarks * - Empty selection tree will return the target object as is. * - Array values are also supported. The selection tree at the array level will be applied to each item in the array. * - If a key is not present in the selection tree, it will be excluded from the result. * - `null` or `undefined` values will be returned as is. * @example * ```ts * * const targetObject = { * a: 1, * b: { * c: 2, * d: { * e: 3, * f: 4, * }, * g: 5, * }, * h: 6, * } * * const selectionTree = { * '*': true, * b: { * d: { * e: true, * }, * g: true, * }, * h: false * } * * const result = maskObjectWithThree(targetObject, selectionTree) * * // result = { * // a: 1, * // b: { * // d: { * // e: 3, * // }, * // g: 5, * // } * // } * ``` * * @param target - The object to be masked. * @param selection - The selection criteria for masking. * @returns The masked object. */ const maskObjectWithThree = (target, selection) => { const maskObjectWithThreeRecursive = (currentTarget, currentSelection) => { if (currentTarget == null) return currentTarget; // If the selection is empty, return the target as is const selectionKeys = Object.keys(currentSelection); if (selectionKeys.length === 0) return currentTarget; if (Array.isArray(currentTarget)) { return currentTarget.map((item) => maskObjectWithThreeRecursive(item, currentSelection)); } const maskedTarget = {}; // add all keys expect explicitly excluded ones if wildcard is present if (WILDCARD_OPERATOR in currentSelection) { const targetKeys = Object.keys(currentTarget); targetKeys.forEach((key) => { if (currentSelection[key] !== false) { maskedTarget[key] = currentTarget[key]; } }); } // handle explicit keys and minus operator const selectKeys = selectionKeys.filter((key) => key !== WILDCARD_OPERATOR); for (const selectKey of selectKeys) { if (!currentSelection[selectKey]) continue; // explicit select false if (!(selectKey in currentTarget)) continue; // select does not exists on target const value = typeof currentSelection[selectKey] === 'object' ? maskObjectWithThreeRecursive(currentTarget[selectKey], currentSelection[selectKey]) : currentTarget[selectKey]; maskedTarget[selectKey] = value; } return maskedTarget; }; return maskObjectWithThreeRecursive(target, selection); }; exports.maskObjectWithThree = maskObjectWithThree; /** * Handle expansion errors by attaching them to the response object. * For arrays, errors will be attached to individual items. * For objects, errors will be attached to the object directly. * * @param expansionErrors - Map of errors with their paths * @param result - Result object to attach errors to * @param includeErrors - Whether to include errors in the response */ const handleExpansionErrors = (expansionErrors, result, includeErrors = true) => { if (expansionErrors.size === 0 || !includeErrors) return; if (Array.isArray(result)) { // For array responses, attach errors to individual items // This requires the error path to include enough information to identify the item const errorsByItemIndex = new Map(); // Group errors by item index for (const [path, error] of expansionErrors.entries()) { // Extract the index from the path if available // Format expected: path.to.property[index] const indexMatch = path.match(/\[(\d+)\]/); if (indexMatch && indexMatch[1]) { const index = parseInt(indexMatch[1], 10); if (!errorsByItemIndex.has(index)) { errorsByItemIndex.set(index, {}); } // Replace the indexed part in the path with empty string const cleanPath = path.replace(/\[\d+\]/, ''); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion errorsByItemIndex.get(index)[cleanPath] = error; } } // Attach errors to individual items errorsByItemIndex.forEach((errors, index) => { if (index < result.length && Object.keys(errors).length > 0) { result[index]._expansionErrors = errors; } }); } else if (result && typeof result === 'object') { // For object responses, add errors as a property const errors = Object.fromEntries(expansionErrors.entries()); result._expansionErrors = errors; } }; exports.handleExpansionErrors = handleExpansionErrors; //# sourceMappingURL=expand.utils.js.map