@finos/legend-shared
Version:
Legend Studio shared utilities and helpers
228 lines • 8.67 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { clone, cloneDeep as deepClone, isEqual as deepEqual, findLast, isEmpty, pickBy, uniqBy, uniq, debounce, throttle, mergeWith, isObject, shuffle, } from 'lodash-es';
import { diff as deepDiff } from 'deep-object-diff';
import { UnsupportedOperationError } from './error/ErrorUtils.js';
import { format as prettyPrintObject } from 'pretty-format';
import { guaranteeNonNullable } from './error/AssertionUtils.js';
// NOTE: We re-export lodash utilities like this so we centralize utility usage in our app
// in case we want to swap out the implementation
export { clone, deepClone, deepEqual, deepDiff, findLast, isEmpty, pickBy, uniqBy, uniq, debounce, throttle, shuffle, };
// NOTE: we can use the `rng` option in UUID V4 to control the random seed during testing
// See https://github.com/uuidjs/uuid#version-4-random
export { v4 as uuid } from 'uuid';
export const getClass = (obj) => obj.constructor;
export const getSuperclass = (_class) => {
if (!_class.name) {
throw new UnsupportedOperationError(`Cannot get superclass for non user-defined classes`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const superclass = Object.getPrototypeOf(_class);
/**
* When it comes to inheritance, JavaScript only has one construct: objects.
* Each object has a private property which holds a link to another object called its prototype.
* That prototype object has a prototype of its own, and so on until an object is reached
* with null as its prototype. By definition, null has no prototype,
* and acts as the final link in this prototype chain.
*
* NOTE: when the prototype name is `empty` we know it's not user-defined classes, so we can return undefined
*/
return superclass?.name ? superclass : undefined;
};
/**
* Check if the specified class is either the same as, or is a superclass of the provided class.
*/
export const isClassAssignableFrom = (cls1, cls2) => {
let currentPrototype = cls2;
while (currentPrototype) {
if (currentPrototype === cls1) {
return true;
}
currentPrototype = getSuperclass(currentPrototype);
}
return false;
};
export const noop = () => () => {
/* do nothing */
};
/**
* Recursively omit keys from an object
*/
export const recursiveOmit = (obj,
/**
* Checker function which returns `true` if the object field should be omit
*/
checker) => {
const newObj = deepClone(obj);
const omit = (_obj, _checker) => {
if (Array.isArray(_obj)) {
_obj.forEach((o) => omit(o, _checker));
}
else {
const o = _obj;
for (const propKey in o) {
if (Object.prototype.hasOwnProperty.call(_obj, propKey)) {
const value = o[propKey];
if (_checker(_obj, propKey)) {
delete o[propKey];
}
else if (isObject(value)) {
omit(value, _checker);
}
}
}
}
};
omit(newObj, checker);
return newObj;
};
/**
* Recursively remove fields with undefined values in object
*/
export const pruneObject = (obj) => pickBy(obj, (val) => val !== undefined);
/**
* Recursively remove fields with null values in object
*
* This is particularly useful in serialization, especially when handling response
* coming from servers where `null` are returned for missing fields. We would like to
* treat them as `undefined` instead, so we want to strip all the `null` values from the
* plain JSON object.
*/
export const pruneNullValues = (obj) => pickBy(obj, (val) => val !== null);
/**
* Recursively sort object keys alphabetically
*/
export const sortObjectKeys = (value) => {
const _sort = (obj) => {
if (Array.isArray(obj)) {
return obj.map(sortObjectKeys);
}
else if (typeof obj === 'object' && obj !== null) {
const oldObj = obj;
const newObj = {};
Object.keys(oldObj)
.sort((a, b) => a.localeCompare(b))
.forEach((key) => {
newObj[key] = _sort(oldObj[key]);
});
return newObj;
}
return obj;
};
return _sort(value);
};
export const parseNumber = (val) => {
const num = Number(val);
if (isNaN(num)) {
throw new Error(`Can't parse number '${val}'`);
}
return num;
};
/**
* Stringify object shallowly
* See https://stackoverflow.com/questions/16466220/limit-json-stringification-depth
*/
export const shallowStringify = (object) => JSON.stringify(object, (key, val) => key && val && typeof val !== 'number'
? Array.isArray(val)
? '[object Array]'
: `${val}`
: val);
export const generateEnumerableNameFromToken = (existingNames, token, delim = 'underscore') => {
if (!token.match(/^[\w_-]+$/)) {
throw new Error(`Token must only contain digits, letters, or special characters _ and -`);
}
const delimiter = delim === 'whitespace' ? ' ' : '_';
const maxCounter = existingNames
.map((name) => {
const matchingCount = name.match(new RegExp(`^${token}${delimiter}(?<count>\\d+)$`));
return matchingCount?.groups?.count
? parseInt(matchingCount.groups.count, 10)
: 0;
})
.reduce((max, num) => Math.max(max, num), 0);
return `${token}${delimiter}${maxCounter + 1}`;
};
export const at = (list, idx, message) => {
return guaranteeNonNullable(list.at(idx), message);
};
/**
* NOTE: This object mutates the input object (obj1)
* To disable this behavior, set `createClone=true`
*/
export const mergeObjects = (obj1, obj2, createClone) => mergeWith(createClone ? deepClone(obj1) : obj1, obj2, (o1, o2) => {
if (Array.isArray(o1)) {
return o1.concat(o2);
}
return undefined;
});
export const promisify = (func) => new Promise((resolve, reject) => setTimeout(() => {
try {
resolve(func());
}
catch (error) {
reject(error);
}
}, 0));
export function sleep(duration) {
return new Promise((resolve) => setTimeout(resolve, duration));
}
export const addUniqueEntry = (array, newEntry, comparator = (val1, val2) => val1 === val2) => {
if (!array.find((entry) => comparator(entry, newEntry))) {
array.push(newEntry);
return true;
}
return false;
};
export const changeEntry = (array, oldEntry, newEntry, comparator = (val1, val2) => val1 === val2) => {
const idx = array.findIndex((entry) => comparator(entry, oldEntry));
if (idx !== -1) {
array[idx] = newEntry;
return true;
}
return false;
};
export const swapEntry = (array, entryOne, entryTwo, comparator = (val1, val2) => val1 === val2) => {
const idxX = array.findIndex((entry) => comparator(entry, entryOne));
const idxY = array.findIndex((entry) => comparator(entry, entryTwo));
if (idxX !== -1 && idxY !== -1) {
array[idxX] = entryTwo;
array[idxY] = entryOne;
return true;
}
return false;
};
export const deleteEntry = (array, entryToDelete, comparator = (val1, val2) => val1 === val2) => {
const idx = array.findIndex((entry) => comparator(entry, entryToDelete));
if (idx !== -1) {
array.splice(idx, 1);
return true;
}
return false;
};
export const printObject = (value, options) => {
const opts = pruneObject({
printFunctionName: false,
maxDepth: options?.deep ? undefined : 1,
});
const text = prettyPrintObject(value, opts);
return (text
// We do these replacements because when we do this for production and the class name is minified,
// we'd better show `[Object]` instead.
.replace(/.*\s\{/g, '{')
.replace(/\[.*\]/g, (val) => ['[Array]', '[Function]'].includes(val) ? val : '[Object]'));
};
export const hasWhiteSpace = (val) => Boolean(val.match(/\s/));
//# sourceMappingURL=CommonUtils.js.map