UNPKG

topkat-utils

Version:

A comprehensive collection of TypeScript/JavaScript utility functions for common programming tasks. Includes validation, object manipulation, date handling, string formatting, and more. Zero dependencies, fully typed, and optimized for performance.

440 lines 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.revertObjectKeysAndValues = exports.createProxy = exports.entries = exports.keys = exports.mergeObjectArrays = exports.objKeys = exports.objEntries = exports.unflattenObject = exports.flattenObject = exports.mergeDeepConfigurable = exports.mergeDeepOverrideArrays = exports.mergeDeep = exports.ensureObjectProp = exports.sortObjKeyAccordingToValue = exports.objFilterUndefinedRecursive = exports.readOnlyForAll = exports.readOnlyRecursive = exports.reassignForbidden = exports.readOnly = exports.objFilterUndefined = exports.deleteByAddress = exports.filterKeys = exports.deepClone = exports.cloneObject = exports.mergeMixins = exports.objForceWriteIfNotSet = exports.objForceWritePath = exports.forcePathInObject = exports.objForceWrite = exports.findByAddressAll = exports.findByAddress = exports.has = exports.simpleObjectMaskOrSelect = void 0; const error_utils_1 = require("./error-utils"); const loop_utils_1 = require("./loop-utils"); const isset_1 = require("./isset"); const is_object_1 = require("./is-object"); const error_utils_2 = require("./error-utils"); const regexp_utils_1 = require("./regexp-utils"); /** Return an object: * * with only selected fields * * OR without masked fields */ function simpleObjectMaskOrSelect(object, maskedOrSelectedFields, mode = 'mask', deleteKeysInsteadOfReturningAnewObject = false) { const allKeys = Object.keys(object); const keysToMask = allKeys.filter(keyName => { if (mode === 'mask') return maskedOrSelectedFields.includes(keyName); else return !maskedOrSelectedFields.includes(keyName); }); if (deleteKeysInsteadOfReturningAnewObject) { keysToMask.forEach(keyNameToDelete => delete object[keyNameToDelete]); return object; } else { return allKeys.reduce((newObject, key) => { if (!keysToMask.includes(key)) newObject[key] = object[key]; return newObject; }, {}); } } exports.simpleObjectMaskOrSelect = simpleObjectMaskOrSelect; /** * check if **object OR array** has property Safely (avoid cannot read property x of null and such) * @param {Object} obj object to test against * @param {string} addr `a.b.c.0.1` will test if myObject has props a that has prop b. Work wit arrays as well (like `arr.0`) */ function has(obj, addr) { if (!(0, isset_1.isset)(obj) || typeof obj !== 'object') return; const propsArr = addr.replace(/\.?\[(\d+)\]/g, '.$1').split('.'); // replace a[3] => a.3; let objChain = obj; return propsArr.every(prop => { objChain = objChain[prop]; return (0, isset_1.isset)(objChain); }); } exports.has = has; /** Find address in an object "a.b.c" IN { a : { b : {c : 'blah' }}} RETURNS 'blah' * @param obj * @param addr accept syntax like "obj.subItem.[0].sub2" OR "obj.subItem.0.sub2" OR "obj.subItem[0].sub2" * @returns the last item of the chain OR undefined if not found */ function findByAddress(obj, addr) { if (addr.length === 0) return obj; // eslint-disable-next-line no-console if (!(0, isset_1.isset)(obj) || typeof obj !== 'object') return console.warn('Main object in `findByAddress` function is undefined or has the wrong type'); const propsArr = Array.isArray(addr) ? addr : addr.replace(/\.?\[(\d+)\]/g, '.$1').split('.'); // replace .[4] AND [4] TO .4 const objRef = propsArr.reduce((objChain, prop) => { if (!(0, isset_1.isset)(objChain) || typeof objChain !== 'object' || !(0, isset_1.isset)(objChain[prop])) return; else return objChain[prop]; }, obj); return objRef; } exports.findByAddress = findByAddress; /** Will return all objects matching that path. Eg: user.*.myVar */ function findByAddressAll(obj, addr, returnAddresses = false) { (0, error_utils_1.err500IfNotSet)({ obj, addr }); if (addr === '') return (returnAddresses ? [addr, obj, undefined, undefined] : obj); const addrRegexp = new RegExp('^' + (0, regexp_utils_1.escapeRegexp)(addr.replace(/\.?\[(\d+)\]/g, '.$1'), // replace .[4] AND [4] TO .4 { parseWildcard: true, wildcardNotMatchingChars: '.[' }) + '$'); const matchingItems = []; (0, loop_utils_1.recursiveGenericFunctionSync)(obj, (item, address, lastElmKey, parent) => { if (addrRegexp.test(address)) matchingItems.push(returnAddresses ? [address, item, lastElmKey, parent] : item); }); return matchingItems; } exports.findByAddressAll = findByAddressAll; /** Enforce writing subItems. Eg: user.name.blah will ensure all are set until the writing of the last item * NOTE: doesn't work when parent is array */ function objForceWrite(obj, addr, item, options = {}) { const { doNotWriteFinalValue = false } = options; const writeFinalValue = !doNotWriteFinalValue; const chunks = addr.replace(/\.?\[(\d+)\]/g, '.[$1').split('.'); let lastItem = obj; chunks.forEach((chunkRaw, i) => { const chunk = chunkRaw.replace(/^\[/, ''); if (i === chunks.length - 1) { if (writeFinalValue) lastItem[chunk] = item; } else if (!(0, isset_1.isset)(lastItem[chunk])) { const nextChunk = chunks[i + 1]; if ((0, isset_1.isset)(nextChunk) && nextChunk.startsWith('[')) lastItem[chunk] = []; else lastItem[chunk] = {}; } else if (typeof lastItem[chunk] !== 'object') { throw new error_utils_2.DescriptiveError(`itemNotTypeObjectOrArrayInAddrChainForObjForceWrite`, { code: 500, origin: 'Validator', chunks: chunks.map(c => c.replace(/\[(\d+)/, '[$1]')), actualValueOfItem: lastItem[chunk], actualChunk: chunk, chunkIndex: i }); } lastItem = lastItem[chunk]; }); return obj; } exports.objForceWrite = objForceWrite; function forcePathInObject(obj, addr) { return objForceWrite(obj, addr, undefined, { doNotWriteFinalValue: true }); } exports.forcePathInObject = forcePathInObject; exports.objForceWritePath = forcePathInObject; /** Enforce writing subItems, only if obj.addr is empty. * Eg: user.name.blah will ensure all are set until the writing of the last item * if user.name.blah has a value it will not change it. * NOTE: doesn't work when parent is array */ function objForceWriteIfNotSet(obj, addr, item) { if (!(0, isset_1.isset)(findByAddress(obj, addr))) return objForceWrite(obj, addr, item); else return obj; } exports.objForceWriteIfNotSet = objForceWriteIfNotSet; /** Merge mixins into class. Use it in the constructor like: mergeMixins(this, {myMixin: true}) */ function mergeMixins(that, ...mixins) { mixins.forEach(mixin => { for (const method in mixin) { that[method] = mixin[method]; } }); } exports.mergeMixins = mergeMixins; function cloneObject(o) { return JSON.parse(JSON.stringify(o)); } exports.cloneObject = cloneObject; /** Deep clone. WILL REMOVE circular references */ function deepClone(obj, cache = []) { let copy; // usefull to not modify 1st level objet by lower levels // this is required for the same object to be referenced not in a redundant way const newCache = [...cache]; if (obj instanceof Date) return new Date(obj); // Handle Array if (Array.isArray(obj)) { if (newCache.includes(obj)) return []; newCache.push(obj); copy = []; for (let i = 0, len = obj.length; i < len; i++) { copy[i] = deepClone(obj[i], newCache); } return copy; } if (typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype) { if (newCache.includes(obj)) return {}; newCache.push(obj); copy = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = deepClone(obj[key], newCache); } } return copy; } return obj; // number, string... } exports.deepClone = deepClone; /** * @param {Object} obj the object on which we want to filter the keys * @param {function} filterFunc function that returns true if the key match the wanted criteria */ function filterKeys(obj, filter) { const clone = cloneObject(obj); (0, loop_utils_1.recursiveGenericFunctionSync)(obj, (_, addr, lastElementKey) => { if (!filter(lastElementKey)) deleteByAddress(clone, addr.split('.')); }); return clone; } exports.filterKeys = filterKeys; /** * @param {Object} obj the object on which we want to delete a property * @param {Array} addrArr addressArray on which to delete the property */ function deleteByAddress(obj, addr) { let current = obj; const addrArr = Array.isArray(addr) ? addr : addr.split('.'); for (let i = 0; i < addrArr.length; i++) { const currentAddr = addrArr[i].replace(/(\[|\])/g, ''); if (i === addrArr.length - 1) delete current[currentAddr]; else current = current[currentAddr]; } } exports.deleteByAddress = deleteByAddress; /** Remove all key/values pair if value is undefined */ function objFilterUndefined(o) { Object.keys(o).forEach(k => !(0, isset_1.isset)(o[k]) && delete o[k]); return o; } exports.objFilterUndefined = objFilterUndefined; /** Lock all 1st level props of an object to read only */ function readOnly(o) { const throwErr = () => { throw new error_utils_2.DescriptiveError('Cannot modify object that is read only', { code: 500 }); }; return new Proxy(o, { set: throwErr, defineProperty: throwErr, deleteProperty: throwErr, }); } exports.readOnly = readOnly; /** Fields of the object can be created BUT NOT reassignated */ function reassignForbidden(o) { return new Proxy(o, { defineProperty: function (that, key, value) { if (key in that) throw new error_utils_2.DescriptiveError(`Cannot reassign the property ${key.toString()} of this object`, { code: 500 }); else { that[key] = value; return true; } }, deleteProperty: function (_, key) { throw new error_utils_2.DescriptiveError(`Cannot delete the property ${key.toString()} of this object`, { code: 500 }); } }); } exports.reassignForbidden = reassignForbidden; /** All fileds and subFields of the object will become readOnly */ function readOnlyRecursive(object) { (0, loop_utils_1.recursiveGenericFunctionSync)(object, (item, _, lastElementKey, parent) => { if (typeof item === 'object' && parent) parent[lastElementKey] = readOnly(item); }); return object; } exports.readOnlyRecursive = readOnlyRecursive; /** @deprecated use readOnlyRecursive instead */ exports.readOnlyForAll = readOnlyRecursive; function objFilterUndefinedRecursive(obj) { if (obj) { const flattenedObj = flattenObject(obj); Object.keys(flattenedObj).forEach(key => { if (!(0, isset_1.isset)(flattenedObj[key])) { delete flattenedObj[key]; } }); return unflattenObject(flattenedObj); } else return obj; } exports.objFilterUndefinedRecursive = objFilterUndefinedRecursive; function sortObjKeyAccordingToValue(unorderedObj, ascending = true) { const orderedObj = {}; const sortingConst = ascending ? 1 : -1; Object.keys(unorderedObj) .sort((keyA, keyB) => unorderedObj[keyA] < unorderedObj[keyB] ? sortingConst : -sortingConst) .forEach(key => { orderedObj[key] = unorderedObj[key]; }); return orderedObj; } exports.sortObjKeyAccordingToValue = sortObjKeyAccordingToValue; /** * Make default value if object key do not exist * @param {object} obj * @param {string} addr * @param {any} defaultValue * @param {function} callback (obj[addr]) => processValue. Eg: myObjAddr => myObjAddr.push('bikou') * @return obj[addr] eventually processed by the callback */ function ensureObjectProp(obj, addr, defaultValue, callback) { (0, error_utils_1.err500IfNotSet)({ obj, addr, defaultValue, callback }); if (!(0, isset_1.isset)(obj[addr])) obj[addr] = defaultValue; if (callback) callback(obj[addr]); return obj[addr]; } exports.ensureObjectProp = ensureObjectProp; /** object and array merge * @warn /!\ Array will be merged and duplicate values will be deleted /!\ * @return {Object} new object result from merge * NOTE: objects in params will NOT be modified*/ function mergeDeep(...objects) { return mergeDeepConfigurable((previousVal, currentVal) => [...previousVal, ...currentVal].filter((elm, i, arr) => arr.indexOf(elm) === i), (previousVal, currentVal) => mergeDeep(previousVal, currentVal), undefined, ...objects); } exports.mergeDeep = mergeDeep; /** object and array merge * @warn /!\ Array will be replaced by the latest object /!\ * @return {Object} new object result from merge * NOTE: objects in params will NOT be modified */ function mergeDeepOverrideArrays(...objects) { return mergeDeepConfigurable(undefined, (previousVal, currentVal) => mergeDeepOverrideArrays(previousVal, currentVal), undefined, ...objects); } exports.mergeDeepOverrideArrays = mergeDeepOverrideArrays; /** object and array merge * @param {Function} replacerForArrays item[key] = (prevValue, currentVal) => () When 2 values are arrays, * @param {Function} replacerForObjects item[key] = (prevValue, currentVal) => () When 2 values are objects, * @param {Function} replacerDefault item[key] = (prevValue, currentVal) => () For all other values * @param {...Object} objects * @return {Object} new object result from merge * NOTE: objects in params will NOT be modified */ function mergeDeepConfigurable(replacerForArrays = (_, curr) => curr, replacerForObjects, replacerDefault = (_, curr) => curr, ...objects) { return objects.reduce((actuallyMerged, obj) => { if (obj && typeof obj === 'object') Object.keys(obj).forEach(key => { const previousVal = actuallyMerged[key]; const currentVal = obj[key]; if (Array.isArray(previousVal) && Array.isArray(currentVal)) { actuallyMerged[key] = replacerForArrays(previousVal, currentVal); } else if ((0, is_object_1.isObject)(previousVal) && (0, is_object_1.isObject)(currentVal)) { actuallyMerged[key] = replacerForObjects(previousVal, currentVal); } else { actuallyMerged[key] = replacerDefault(previousVal, currentVal); } }); return actuallyMerged; }, {}); } exports.mergeDeepConfigurable = mergeDeepConfigurable; /** { a: {b:2}} => {'a.b':2} useful for translations * NOTE: will remove circular references */ function flattenObject(data, config = {}) { const { withoutArraySyntax = false, withArraySyntaxMinified = false } = config; const result = {}; const seenObjects = []; // avoidCircular reference to infinite loop const recurse = (cur, prop) => { if (Array.isArray(cur)) { const l = cur.length; let i = 0; if (withoutArraySyntax) recurse(cur[0], prop); else { for (; i < l; i++) recurse(cur[i], prop + (withArraySyntaxMinified ? `.${i}` : `[${i}]`)); if (l == 0) result[prop] = []; } } else if ((0, is_object_1.isObject)(cur)) { // is object try { if (seenObjects.includes(cur)) cur = deepClone(cur); // avoid circular ref but allow duplicate objects else seenObjects.push(cur); const isEmpty = Object.keys(cur).length === 0; for (const p in cur) recurse(cur[p], (prop ? prop + '.' : '') + p.replace(/\./g, '%')); // allow prop to contain special chars like points); if (isEmpty && prop) result[prop] = {}; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // eslint-disable-next-line no-console console.warn('Circular reference in flattenObject, impossible to parse'); } } else result[prop] = cur; }; recurse(data, ''); return result; } exports.flattenObject = flattenObject; /** {'a.b':2} => { a: {b:2}} */ function unflattenObject(data) { const newO = {}; for (const [addr, value] of Object.entries(data)) objForceWrite(newO, addr, value); return newO; } exports.unflattenObject = unflattenObject; /** Mean to fix typing because type for Object.entries is not accurate. Ref: https://stackoverflow.com/questions/66565322/get-type-keys-in-typescript * /!\ THIS WILL REMOVE SYMBOL AND NUMBER FROM KEY TYPES as this is 99% of the time unwanted for generic objects */ function objEntries(obj) { return Object.entries(obj); } exports.objEntries = objEntries; /** Mean to fix typing because type for Object.keys is not accurate */ function objKeys(obj) { return Object.keys(obj); } exports.objKeys = objKeys; /** Will merge all arrays of an object into a single array */ function mergeObjectArrays(obj) { return Object.values(obj).flat(); } exports.mergeObjectArrays = mergeObjectArrays; exports.keys = objKeys; exports.entries = objEntries; /** A Helper to create JavascriptProxies, will add __isProxy and toJSON helper to prevent error when logging the proxy and to be able to check if the object is proxyfied */ function createProxy(obj, optn) { return new Proxy(obj, { get(target, prop, receiver) { if (prop === '__isProxy') return true; else if (prop === 'toJSON') { if ('toJSON' in target) return target.toJSON; else return () => (optn?.jsonRepresentation?.(target) || '[Proxy]'); } return optn.get(target, prop, receiver); } }); } exports.createProxy = createProxy; /** Revert object keys and values. * @example revertObjectKeysAndValues({ a: 1, b: 2 }) => { 1: 'a', 2: 'b' } * @param obj Object to revert * @returns New object with keys and values swapped */ function revertObjectKeysAndValues(obj) { const result = {}; for (const key in obj) { const value = obj[key]; result[value] = key; } return result; } exports.revertObjectKeysAndValues = revertObjectKeysAndValues; //# sourceMappingURL=object-utils.js.map