key-hierarchy
Version:
A tiny TypeScript library for managing key hierarchies. The perfect companion for TanStack Query.
191 lines (186 loc) • 6.94 kB
JavaScript
//#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