UNPKG

@stackbit/utils

Version:
261 lines (243 loc) 9.41 kB
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, [], []); }