UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

454 lines (453 loc) 13.3 kB
import { _isEmpty, _isObject } from '../is.util.js'; import { _objectEntries, SKIP } from '../types.js'; /** * Returns clone of `obj` with only `props` preserved. * Opposite of Omit. */ export function _pick(obj, props, opt = {}) { if (opt.mutate) { // Start as original object (mutable), DELETE properties that are not whitelisted for (const k of Object.keys(obj)) { if (!props.includes(k)) delete obj[k]; } return obj; } // Start as empty object, pick/add needed properties const r = {}; for (const k of props) { if (k in obj) r[k] = obj[k]; } return r; } /** * Sets all properties of an object except passed ones to `undefined`. * This is a more performant alternative to `_pick` that does picking/deleting. */ export function _pickWithUndefined(obj, props, opt = {}) { const r = opt.mutate ? obj : { ...obj }; for (const k of Object.keys(r)) { if (!props.includes(k)) { r[k] = undefined; } } return r; } /** * Returns clone of `obj` with `props` omitted. * Opposite of Pick. */ export function _omit(obj, props, opt = {}) { if (opt.mutate) { for (const k of props) { delete obj[k]; } return obj; } const r = {}; for (const k of Object.keys(obj)) { if (!props.includes(k)) r[k] = obj[k]; } return r; } /** * Sets all passed properties of an object to `undefined`. * This is a more performant alternative to `_omit` that does picking/deleting. */ export function _omitWithUndefined(obj, props, opt = {}) { const r = opt.mutate ? obj : { ...obj }; for (const k of props) { r[k] = undefined; } return r; } /** * Returns object with filtered keys from `props` array. * E.g: * _mask({...}, [ * 'account.id', * 'account.updated', * ]) */ export function _mask(obj, props, opt = {}) { const r = opt.mutate ? obj : _deepCopy(obj); for (const k of props) { _unset(r, k); } return r; } /** * Removes "falsy" values from the object. */ export function _filterFalsyValues(obj, opt = {}) { return _filterObject(obj, (_k, v) => !!v, opt); } /** * Removes values from the object that are `null` or `undefined`. */ export function _filterNullishValues(obj, opt = {}) { return _filterObject(obj, (_k, v) => v !== undefined && v !== null, opt); } /** * Removes values from the object that are `undefined`. * Only `undefined` values are removed. `null` values are kept! */ export function _filterUndefinedValues(obj, opt = {}) { return _filterObject(obj, (_k, v) => v !== undefined, opt); } export function _filterEmptyArrays(obj, opt = {}) { return _filterObject(obj, (_k, v) => !Array.isArray(v) || v.length > 0, opt); } /** * Returns clone of `obj` without properties that does not pass `predicate`. * Allows filtering by both key and value. */ export function _filterObject(obj, predicate, opt = {}) { if (opt.mutate) { for (const [k, v] of _objectEntries(obj)) { if (!predicate(k, v, obj)) { delete obj[k]; } } return obj; } const r = {}; for (const [k, v] of _objectEntries(obj)) { if (predicate(k, v, obj)) { r[k] = v; } } return r; } /** * var users = { * 'fred': { 'user': 'fred', 'age': 40 }, * 'pebbles': { 'user': 'pebbles', 'age': 1 } * } * * _mapValues(users, (_key, value) => value.age) * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) * * To skip some key-value pairs - use _mapObject instead. */ export function _mapValues(obj, mapper, opt = {}) { const map = opt.mutate ? obj : {}; for (const [k, v] of Object.entries(obj)) { map[k] = mapper(k, v, obj); } return map; } /** * _.mapKeys({ 'a': 1, 'b': 2 }, (key, value) => key + value) * // => { 'a1': 1, 'b2': 2 } * * Does not support `mutate` flag. * * To skip some key-value pairs - use _mapObject instead. */ export function _mapKeys(obj, mapper) { const map = {}; for (const [k, v] of Object.entries(obj)) { map[mapper(k, v, obj)] = v; } return map; } /** * Maps object through predicate - a function that receives (k, v, obj) * k - key * v - value * obj - whole object * * Order of arguments in the predicate is different form _mapValues / _mapKeys! * * Predicate should return a _tuple_ [0, 1], where: * 0 - key of returned object (string) * 1 - value of returned object (any) * * If predicate returns SKIP symbol - such key/value pair is ignored (filtered out). * * Non-string keys are passed via String(...) */ export function _mapObject(obj, mapper) { const map = {}; for (const [k, v] of Object.entries(obj)) { const r = mapper(k, v, obj); if (r === SKIP) continue; map[r[0]] = r[1]; } return map; } export function _findKeyByValue(obj, v) { return Object.entries(obj).find(([_, value]) => value === v)?.[0]; } export function _objectNullValuesToUndefined(obj, opt = {}) { return _mapValues(obj, (_k, v) => (v === null ? undefined : v), opt); } /** * Deep copy object (by json parse/stringify, since it has unbeatable performance+simplicity combo). */ export function _deepCopy(o, reviver) { return JSON.parse(JSON.stringify(o), reviver); } /** * Performance-optimized implementation of merging two objects * without mutating any of them. * (if you are allowed to mutate - there can be a faster implementation). * * Gives ~40% speedup with map sizes between 10 and 100k items, * compared to {...obj1, ...obj2} or Object.assign({}, obj1, obj2). * * Only use it in hot paths that are known to be performance bottlenecks, * otherwise it's not worth it (use normal object spread then). */ export function _mergeObjects(obj1, obj2) { const map = {}; for (const k of Object.keys(obj1)) map[k] = obj1[k]; for (const k of Object.keys(obj2)) map[k] = obj2[k]; return map; } /** * Returns `undefined` if it's empty (according to `_isEmpty()` specification), * otherwise returns the original object. */ export function _undefinedIfEmpty(obj) { return _isEmpty(obj) ? undefined : obj; } /** * Filters the object by removing all key-value pairs where Value is Empty (according to _isEmpty() specification). */ export function _filterEmptyValues(obj, opt = {}) { return _filterObject(obj, (_k, v) => !_isEmpty(v), opt); } /** * Recursively merges own and inherited enumerable properties of source * objects into the destination object, skipping source properties that resolve * to `undefined`. Array and plain object properties are merged recursively. * Other objects and value types are overridden by assignment. Source objects * are applied from left to right. Subsequent sources overwrite property * assignments of previous sources. * * Works as "recursive Object.assign". * * **Note:** This method mutates `object`. * * @category Object * @param target The destination object. * @param sources The source objects. * @returns Returns `object`. * @example * * var users = { * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] * }; * * var ages = { * 'data': [{ 'age': 36 }, { 'age': 40 }] * }; * * _.merge(users, ages); * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } * * Based on: https://gist.github.com/Salakar/1d7137de9cb8b704e48a */ export function _merge(target, ...sources) { for (const source of sources) { if (!_isObject(source)) continue; for (const key of Object.keys(source)) { if (_isObject(source[key])) { ; target[key] ||= {}; _merge(target[key], source[key]); } else { ; target[key] = source[key]; } } } return target; } /** * Trims all object VALUES deeply. * Doesn't touch object KEYS. * Mutates. */ export function _deepTrim(o) { if (!o) return o; if (typeof o === 'string') { return o.trim(); } if (typeof o === 'object') { for (const k of Object.keys(o)) { o[k] = _deepTrim(o[k]); } } return o; } // from: https://github.com/jonschlinkert/unset-value // mutates obj export function _unset(obj, prop) { if (!_isObject(obj)) { return; } if (obj.hasOwnProperty(prop)) { delete obj[prop]; return; } const segs = prop.split('.'); let last = segs.pop(); while (segs.length && segs[segs.length - 1].endsWith('\\')) { last = segs.pop().slice(0, -1) + '.' + last; } while (segs.length && _isObject(obj)) { const k = segs.shift(); obj = obj[k]; } if (!_isObject(obj)) return; delete obj[last]; } export function _invert(o) { const inv = {}; Object.keys(o).forEach(k => { inv[o[k]] = k; }); return inv; } export function _invertMap(m) { const inv = new Map(); m.forEach((v, k) => inv.set(v, k)); return inv; } /** * Gets the property value at path of object. * * @example * const obj = {a: 'a', b: 'b', c: { cc: 'cc' }} * _get(obj, 'a') // 'a' * _get(obj, 'c.cc') // 'cc' * _get(obj, 'c[cc]') // 'cc' * _get(obj, 'unknown.path') // undefined */ export function _get(obj = {}, path = '') { return (path .replaceAll(/\[([^\]]+)]/g, '.$1') .split('.') // eslint-disable-next-line unicorn/no-array-reduce .reduce((o, p) => o?.[p], obj)); } /** * Sets the value at path of object. If a portion of path doesn’t exist it’s created. Arrays are created for * missing index properties while objects are created for all other missing properties. * * @param obj The object to modify. * @param path The path of the property to set. * @param value The value to set. * @return Returns object. * * Based on: https://stackoverflow.com/a/54733755/4919972 */ export function _set(obj, path, value) { // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok if (!obj || Object(obj) !== obj || !path) return obj; // When obj is not an object // If not yet an array, get the keys from the string-path if (!Array.isArray(path)) { path = String(path).match(/[^.[\]]+/g) || []; } else if (!path.length) { return obj; } // eslint-disable-next-line unicorn/no-array-reduce ; path.slice(0, -1).reduce((a, c, i) => // biome-ignore lint/style/useConsistentBuiltinInstantiation: ok Object(a[c]) === a[c] // Does the key exist and is its value an object? ? // Yes: then follow that path a[c] : // No: create the key. Is the next key a potential array-index? (a[c] = // eslint-disable-next-line Math.abs(path[i + 1]) >> 0 === +path[i + 1] ? [] // Yes: assign a new array object : {}), // No: assign a new plain object obj)[path[path.length - 1]] = value; // Finally assign the value to the last key return obj; // allow chaining } /** * Checks if `path` is a direct property of `object` (not null, not undefined). * * @category Object * @param obj The object to query. * @param path The path to check. * @returns Returns `true` if `path` exists, else `false`. * @example * * var object = { 'a': { 'b': { 'c': 3 } } }; * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) }); * * _.has(object, 'a'); * // => true * * _.has(object, 'a.b.c'); * // => true * * _.has(object, ['a', 'b', 'c']); * // => true * * _.has(other, 'a'); * // => false */ export function _has(obj, path) { const v = _get(obj, path); return v !== undefined && v !== null; } /** * Does Object.freeze recursively for given object. * * Based on: https://github.com/substack/deep-freeze/blob/master/index.js */ export function _deepFreeze(o) { Object.freeze(o); Object.getOwnPropertyNames(o).forEach(prop => { if (o.hasOwnProperty(prop) && o[prop] !== null && (typeof o[prop] === 'object' || typeof o[prop] === 'function') && !Object.isFrozen(o[prop])) { _deepFreeze(o[prop]); } }); } /** * let target: T = { a: 'a', n: 1} * let source: T = { a: 'a2', b: 'b' } * * _objectAssignExact(target, source) * * Does the same as `target = source`, * except that it mutates the target to make it exactly the same as source, * while keeping the reference to the same object. * * This way it can "propagate deletions". * E.g source doesn't have the `n` property, so it'll be deleted from target. * With normal Object.assign - it'll override the keys that `source` has, but not the * "missing/deleted keys". * * To make mutation extra clear - function returns void (unlike Object.assign). */ export function _objectAssignExact(target, source) { Object.assign(target, source); for (const k of Object.keys(target)) { if (!(k in source)) { // consider setting it to undefined maybe? delete target[k]; } } }