import { pascalToKebab } from "./strings"; export function _jsonp(obj: any, space = 2): string { return JSON.stringify(obj, null, space); } export function isPlainObject(value: unknown): value is Record { return Object.prototype.toString.call(value) === "[object Object]"; } export function isObject(value: unknown): value is Record { return value !== null && typeof value === "object"; } export function omitKeys( obj: T, keys_: readonly K[], ): Omit> { const keys = new Set(keys_); const result = {} as Omit>; for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) { if (!keys.has(key as K)) { (result as any)[key] = value; } } return result; } export function pickKeys( obj: T, keys_: readonly K[], ): Pick> { const keys = new Set(keys_); const result = {} as Pick>; for (const [key, value] of Object.entries(obj) as [keyof T, T[keyof T]][]) { if (keys.has(key as K)) { (result as any)[key] = value; } } return result; } export function safelyParseObjectValues(obj: T): T { return Object.entries(obj).reduce((acc, [key, value]) => { try { // @ts-ignore acc[key] = JSON.parse(value); } catch (error) { // @ts-ignore acc[key] = value; } return acc; }, {} as T); } export function keepChanged(origin: T, updated: T): Partial { return Object.keys(updated).reduce( (acc, key) => { if (updated[key] !== origin[key]) { acc[key] = updated[key]; } return acc; }, {} as Partial, ); } export function objectKeysPascalToKebab(obj: any, ignoreKeys: string[] = []): any { if (obj === null || typeof obj !== "object") { return obj; } if (Array.isArray(obj)) { return obj.map((item) => objectKeysPascalToKebab(item, ignoreKeys)); } return Object.keys(obj).reduce( (acc, key) => { const kebabKey = ignoreKeys.includes(key) ? key : pascalToKebab(key); acc[kebabKey] = objectKeysPascalToKebab(obj[key], ignoreKeys); return acc; }, {} as Record, ); } export function filterKeys( obj: Object, keysToFilter: string[], ): Object { const result = {} as Object; for (const key in obj) { const shouldFilter = keysToFilter.some((filterKey) => key.includes(filterKey)); if (!shouldFilter) { if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { result[key] = filterKeys(obj[key], keysToFilter); } else { result[key] = obj[key]; } } } return result; } export function transformObject, U>( object: T, transform: (value: T[keyof T], key: keyof T) => U | undefined, ): { [K in keyof T]: U } { const result = {} as { [K in keyof T]: U }; for (const [key, value] of Object.entries(object) as [keyof T, T[keyof T]][]) { const t = transform(value, key); if (typeof t !== "undefined") { result[key] = t; } } return result; } export const objectTransform = transformObject; export function objectEach, U>( object: T, each: (value: T[keyof T], key: keyof T) => U, ): void { Object.entries(object).forEach( ([key, value]) => { each(value, key); }, {} as { [K in keyof T]: U }, ); } /** * Deep merge two objects. * @param target * @param ...sources */ export function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); } export function getFullPathKeys(obj: any, parentPath: string = ""): string[] { let keys: string[] = []; for (const key in obj) { const fullPath = parentPath ? `${parentPath}.${key}` : key; keys.push(fullPath); if (typeof obj[key] === "object" && obj[key] !== null) { keys = keys.concat(getFullPathKeys(obj[key], fullPath)); } } return keys; } export function flattenObject(obj: any, parentKey = "", result: any = {}): any { for (const key in obj) { if (key in obj) { const newKey = parentKey ? `${parentKey}.${key}` : key; if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) { flattenObject(obj[key], newKey, result); } else if (Array.isArray(obj[key])) { obj[key].forEach((item, index) => { const arrayKey = `${newKey}.${index}`; if (typeof item === "object" && item !== null) { flattenObject(item, arrayKey, result); } else { result[arrayKey] = item; } }); } else { result[newKey] = obj[key]; } } } return result; } export function objectDepth(object: object): number { let level = 1; for (const key in object) { if (typeof object[key] === "object") { const depth = objectDepth(object[key]) + 1; level = Math.max(depth, level); } } return level; } export function limitObjectDepth(obj: T, maxDepth: number): T { function _limit(current: any, depth: number): any { if (isPlainObject(current)) { if (depth > maxDepth) { return undefined; } const result: any = {}; for (const key in current) { if (Object.prototype.hasOwnProperty.call(current, key)) { result[key] = _limit(current[key], depth + 1); } } return result; } if (Array.isArray(current)) { // Arrays themselves are not limited, but their object elements are return current.map((item) => _limit(item, depth)); } // Primitives are always returned, regardless of depth return current; } return _limit(obj, 1); } export function objectCleanEmpty(obj: Obj): Obj { if (!obj) return obj; return Object.entries(obj).reduce((acc, [key, value]) => { if (value && Array.isArray(value) && value.some((v) => typeof v === "object")) { const nested = value.map(objectCleanEmpty); if (nested.length > 0) { acc[key] = nested; } } else if (value && typeof value === "object" && !Array.isArray(value)) { const nested = objectCleanEmpty(value); if (Object.keys(nested).length > 0) { acc[key] = nested; } } else if (value !== "" && value !== null && value !== undefined) { acc[key] = value; } return acc; }, {} as any); } /** * Lodash's merge implementation caused issues in Next.js environments * From: https://thescottyjam.github.io/snap.js/#!/nolodash/merge * NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge. * @param object * @param sources */ export function mergeObject(object, ...sources) { for (const source of sources) { for (const [key, value] of Object.entries(source)) { if (value === undefined) { continue; } // These checks are a week attempt at mimicking the various edge-case behaviors // that Lodash's `_.merge()` exhibits. Feel free to simplify and // remove checks that you don't need. if (!isPlainObject(value) && !Array.isArray(value)) { object[key] = value; } else if (Array.isArray(value) && !Array.isArray(object[key])) { object[key] = value; } else if (!isObject(object[key])) { object[key] = value; } else { mergeObject(object[key], value); } } } return object; } /** * Lodash's mergeWith implementation caused issues in Next.js environments * From: https://thescottyjam.github.io/snap.js/#!/nolodash/mergeWith * NOTE: This mutates `object`. It also may mutate anything that gets attached to `object` during the merge. * @param object * @param sources * @param customizer */ export function mergeObjectWith(object, source, customizer) { for (const [key, value] of Object.entries(source)) { const mergedValue = customizer(object[key], value, key, object, source); if (mergedValue !== undefined) { object[key] = mergedValue; continue; } // Otherwise, fall back to default behavior if (value === undefined) { continue; } // These checks are a week attempt at mimicking the various edge-case behaviors // that Lodash's `_.merge()` exhibits. Feel free to simplify and // remove checks that you don't need. if (!isPlainObject(value) && !Array.isArray(value)) { object[key] = value; } else if (Array.isArray(value) && !Array.isArray(object[key])) { object[key] = value; } else if (!isObject(object[key])) { object[key] = value; } else { mergeObjectWith(object[key], value, customizer); } } return object; } export function isEqual(value1: any, value2: any): boolean { // Each type corresponds to a particular comparison algorithm const getType = (value: any) => { if (value !== Object(value)) return "primitive"; if (Array.isArray(value)) return "array"; if (value instanceof Map) return "map"; if (value != null && [null, Object.prototype].includes(Object.getPrototypeOf(value))) return "plainObject"; if (value instanceof Function) return "function"; throw new Error( `deeply comparing an instance of type ${value1.constructor?.name} is not supported.`, ); }; const type = getType(value1); if (type !== getType(value2)) { return false; } if (type === "primitive") { return value1 === value2 || (Number.isNaN(value1) && Number.isNaN(value2)); } else if (type === "array") { return ( value1.length === value2.length && value1.every((iterValue: any, i: number) => isEqual(iterValue, value2[i])) ); } else if (type === "map") { // In this particular implementation, map keys are not // being deeply compared, only map values. return ( value1.size === value2.size && [...value1].every(([iterKey, iterValue]) => { return value2.has(iterKey) && isEqual(iterValue, value2.get(iterKey)); }) ); } else if (type === "plainObject") { const value1AsMap = new Map(Object.entries(value1)); const value2AsMap = new Map(Object.entries(value2)); return ( value1AsMap.size === value2AsMap.size && [...value1AsMap].every(([iterKey, iterValue]) => { return value2AsMap.has(iterKey) && isEqual(iterValue, value2AsMap.get(iterKey)); }) ); } else if (type === "function") { // just check signature return value1.toString() === value2.toString(); } else { throw new Error("Unreachable"); } } export function getPath( object: object, _path: string | (string | number)[], defaultValue = undefined, ): any { const path = typeof _path === "string" ? _path.split(/[.\[\]\"]+/).filter((x) => x) : _path; if (path.length === 0) { return object; } try { const [head, ...tail] = path; if (!head || !(head in object)) { return defaultValue; } return getPath(object[head], tail, defaultValue); } catch (error) { if (typeof defaultValue !== "undefined") { return defaultValue; } throw new Error(`Invalid path: ${path.join(".")}`); } } export function setPath(object: object, _path: string | (string | number)[], value: any) { let path = _path; // Optional string-path support. // You can remove this `if` block if you don't need it. if (typeof path === "string") { const isQuoted = (str) => str[0] === '"' && str.at(-1) === '"'; path = path .split(/[.\[\]]+/) .filter((x) => x) .map((x) => (!Number.isNaN(Number(x)) ? Number(x) : x)) .map((x) => (typeof x === "string" && isQuoted(x) ? x.slice(1, -1) : x)); } if (path.length === 0) { throw new Error("The path must have at least one entry in it"); } const [head, ...tail] = path as any; if (tail.length === 0) { object[head] = value; return object; } if (!(head in object)) { object[head] = typeof tail[0] === "number" ? [] : {}; } setPath(object[head], tail, value); return object; } export function objectToJsLiteral(value: object, indent: number = 0, _level: number = 0): string { const nl = indent ? "\n" : ""; const pad = (lvl: number) => (indent ? " ".repeat(indent * lvl) : ""); const openPad = pad(_level + 1); const closePad = pad(_level); // primitives if (value === null) return "null"; if (value === undefined) return "undefined"; const t = typeof value; if (t === "string") return JSON.stringify(value); // handles escapes if (t === "number" || t === "boolean") return String(value); // arrays if (Array.isArray(value)) { const out = value .map((v) => objectToJsLiteral(v, indent, _level + 1)) .join(", " + (indent ? nl + openPad : "")); return ( "[" + (indent && value.length ? nl + openPad : "") + out + (indent && value.length ? nl + closePad : "") + "]" ); } // objects if (t === "object") { const entries = Object.entries(value).map(([k, v]) => { const idOk = /^[A-Za-z_$][\w$]*$/.test(k); // valid identifier? const key = idOk ? k : JSON.stringify(k); // quote if needed return key + ": " + objectToJsLiteral(v, indent, _level + 1); }); const out = entries.join(", " + (indent ? nl + openPad : "")); return ( "{" + (indent && entries.length ? nl + openPad : "") + out + (indent && entries.length ? nl + closePad : "") + "}" ); } throw new TypeError(`Unsupported data type: ${t}`); } // lodash-es compatible `pick` with perfect type inference export function pick(obj: T, keys: K[]): Pick { return keys.reduce( (acc, key) => { if (key in obj) { acc[key] = obj[key]; } return acc; }, {} as Pick, ); } export function deepFreeze(object: T): T { if (Object.isFrozen(object)) return object; // Retrieve the property names defined on object const propNames = Reflect.ownKeys(object); // Freeze properties before freezing self for (const name of propNames) { const value = object[name]; if ((value && typeof value === "object") || typeof value === "function") { deepFreeze(value); } } return Object.freeze(object); }