UNPKG

@crudmates/masq

Version:

Flexible field masking and relation selection for REST APIs and data filtering.

265 lines 9.4 kB
"use strict"; /** * masq: Flexible field masking and relation selection for REST, GraphQL, and data APIs. * @packageDocumentation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.parseFieldsSpec = parseFieldsSpec; exports.applyFieldMask = applyFieldMask; exports.applyMask = applyMask; exports.parseRelationsSpec = parseRelationsSpec; exports.isValidMask = isValidMask; /** * Parse a field mask string into an object structure, supporting aliasing with <alias>. * Examples: * id,name => { id: true, name: true } * model<models>(make<makes>) => { model: { __alias: 'models', make: { __alias: 'makes' } } } * * => { '*': true } * id,name,branch(*) => { id: true, name: true, branch: { '*': true } } * *,-password => { '*': true, password: false } * id,name,-secret,branch(id,name,-internal) => { id: true, name: true, secret: false, branch: { id: true, name: true, internal: false } } */ function parseFieldsSpec(fieldsSpec) { const result = {}; if (fieldsSpec.trim() === '*') return { '*': true }; const fields = []; let currentField = ''; let parenthesesDepth = 0; for (let i = 0; i < fieldsSpec.length; i++) { const char = fieldsSpec[i]; if (char === ',' && parenthesesDepth === 0) { if (currentField.trim()) fields.push(currentField.trim()); currentField = ''; } else { if (char === '(') parenthesesDepth++; else if (char === ')') parenthesesDepth--; currentField += char; } } if (currentField.trim()) fields.push(currentField.trim()); for (const field of fields) { const aliasMatch = field.match(/^([\w]+)<([\w]+)>(.*)$/); if (aliasMatch) { const key = aliasMatch[1]; const alias = aliasMatch[2]; const rest = aliasMatch[3]; if (key && alias) { if (rest && rest.startsWith('(')) { const nestedFields = rest.substring(1, rest.lastIndexOf(')')); result[key] = { __alias: alias, ...parseFieldsSpec(nestedFields) }; } else { result[key] = { __alias: alias }; } } continue; } const openParen = field.indexOf('('); if (openParen === -1) { if (field.startsWith('-')) { const fieldName = field.substring(1); result[fieldName] = false; } else { result[field] = true; } } else { const fieldName = field.substring(0, openParen); const closeParen = field.lastIndexOf(')'); if (closeParen > openParen) { const nestedFields = field.substring(openParen + 1, closeParen); result[fieldName] = parseFieldsSpec(nestedFields); } else { result[fieldName] = parseFieldsSpec(field.substring(openParen + 1)); } } } return result; } /** * Apply a field mask to an object or array, supporting aliasing with __alias. * If mask is a string, it is parsed first. * @param obj - The object or array to filter * @param mask - The field mask (object structure or string) * @returns Filtered object/array with possible key renaming */ function applyFieldMask(obj, mask) { if (!obj || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return obj.map((item) => applyFieldMask(item, mask)); let result = {}; if (mask['*'] === true) { result = { ...obj }; for (const key in mask) { if (key !== '*' && mask[key] === false) { delete result[key]; } else if (key !== '*' && typeof mask[key] === 'object' && mask[key] !== null) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; const alias = mask[key].__alias; const maskKeys = Object.keys(mask[key]).filter((k) => k !== '__alias'); let masked; if (maskKeys.length === 0) { masked = value; } else { masked = Array.isArray(value) ? value.map((item) => applyFieldMask(item, mask[key])) : applyFieldMask(value, mask[key]); } if (alias) { result[alias] = masked; delete result[key]; } else { result[key] = masked; } } } } } else { for (const key in mask) { if (mask[key] === false) continue; if (Object.prototype.hasOwnProperty.call(obj, key)) { const value = obj[key]; const maskValue = mask[key]; if (maskValue === true) { result[key] = value; } else if (typeof maskValue === 'object' && maskValue !== null) { const alias = maskValue.__alias; const maskKeys = Object.keys(maskValue).filter((k) => k !== '__alias'); let masked; if (maskKeys.length === 0) { masked = value; } else { masked = Array.isArray(value) ? value.map((item) => applyFieldMask(item, maskValue)) : applyFieldMask(value, maskValue); } if (alias) { result[alias] = masked; } else { result[key] = masked; } } } } } return result; } /** * Apply a field mask to an object or array, parsing the mask if needed. */ function applyMask(data, maskValue) { if (!maskValue) return data; let parsedMask = maskValue; if (typeof maskValue === 'string') { try { parsedMask = parseFieldsSpec(maskValue); } catch { return data; } } return applyFieldMask(data, parsedMask); } /** * Parse a relation string like model(make),bodyType,category into join descriptors. * Returns an array of { path, alias } objects for use in dynamic joins. */ function parseRelationsSpec(relationsStr, baseAlias) { if (!relationsStr) return []; const str = relationsStr || ''; const result = []; let i = 0; function parseLevel(parentPath) { const joins = []; let buffer = ''; while (i < str.length) { const char = str[i]; if (char === '(') { i++; const parent = buffer.trim(); const parentPathFull = parentPath ? `${parentPath}.${parent}` : `${baseAlias}.${parent}`; joins.push({ path: parentPathFull, alias: parent }); joins.push(...parseLevel(parent)); buffer = ''; } else if (char === ')') { if (buffer.trim()) { const field = buffer.trim(); const path = parentPath ? `${parentPath}.${field}` : `${baseAlias}.${field}`; joins.push({ path, alias: field }); } buffer = ''; i++; break; } else if (char === ',') { if (buffer.trim()) { const field = buffer.trim(); const path = parentPath ? `${parentPath}.${field}` : `${baseAlias}.${field}`; joins.push({ path, alias: field }); } buffer = ''; i++; } else { buffer += char; i++; } } if (buffer.trim()) { const field = buffer.trim(); const path = parentPath ? `${parentPath}.${field}` : `${baseAlias}.${field}`; joins.push({ path, alias: field }); } return joins; } result.push(...parseLevel('')); return result; } /** * Validate a mask object against allowed fields structure. */ function isValidMask(maskObj, allowed) { if (typeof maskObj !== 'object' || maskObj === null) return false; for (const key of Object.keys(maskObj)) { // Allow wildcard if (key === '*' || key === '__alias') { continue; } // Check if key exists in allowed fields if (!(key in allowed)) return false; // Handle nested objects if (typeof maskObj[key] === 'object' && maskObj[key] !== null) { if (!isValidMask(maskObj[key], allowed[key])) return false; } // Handle boolean values (true for inclusion, false for omission) if (typeof maskObj[key] === 'boolean') { continue; } } return true; } //# sourceMappingURL=index.js.map