UNPKG

dbl-utils

Version:

Utilities for dbl, adba and others projects

289 lines (288 loc) 11.4 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { deepMerge } from "./object-mutation"; import { v4 as uuidv4 } from "uuid"; // Formatter functions by type const FORMATTERS = { string: (v) => v, number: (v) => Number(v), boolean: (v) => v.toLowerCase() === "true", }; /** * Flattens an object into a single-level object with delimited keys. * * * @param object - The object to flatten * @param options - Configuration options for flattening * @returns A single-level object with flattened keys * @throws Will throw an error if the input is not an object * * @example * ```ts * const result = flatten({ a: { b: 1 } }); * // result => { 'a.b': 1 } * ``` */ export function flatten(object, options = {}) { if (options.delimiter === undefined) options.delimiter = "."; if (options.ommit === undefined) options.ommit = []; if (options.ommitArrays === undefined) options.ommitArrays = false; const newObject = {}; if (typeof object !== "object" || object === null) throw new Error("Must be object"); /** * Recursively flattens the object. * * @param element - Current element being processed * @param key - Current key in the object * @param index - Current index being constructed */ const recursive = (element, key, index) => { var _a; const newIndex = index ? [index, key].join(options.delimiter) : key; if (typeof element === "object" && element !== null) { const ommitedByFn = typeof options.ommitFn === "function" && !!options.ommitFn(key, element, newIndex); if (Array.isArray(element)) { if (options.ommitArrays || ommitedByFn) { newObject[newIndex] = element; } else { element.forEach((e, i) => recursive(e, i.toString(), newIndex)); } } else { if ((key && ((_a = options.ommit) === null || _a === void 0 ? void 0 : _a.includes(key))) || ommitedByFn) { newObject[newIndex] = element; } else { Object.entries(element).forEach(([i, e]) => recursive(e, i, newIndex)); } } } else { newObject[newIndex] = element; } }; recursive(object, options.prefix); return newObject; } /** * Reconstructs a nested object from a flattened one using a delimiter. * Numeric keys are automatically treated as array indices so arrays * are recreated correctly. * * @param object - The flattened object. * @param delimiter - The string delimiter used to split keys. * @returns The nested (unflattened) object. * * @example * ```ts * const obj = { 'a.0': 1, 'a.1': 2 }; * unflatten(obj); * // => { a: [1, 2] } * ``` */ export function unflatten(object, delimiter = ".") { if (typeof object !== "object" || object === null) throw new Error("Input must be an object"); const result = {}; const isInteger = (key) => /^\d+$/.test(key); for (const flatKey of Object.keys(object)) { const value = object[flatKey]; const keys = flatKey.split(delimiter); let current = result; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const nextKey = keys[i + 1]; const isLast = i === keys.length - 1; const keyIsIndex = isInteger(key); const nextIsIndex = isInteger(nextKey); // Convert key if it should be array index const realKey = keyIsIndex ? parseInt(key, 10) : key; // Handle last key (set value) if (isLast) { current[realKey] = value; } else { // Create container if needed if (current[realKey] === undefined) { current[realKey] = nextIsIndex ? [] : {}; } // Move deeper current = current[realKey]; } } } return result; } /** * Converts a string into its original type (string, number, boolean). */ export function formatAs(value, type) { var _a; const format = (_a = FORMATTERS[type]) !== null && _a !== void 0 ? _a : FORMATTERS.string; return format(value); } /** * Adds extra formatter functions to the internal registry. */ export const addFormatters = (extraFormatters) => { Object.assign(FORMATTERS, extraFormatters); return true; }; /** * Serializes an object or array of objects into a flat list of field entries. * You can customize the process using `before` (pre-processing per item) * and `after` (post-processing per field). * * @example * ```ts * const data = { a: 1, b: { c: 2 } }; * const entries = await serialize(data); * // entries[0] => { path: 'a', value: '1', type: 'number', uuid: '...' } * ``` */ export const serialize = (data_1, ...args_1) => __awaiter(void 0, [data_1, ...args_1], void 0, function* (data, opts = {}) { // Default options opts = Object.assign({ metaIdentifier: "$", groupKey: "uuid", }, opts); const serialized = (yield Promise.all([data] .flat() .map((rawLineContent, i) => __awaiter(void 0, void 0, void 0, function* () { var _a; const lineContent = opts.before ? deepMerge(rawLineContent, (_a = (yield opts.before(rawLineContent, i))) !== null && _a !== void 0 ? _a : {}) : rawLineContent; // Separate metadata fields const metaValues = {}; Object.keys(lineContent).forEach((k) => { if (!k.startsWith(opts.metaIdentifier)) return; metaValues[k.slice(opts.metaIdentifier.length)] = lineContent[k]; delete lineContent[k]; }); // Generate a groupKey if none is provided in metadata const groupKeyName = opts.groupKey; metaValues[groupKeyName] = metaValues[groupKeyName] || uuidv4(); const tmp = flatten(lineContent, { delimiter: "/" }); return yield Promise.all(Object.entries(tmp).map((_a) => __awaiter(void 0, [_a], void 0, function* ([path, rawValue]) { const cpMetaValues = Object.assign({}, metaValues); const value = opts.after ? yield opts.after(path, rawValue, cpMetaValues) : rawValue; const obj = [null, undefined].includes(value) ? false : Object.assign({ path, value: String(value), type: typeof value, [groupKeyName]: metaValues[groupKeyName] }, cpMetaValues); return obj; }))); })))) .flat() .filter(Boolean); return serialized; }); /** * Deserializes a flat list of entries into full objects. * Groups entries by a defined key (e.g. uuid or content_uuid) if present. * Optionally applies a `before` hook to each entry before processing. * * @example * ```ts * const objects = await deserialize(entries); * // objects => [{ a: 1, b: { c: 2 } }] * ``` */ export const deserialize = (serialized_1, ...args_1) => __awaiter(void 0, [serialized_1, ...args_1], void 0, function* (serialized, opts = {}) { var _a, _b, _c, _d, _e; var _f, _g; opts = Object.assign({ metaIdentifier: "$", groupKey: "uuid", metaObject: false, }, opts); const grouped = {}; for (const rawEntry of serialized) { const entry = opts.before ? deepMerge(rawEntry, (_a = (yield opts.before(rawEntry))) !== null && _a !== void 0 ? _a : {}) : rawEntry; const { path, value, type } = entry, meta = __rest(entry, ["path", "value", "type"]); const formattedValue = formatAs(value, type); const groupId = (_b = meta[opts.groupKey]) !== null && _b !== void 0 ? _b : "__ungrouped__"; grouped[groupId] = (_c = grouped[groupId]) !== null && _c !== void 0 ? _c : {}; const groupMeta = opts.metaObject ? ((_d = (_f = grouped[groupId])[_g = opts.metaIdentifier]) !== null && _d !== void 0 ? _d : (_f[_g] = {})) : grouped[groupId]; // Always store the groupKey const idKey = opts.metaObject ? opts.groupKey : opts.metaIdentifier + opts.groupKey; groupMeta[idKey] = (_e = groupMeta[idKey]) !== null && _e !== void 0 ? _e : groupId; // Use Set to accumulate metadata Object.entries(meta).forEach(([key, rawVal]) => { let val; switch (true) { case rawVal === null: val = undefined; break; case typeof rawVal === "object" && rawVal instanceof Date: val = rawVal.toISOString(); break; case typeof rawVal === "object": val = String(rawVal); break; default: val = rawVal; break; } const metaKey = opts.metaObject ? key : opts.metaIdentifier + key; const current = groupMeta[metaKey]; if (!current) groupMeta[metaKey] = new Set([val]); else if (current instanceof Set) current.add(val); else groupMeta[metaKey] = new Set([current, val]); }); grouped[groupId][path] = formattedValue; } // Final cleanup: convert Set → single value, array or remove for (const group of Object.values(grouped)) { const container = opts.metaObject ? group[opts.metaIdentifier] : group; for (const [key, value] of Object.entries(container)) { if (value instanceof Set) { const arr = Array.from(value).filter(Boolean); if (arr.length === 0) delete container[key]; else if (arr.length === 1) container[key] = arr[0]; else container[key] = arr; } } } const deserialized = Object.values(grouped).map((flat) => unflatten(flat, "/")); return deserialized; });