@stackbit/utils
Version:
Stackbit utilities
261 lines (243 loc) • 9.41 kB
text/typescript
import _, { PropertyPath } from 'lodash';
/**
* Gets the value at the first path of object having non undefined value.
* If all paths resolve to undefined values, the defaultValue is returned.
*
* @param object
* @param paths
* @param defaultValue
*/
export function getFirst(object: any, paths: PropertyPath, defaultValue?: any): any {
const result = _(object).at(paths).reject(_.isUndefined).first();
return _.isUndefined(result) ? defaultValue : result;
}
/**
* Appends the `value` to the end of the array located at the specified `path` in
* the provided `object`. If array at the specified `path` doesn't exist, the
* function creates a new array with a single `value` item.
*
* @param object
* @param path
* @param value
*/
export function append(object: any, path: PropertyPath, value: any): void {
if (!_.has(object, path)) {
_.set(object, path, []);
}
_.get(object, path).push(value);
}
/**
* Prepends the `value` to the beginning of the array located at the specified
* `path` in the provided `object`. If array at the specified `path` doesn't
* exist, the function creates a new array with a single `value` item.
*
* @param object
* @param path
* @param value
*/
export function prepend(object: any, path: PropertyPath, value: any): void {
if (!_.has(object, path)) {
_.set(object, path, []);
}
_.get(object, path).unshift(value);
}
/**
* Concatenates the `value` with an array located at the specified `path` in the
* provided `object`. If array at the specified `path` doesn't exist, the
* function creates a new array and concatenates it with `value`.
*
* @param object
* @param path
* @param value
*/
export function concat(object: any, path: PropertyPath, value: any) {
if (!_.has(object, path)) {
_.set(object, path, []);
}
const result = _.get(object, path).concat(value);
_.set(object, path, result);
}
/**
* Copies the value from the `sourceObject` at the specified `sourcePath` to
* the `targetObject` at the specified `targetPath`, optionally transforming the
* value using the `transform` function.
*
* If `sourcePath` resolves to `undefined`, this method does nothing.
*
* @param sourceObject
* @param sourcePath
* @param targetObject
* @param targetPath
* @param transform
*/
export function copy(sourceObject: any, sourcePath: PropertyPath, targetObject: any, targetPath: PropertyPath, transform?: (value: any) => any) {
if (_.has(sourceObject, sourcePath)) {
let value = _.get(sourceObject, sourcePath);
if (transform) {
value = transform(value);
}
_.set(targetObject, targetPath, value);
}
}
/**
* Copies the value from the `sourceObject` at the specified `sourcePath` to
* the `targetObject` at the specified `targetPath`.
*
* If `targetPath` resolves to `undefined`, this method does nothing.
* If `sourcePath` resolves to `undefined`, this method does nothing.
*
* Optionally transform the value using the `transform` function.
*
* @param sourceObject
* @param sourcePath
* @param targetObject
* @param targetPath
* @param transform
*/
export function copyIfNotSet(sourceObject: any, sourcePath: PropertyPath, targetObject: any, targetPath: PropertyPath, transform?: (value: any) => any) {
if (!_.has(targetObject, targetPath)) {
copy(sourceObject, sourcePath, targetObject, targetPath, transform);
}
}
/**
* Renames `oldPath` to a `newPath`.
*
* @param object
* @param oldPath
* @param newPath
*/
export function rename(object: any, oldPath: PropertyPath, newPath: PropertyPath) {
if (_.has(object, oldPath)) {
_.set(object, newPath, _.get(object, oldPath));
oldPath = _.toPath(oldPath);
if (oldPath.length > 1) {
object = _.get(object, _.initial(oldPath));
}
const lastKey = _.last(oldPath);
if (lastKey) {
delete object[lastKey];
}
}
}
/**
* Removed all null and undefined properties from the passed object
* @param object
*/
export function omitByNil<T extends Record<string, any>>(object: T): T {
return _.omitBy(object, _.isNil) as T;
}
export function omitByUndefined<T extends Record<string, any>>(object: T): T {
return _.omitBy(object, _.isUndefined) as T;
}
export function undefinedIfEmpty<T extends Record<string, any> | any[]>(value: T): T | undefined {
return _.isEmpty(value) ? undefined : value;
}
export type KeyPath = (string | number)[];
/**
* Creates an object with the same keys as `object` and values generated by
* recursively running each own enumerable string keyed property of `object` through
* `iteratee`.
*
* @param {any} object
* @param {Function} iteratee
* @param [options]
* @param {boolean} [options.context] The value of `this` provided for the call to `iteratee`. Default: undefined
* @param {boolean} [options.iterateCollections] Should the `iteratee` be called for collections. Default: true
* @param {boolean} [options.iteratePrimitives] Should the `iteratee` be called for primitives. Default: true
* @param {boolean} [options.includeKeyPath] Should the `iteratee` be called with `keyPath` parameter. Default: true
*/
export function deepMap<T>(
object: T,
iteratee: (value: any, object: T) => any,
options: { context?: any; iterateCollections?: boolean; iteratePrimitives?: boolean; includeKeyPath: false }
): any;
export function deepMap<T>(
object: T,
iteratee: (value: any, keyPath: KeyPath, mappedValueStack: any[], object: T) => any,
options?: { context?: any; iterateCollections?: boolean; iteratePrimitives?: boolean; includeKeyPath?: true }
): any;
export function deepMap(
object: any,
iteratee: (...args: any[]) => any,
options?: { context?: any; iterateCollections?: boolean; iteratePrimitives?: boolean; includeKeyPath?: boolean }
): any {
const context = _.get(options, 'context');
const iterateCollections = _.get(options, 'iterateCollections', true);
const iteratePrimitives = _.get(options, 'iteratePrimitives', true);
const includeKeyPath = _.get(options, 'includeKeyPath', true);
function _mapDeep(value: any, keyPath: KeyPath | null, mappedValueStack: any[] | null) {
const invokeIteratee = _.isPlainObject(value) || _.isArray(value) ? iterateCollections : iteratePrimitives;
if (invokeIteratee) {
value =
options?.includeKeyPath === false ? iteratee.call(context, value, object) : iteratee.call(context, value, keyPath, mappedValueStack, object);
}
const childrenIterator = (val: any, key: string | number) => {
if (includeKeyPath) {
return _mapDeep(val, _.concat(keyPath!, key), _.concat(mappedValueStack!, value));
}
return _mapDeep(val, null, null);
};
if (_.isPlainObject(value)) {
value = _.mapValues(value, childrenIterator);
} else if (Array.isArray(value)) {
value = _.map(value, childrenIterator);
}
return value;
}
return _mapDeep(object, [], []);
}
export async function asyncMapDeep(
value: any,
iteratee: (options: { value: any; keyPath: (string | number)[]; stack: any[]; skipNested: () => void }) => Promise<any>,
options: { postOrder?: boolean; iterateCollections?: boolean; iteratePrimitives?: boolean; iterateScalars?: boolean; context?: any } = {}
) {
const context = _.get(options, 'context');
const iterateCollections = _.get(options, 'iterateCollections', true);
const iteratePrimitives = _.get(options, 'iteratePrimitives', _.get(options, 'iterateScalars', true));
const postOrder = _.get(options, 'postOrder', false);
async function _mapDeep(value: any, keyPath: (string | number)[], stack: any[]) {
const invokeIteratee = _.isPlainObject(value) || _.isArray(value) ? iterateCollections : iteratePrimitives;
let shouldSkipNested = false;
if (invokeIteratee && !postOrder) {
value = await iteratee.call(context, {
value,
keyPath,
stack,
skipNested: () => {
shouldSkipNested = true;
}
});
}
// check if we should stop handling current branch
if (shouldSkipNested) {
return value;
}
if (_.isPlainObject(value)) {
const mappedObject: Record<string, any> = {};
const keys = Object.keys(value);
const values = await Promise.all(
_.map(keys, (key) => {
return _mapDeep(value[key], _.concat(keyPath, key), _.concat(stack, value));
})
);
keys.forEach((key, i) => (mappedObject[key] = values[i]));
value = mappedObject;
} else if (_.isArray(value)) {
value = await Promise.all(
_.map(value, (val, key) => {
return _mapDeep(val, _.concat(keyPath, key), _.concat(stack, [value]));
})
);
}
if (invokeIteratee && postOrder) {
value = await iteratee.call(context, {
value,
keyPath,
stack,
skipNested: () => {}
});
}
return value;
}
return _mapDeep(value, [], []);
}