UNPKG

key-hierarchy

Version:

A tiny TypeScript library for managing key hierarchies. The perfect companion for TanStack Query.

191 lines (186 loc) 6.94 kB
//#region src/runtime/utils.ts function deepFreeze(value) { if (value === void 0 || value === null) return value; Object.freeze(value); Object.getOwnPropertyNames(value).forEach((prop) => { if (value[prop] !== null && (typeof value[prop] === "object" || typeof value[prop] === "function") && !Object.isFrozen(value[prop])) deepFreeze(value[prop]); }); return value; } function createClone(value) { if (typeof value === "function" || typeof value === "symbol") return value; return structuredClone(value); } //#endregion //#region src/types.ts const DYNAMIC_SEGMENT = Symbol("DynamicSegment"); const DYNAMIC_EXTENDED_SEGMENT = Symbol("DynamicExtendedSegment"); //#endregion //#region src/runtime/precompute.ts function precomputeHierarchy(path, currentConfig, options) { const result = {}; const keys = [...Object.keys(currentConfig), ...Object.getOwnPropertySymbols(currentConfig)].filter((key) => key !== DYNAMIC_SEGMENT && key !== DYNAMIC_EXTENDED_SEGMENT); for (const key of keys) { const value = currentConfig[key]; const currentPath = [...path, key]; if (typeof value === "boolean") result[key] = options.freeze ? deepFreeze(currentPath) : [...currentPath]; else if (typeof value === "object" && value !== null) if (DYNAMIC_EXTENDED_SEGMENT in value) { const extendedConfig = { ...value }; result[key] = (arg) => { const argPath = options.freeze ? createClone(arg) : arg; const functionPath = [...path, [key, argPath]]; const nested = precomputeHierarchy(functionPath, extendedConfig, options); nested.__key = options.freeze ? deepFreeze(functionPath) : functionPath; return nested; }; } else if (DYNAMIC_SEGMENT in value) result[key] = (arg) => { const argPath = options.freeze ? createClone(arg) : arg; const functionPath = [...path, [key, argPath]]; return options.freeze ? deepFreeze(functionPath) : functionPath; }; else { const nested = precomputeHierarchy(currentPath, value, options); nested.__key = options.freeze ? deepFreeze(currentPath) : [...currentPath]; result[key] = nested; } } return result; } //#endregion //#region src/runtime/proxy.ts function createProxy(path, currentConfig, options) { return new Proxy({}, { get(_, prop) { if (prop === "__key") { if (options.freeze) return deepFreeze([...path]); return [...path]; } const value = currentConfig[prop]; if (typeof value === "object" && value !== null && DYNAMIC_EXTENDED_SEGMENT in value) return (arg) => { const argPath = options.freeze ? createClone(arg) : arg; return createProxy([...path, [prop, argPath]], { ...value }, options); }; if (typeof value === "object" && value !== null && DYNAMIC_SEGMENT in value) return (arg) => { const argPath = options.freeze ? createClone(arg) : arg; const functionPath = [...path, [prop, argPath]]; if (options.freeze) return deepFreeze(functionPath); return functionPath; }; if (typeof value === "object" && value !== null) return createProxy([...path, prop], value, options); if (options.freeze) return deepFreeze([...path, prop]); return [...path, prop]; } }); } //#endregion //#region src/index.ts function resolveConfigOrBuilder(configOrBuilder) { return typeof configOrBuilder === "function" ? configOrBuilder(dynamicHelper) : configOrBuilder; } /** * Defines a key hierarchy based on the provided configuration and options. * @remarks **Note:** This function uses {@link Proxy} objects by default. See {@link KeyHierarchyOptions.method} for other options. * @param config - The declarative {@link KeyHierarchyConfig} of the key hierarchy. * @param options - The {@link KeyHierarchyOptions} for the key hierarchy. * @returns The {@link KeyHierarchy} derived from the given config and options. * @example * ```ts * const keys = defineKeyHierarchy((dynamic) => ({ * users: { * getAll: true, * create: true, * byId: dynamic<number>().extend({ * get: true, * update: true, * delete: true, * }), * }, * posts: { * byUserId: dynamic<number>(), * byMonth: dynamic<number>().extend({ * byDay: dynamic<number>(), * }), * byAuthorAndYear: dynamic<{ authorId: string, year: number }>(), * byTags: dynamic<{ tags: string[], filter?: PostFilter }>(), * } * })) * console.log(keys.users.getAll) // readonly ['users', 'getAll'] * console.log(keys.users.byId(42).update) // readonly ['users', ['byId', 42], 'update'] * console.log(keys.users.byId(42).__key) // readonly ['users', ['byId', 42]] * console.log(keys.posts.byUserId(42)) // readonly ['posts', ['byUserId', 42]] * console.log(keys.posts.byMonth(3).byDay(15)) // readonly ['posts', ['byMonth', 3], ['byDay', 15]] * console.log(keys.posts.byAuthorAndYear({ authorId: 'id', year: 2023 })) // readonly ['posts', ['byAuthorAndYear', { authorId: 'id', year: 2023 }]] * ``` */ function defineKeyHierarchy(configOrBuilder, options = {}) { const resolvedOptions = { freeze: false, method: "proxy", ...options }; const resolvedConfig = defineKeyHierarchyModule(configOrBuilder); if (resolvedOptions.method === "precompute") return precomputeHierarchy([], resolvedConfig, resolvedOptions); return createProxy([], resolvedConfig, resolvedOptions); } /** * Defines a key hierarchy module. * @remarks This function is a no-op and is used for type inference only. * @param config - The declarative {@link KeyHierarchyConfig} of the key hierarchy module. * @returns The {@link KeyHierarchyConfig} of the key hierarchy module. * @example * ```ts * const userKeyModule = defineKeyHierarchyModule((dynamic) => ({ * getAll: true, * create: true, * byId: dynamic<number>().extend({ * get: true, * update: true, * delete: true, * }) * })) * * const postKeyModule = defineKeyHierarchyModule((dynamic) => ({ * byUserId: dynamic<number>(), * byMonth: dynamic<number>().extend({ * byDay: dynamic<number>(), * }) * }) * * const keys = defineKeyHierarchy({ * users: userKeyModule, * posts: postKeyModule * }) * ``` */ function defineKeyHierarchyModule(configOrBuilder) { return resolveConfigOrBuilder(configOrBuilder); } /** * Helper function to define dynamic key segments with a single argument. * @returns A dynamic key segment that can optionally be extended with a nested configuration. * @example * ```ts * defineKeyHierarchy({ * getAll: true, * byGroup: dynamic<{ groupId: string }>(), * byId: dynamic<string>().extend({ get: true, update: true }), * byMonth: dynamic<number>().extend({ * byDay: dynamic<number>(), * }), * byAuthorAndYear: dynamic<{ authorId: string, year: number }>(), * byTags: dynamic<{ tags: string[], filter?: PostFilter }>(), * }) * ``` */ function dynamicHelper() { return { [DYNAMIC_SEGMENT]: void 0, extend(config) { return { [DYNAMIC_EXTENDED_SEGMENT]: void 0, ...config }; } }; } //#endregion export { defineKeyHierarchy, defineKeyHierarchyModule }; //# sourceMappingURL=index.js.map