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.
426 lines • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
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[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;
//# sourceMappingURL=object-utils.js.map