dbl-utils
Version:
Utilities for dbl, adba and others projects
221 lines (202 loc) • 7.39 kB
text/typescript
type MutationFunction = (key: string, value: any, data: any) => any;
interface MergeOptions {
mutation: MutationFunction;
ommit?: string[];
data?: any;
}
interface ConfigOptions {
fix?: (target: any, source: any) => any;
}
interface TransformOptions {
beforeFunc?: (context: Context) => any;
duringFunc?: (context: Context) => any;
afterFunc?: (context: Context) => any;
nonObjectFunc?: (context: Context) => any;
filter?: string | string[] | ((context: Context) => boolean);
key?: string;
root?: any;
}
interface Context {
input: any;
key: string | null;
parentObj: any;
parentKey: string | null;
path: string;
}
let useConfig: ConfigOptions = {};
/**
* Check if an item is a simple object.
* @param item - The item to check.
* @returns True if the item is an object, false otherwise.
*/
function isObject(item: any): boolean {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* Deeply merge objects into the target object.
* @param target - The target object where to merge.
* @param sources - The sources to be merged into the target.
* @returns The merged target object.
*/
function effectiveDeepMerge(target: any, ...sources: any[]): any {
if (!sources.length) return target;
const source = sources.shift();
let fixValue;
if (typeof useConfig.fix === 'function') {
fixValue = useConfig.fix(target, source);
}
if (fixValue !== undefined) target = fixValue;
else if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
effectiveDeepMerge(target[key], source[key]);
} else if (typeof source[key] !== 'undefined') {
Object.assign(target, { [key]: source[key] });
}
}
}
return effectiveDeepMerge(target, ...sources);
}
/**
* Execute a mutation function on the object and merge results.
* @param target - The target object to be merged with mutation.
* @param options - Options for mutation, ommit keys and additional data.
* @param parentKey - Internal tracking key for recursion.
* @returns The mutated and merged target object.
*
* @example
* ```ts
* await mergeWithMutation({ a: { b: 1 } }, { mutation: () => ({ c: 2 }) });
* // => { a: { b: 1, c: 2 } }
* ```
*/
export async function mergeWithMutation(
target: any,
{ mutation, ommit = [], data }: MergeOptions,
parentKey = ''
): Promise<any> {
for (const key in target) {
if (!ommit.includes(key) && typeof target[key] === 'object') {
const keyFixed = Array.isArray(target) ? parentKey + '.' + key : key;
const mergeResult = await mutation(keyFixed, target[key], data);
if (mergeResult) effectiveDeepMerge(target[key], mergeResult);
await mergeWithMutation(target[key], { mutation, ommit, data }, key);
}
}
return target;
}
/**
* Deep merge multiple objects.
* @param target - The target object where other objects will be merged.
* @param sources - Other objects to be merged into the target.
* @returns The merged object.
*
* @example
* ```ts
* deepMerge({}, { a: 1 }, { b: 2 }); // { a: 1, b: 2 }
* ```
*/
export function deepMerge(target: any, ...sources: any[]): any {
const mergedObject = effectiveDeepMerge(target, ...sources);
useConfig = {};
return mergedObject;
}
/**
* Configure deep merge behavior. Useful for setting a custom
* fixer function before calling {@link deepMerge}.
*
* @param config - Configuration options.
*
* @example
* ```ts
* deepMerge.setConfig({ fix: () => ({ fixed: true }) });
* deepMerge({}, { a: 1 }); // { fixed: true }
* ```
*/
deepMerge.setConfig = (config: ConfigOptions) => {
useConfig = config;
};
/**
* Transforms and flattens a JSON object.
*
* @param json - The JSON object to be transformed.
* @param options - Options for the mutation functions.
* @returns An array with the modified object and the flattened object.
*
* @example
* ```ts
* transformJson({ a: { b: 1 } }, { filter: 'a' });
* // => [{ a: { b: 1 } }, { a: { b: 1 } }]
* ```
*/
export function transformJson(json: any, options: TransformOptions = {}): [any, any] {
const objCopy = JSON.parse(JSON.stringify(json));
const flattenObj: Record<string, any> = {};
const recursive = (
obj: any,
key: string | null = null,
parentObj: any = null,
parentKey: string | null = null,
path: string = ''
) => {
if (typeof options.beforeFunc === 'function') {
const mutation = options.beforeFunc({ input: obj, key, parentObj, parentKey, path });
if (mutation && typeof mutation === 'object') {
if (mutation.replace) parentObj[key as string] = mutation.replace;
else if (mutation.merge) deepMerge(obj, mutation.merge);
else if (mutation.delete) delete parentObj[key as string];
}
}
for (const innerKey in obj) {
const value = obj[innerKey];
const newPath = path ? `${path}.${innerKey}` : innerKey;
if (typeof options.filter === 'string' && innerKey === options.filter) {
flattenObj[newPath] = value;
continue;
} else if (Array.isArray(options.filter) && options.filter.includes(innerKey)) {
flattenObj[newPath] = value;
continue;
} else if (
typeof options.filter === 'function' &&
!options.filter({ input: value, key: innerKey, parentObj: obj, parentKey: key, path: newPath })
) {
flattenObj[newPath] = value;
continue;
}
if (typeof value === 'object' && value !== null) {
if (typeof options.duringFunc === 'function') {
const mutation = options.duringFunc({ input: value, key: innerKey, parentObj: obj, parentKey: key, path: newPath });
if (mutation && typeof mutation === 'object') {
if (mutation.replace) obj[innerKey] = mutation.replace;
else if (mutation.merge) obj[innerKey] = deepMerge(value, mutation.merge);
else if (mutation.delete) { delete obj[innerKey]; continue; }
}
}
recursive(value, innerKey, obj, key, newPath);
} else {
if (options.nonObjectFunc) {
const mutation = options.nonObjectFunc({ input: value, key: innerKey, parentObj: obj, parentKey: key, path: newPath });
if (mutation && typeof mutation === 'object') {
if (mutation.replace) obj[innerKey] = mutation.replace;
else if (mutation.merge) obj[innerKey] += mutation.merge;
else if (mutation.delete) { delete obj[innerKey]; continue; }
}
}
flattenObj[newPath] = value;
}
}
if (typeof options.afterFunc === 'function') {
const mutation = options.afterFunc({ input: obj, key, parentObj, parentKey, path });
if (mutation && typeof mutation === 'object') {
if (mutation.replace) parentObj[key as string] = mutation.replace;
else if (mutation.merge) deepMerge(obj, mutation.merge);
else if (mutation.delete) delete parentObj[key as string];
}
}
};
const rootKey = options.key || '_root';
const rootObj: any = options.root || {};
recursive(objCopy, rootKey, rootObj);
return [objCopy, rootObj[rootKey]];
}