dbl-utils
Version:
Utilities for dbl, adba and others projects
298 lines (297 loc) • 11.7 kB
JavaScript
;
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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.deserialize = exports.serialize = exports.addFormatters = void 0;
exports.flatten = flatten;
exports.unflatten = unflatten;
exports.formatAs = formatAs;
const object_mutation_1 = require("./object-mutation");
const uuid_1 = require("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 }
* ```
*/
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] }
* ```
*/
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).
*/
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.
*/
const addFormatters = (extraFormatters) => {
Object.assign(FORMATTERS, extraFormatters);
return true;
};
exports.addFormatters = addFormatters;
/**
* 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: '...' }
* ```
*/
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
? (0, object_mutation_1.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] || (0, uuid_1.v4)();
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;
});
exports.serialize = serialize;
/**
* 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 } }]
* ```
*/
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
? (0, object_mutation_1.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;
});
exports.deserialize = deserialize;