UNPKG

dbl-utils

Version:

Utilities for dbl, adba and others projects

378 lines (377 loc) 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = resolveRefs; const object_mutation_1 = require("./object-mutation"); const flat_1 = require("./flat"); /** * Resolve references within objects and arrays using advanced reference resolution. * * This powerful utility enables dynamic object composition through multiple reference types: * global references, relative references, string interpolation, and template inheritance. * Perfect for configuration management, dynamic content generation, and object templating. * * @param object - The source object or array containing references to resolve. * References use `$path/to/value` syntax for global lookups or * `$./path` for relative references. * @param schema - Schema object used as the base for global reference lookups. * If not provided, uses a deep clone of the source object. * This is where `$path/to/value` references are resolved against. * @param rules - Custom rule definitions for advanced reference processing. * Each rule maps a reference key to a tuple `[taskName, ...args]`. * Built-in tasks: 'iterate', 'join', 'ignore', 'if'. * @param extraTasks - Additional custom task functions that can be used in rules. * Each task receives the resolved arguments and should return a value. * @returns A new object with all references resolved and templates expanded. * * ## Reference Types * * ### 1. Global References * Reference values anywhere in the schema using absolute paths: * - `"$path/to/value"` - Direct reference to a value * - `"${path/to/value}"` - String interpolation (embeds value in string) * - `"prefix-${path/to/value}-suffix"` - Mixed string with interpolated values * * ### 2. Object Template References * Extend and modify referenced objects: * - `{ ref: "$path/to/template", newProp: "value" }` - Template inheritance * - `{ ref: "path/to/template", existingProp: "override" }` - Property override * * ### 3. Relative References * Reference values within the current object context: * - `"$./property"` - Direct relative reference * - `"${./nested/property}"` - Relative string interpolation * - `"${./path}-${./other}"` - Multiple relative references in strings * * ### 4. Template System with Deferred Resolution * Use the special `"."` key for references resolved after template merging: * - `"."` object contains references processed after base template + overrides * - Enables templates to reference their final merged state * - Supports recursive resolution until stable state is reached * * ## Advanced Features * * ### Custom Rules and Tasks * Define reusable logic for complex transformations: * ```ts * const rules = { * "$users": ["iterate", "$data/users", "currentUser"], * "$joinNames": ["join", "$people", ", ", "$extraPeople"] * }; * * const extraTasks = { * uppercase: (value: string) => value.toUpperCase(), * calculate: (a: number, b: number) => a + b * }; * ``` * * ### Built-in Tasks * - `iterate(array, itemName)` - Process array items with template * - `join(array, separator, ...extra)` - Join arrays with separator * - `ignore(path, fallback)` - Safe lookup with fallback * - `if(condition, trueValue, falseValue)` - Conditional resolution * * @example * Basic global references: * ```ts * const config = { api: { url: "https://api.com", key: "abc123" } }; * const settings = { endpoint: "$api/url", auth: "Bearer ${api/key}" }; * * const result = resolveRefs(settings, config); * // { endpoint: "https://api.com", auth: "Bearer abc123" } * ``` * * @example * Template inheritance with property overrides: * ```ts * const data = { * defaultUser: { name: "Guest", role: "viewer", active: false }, * admin: { ref: "$defaultUser", name: "Admin", role: "admin", active: true }, * viewer: { ref: "$defaultUser", name: "John" } * }; * * const result = resolveRefs(data); * // result.admin = { name: "Admin", role: "admin", active: true } * // result.viewer = { name: "John", role: "viewer", active: false } * ``` * * @example * Relative references with template system: * ```ts * const data = { * user1: { ref: "$templates/user", firstName: "Alice", lastName: "Smith" }, * user2: { ref: "$templates/user", firstName: "Bob", lastName: "Jones" }, * templates: { * user: { * firstName: "", * lastName: "", * email: "", * ".": { * fullName: "${./firstName} ${./lastName}", * email: "${./firstName}.${./lastName}@company.com", * profile: { * displayName: "$./fullName", * username: "$./firstName" * } * } * } * } * }; * * const result = resolveRefs(data); * // result.user1.fullName = "Alice Smith" * // result.user1.email = "Alice.Smith@company.com" * // result.user1.profile.displayName = "Alice Smith" * ``` * * @example * Configuration management with mixed references: * ```ts * const config = { * env: { baseUrl: "https://api.prod.com", version: "v2" }, * services: { * auth: { ref: "$templates/service", path: "/auth", timeout: 5000 }, * users: { ref: "$templates/service", path: "/users", timeout: 3000 } * }, * templates: { * service: { * path: "/", * timeout: 30000, * retries: 3, * ".": { * url: "${env/baseUrl}/${env/version}${./path}", * config: { * endpoint: "$./url", * timeout: "$./timeout", * retries: "$./retries" * } * } * } * } * }; * * const result = resolveRefs(config); * // result.services.auth.url = "https://api.prod.com/v2/auth" * // result.services.auth.config.endpoint = "https://api.prod.com/v2/auth" * ``` * * @example * Custom rules and tasks: * ```ts * const data = { users: ["Alice", "Bob"], items: [1, 2, 3] }; * const template = { * userList: "$buildUserList", * summary: "$joinItems" * }; * * const rules = { * "$buildUserList": ["iterate", "$users", "user"], * "$joinItems": ["join", "$items", " + "] * }; * * const extraTasks = { * format: (template: string, ...values: any[]) => * template.replace(/{(\d+)}/g, (_, i) => values[i]) * }; * * const result = resolveRefs(template, data, rules, extraTasks); * ``` * * ## Best Practices * * 1. **Schema Organization**: Keep templates in a dedicated section (e.g., `templates/`) * 2. **Reference Clarity**: Use descriptive paths like `config/database/url` vs `c/d/u` * 3. **Template Design**: Use relative references in `"."` for dynamic template properties * 4. **Performance**: Avoid deep circular references that could cause infinite loops * 5. **Type Safety**: Consider using TypeScript interfaces for your schema structure * * ## Error Handling * * - Invalid references return the original reference string * - Missing paths in schema return `undefined` * - Circular references are prevented through iteration limits * - Type mismatches in string interpolation are converted to strings */ function resolveRefs(object, schema = JSON.parse(JSON.stringify(object)), rules = {}, extraTasks = {}) { const processRules = (key) => { if (!rules[key]) return undefined; const tasks = Object.assign({ iterate: (keyData, itemName) => { const data = loop(keyData); if (!Array.isArray(data)) return []; const itemFound = key .substring(1) .split("/") .reduce((obj, key) => obj[key], schema); const builded = data.map((item) => { schema[itemName] = item; return loop(itemFound); }); delete schema[itemName]; return builded; }, join: (first, join, ...next) => { const f = loop(first); if (Array.isArray(f)) return f.join(join); return [f, ...loop([join, next])].join(""); }, ignore: (d, def) => d .substring(1) .split("/") .reduce((obj, key) => obj[key], schema) || def, if: (d, found, def) => (d .substring(1) .split("/") .reduce((obj, key) => obj[key], schema) && loop(found)) || def }, extraTasks); const [task, ...attrs] = rules[key]; return tasks[task](...attrs); }; const loop = (item, currentContext, skipRelativeRefs = false) => { if (item === null) return item; if (Array.isArray(item)) { return item.map((a) => loop(a, currentContext, skipRelativeRefs)).flat(); } else if (typeof item === "object") { let toReturn = {}; if (item.ref) { const ref = item.ref; delete item.ref; const unflattened = (0, flat_1.unflatten)(item, "/"); // Skip relative references when getting the template const refObj = loop(ref, currentContext, true); const modify = loop(unflattened, currentContext, true); // First merge the reference object with modifications, excluding "." for now let baseObject = {}; let relativeRefs = null; if (typeof refObj === "object" && refObj !== null && refObj["."]) { // Extract relative references from the template relativeRefs = refObj["."]; baseObject = Object.assign({}, refObj); delete baseObject["."]; } else { baseObject = refObj; } // Merge base object with modifications let mergedObject; if (typeof baseObject === "string") { mergedObject = (0, object_mutation_1.deepMerge)({ ref: baseObject }, modify); } else { mergedObject = (0, object_mutation_1.deepMerge)({}, baseObject, modify); } // Now process relative references if they exist, using the merged object as context if (relativeRefs) { let processedRelativeRefs = loop(relativeRefs, mergedObject); // Keep processing relative references until no more changes are made let hasChanges = true; while (hasChanges) { const currentContext = (0, object_mutation_1.deepMerge)(mergedObject, processedRelativeRefs); const newProcessed = loop(processedRelativeRefs, currentContext); hasChanges = JSON.stringify(newProcessed) !== JSON.stringify(processedRelativeRefs); processedRelativeRefs = newProcessed; } toReturn = (0, object_mutation_1.deepMerge)(mergedObject, processedRelativeRefs); } else { toReturn = mergedObject; } } else { Object.keys(item).forEach((i) => { if (i === "." && !skipRelativeRefs) { // El objeto "." contiene referencias relativas al objeto actual // Necesitamos procesar primero el resto del objeto para tener el contexto completo Object.keys(item).forEach((j) => { if (j !== ".") { toReturn[j] = loop(item[j], currentContext, skipRelativeRefs); } }); // Ahora procesamos las referencias relativas con el objeto ya construido const relativeRefs = loop(item[i], toReturn); toReturn = (0, object_mutation_1.deepMerge)(toReturn, relativeRefs); } else if (i !== "." || skipRelativeRefs) { // Solo procesar si no es "." o si estamos saltando referencias relativas if (!item["."] || skipRelativeRefs) { toReturn[i] = loop(item[i], currentContext, skipRelativeRefs); } } }); } return toReturn; } else if (typeof item === "string" && item[0] === "$" && item[1] !== "{") { const fixed = processRules(item); if (fixed !== undefined) return fixed; // Check for relative reference starting with $./ if (item.startsWith("$./")) { if (!currentContext) return item; // No context available let keys = item.substring(3).split("/"); // Remove $./ prefix try { let data = keys.reduce((obj, key) => obj[key], currentContext); data = JSON.parse(JSON.stringify(data)); return loop(data, currentContext, skipRelativeRefs); } catch (error) { return item; } } let keys = item.substring(1).split("/"); // Get the content of $path/to/element let data; try { data = keys.reduce((obj, key) => obj[key], schema); data = JSON.parse(JSON.stringify(data)); } catch (error) { return item; } return loop(data, currentContext, skipRelativeRefs); } else if (typeof item === "string" && item.includes("${")) { // Replace all occurrences of ${some/path} within the string const pattern = /\$\{([^}]+?)\}/g; // Matches ${something/here} let result = item; result = result.replace(pattern, (match, path) => { try { // Check for relative reference starting with ./ if (path.startsWith("./")) { if (!currentContext) return match; // No context available const keys = path.substring(2).split("/"); // Remove ./ prefix let data = keys.reduce((obj, key) => obj[key], currentContext); data = JSON.parse(JSON.stringify(data)); const processed = loop(data, currentContext, skipRelativeRefs); return typeof processed === "string" ? processed : JSON.stringify(processed); } else { // Regular global reference const keys = path.split("/"); let data = keys.reduce((obj, key) => obj[key], schema); data = JSON.parse(JSON.stringify(data)); const processed = loop(data, currentContext, skipRelativeRefs); return typeof processed === "string" ? processed : JSON.stringify(processed); } } catch (err) { return match; // If lookup fails, keep the original } }); return result; } else { return item; } }; return loop(JSON.parse(JSON.stringify(object))); }