import * as node_uuid from 'uuid' import {ErrorWithContext} from "../error/ErrorWithContext"; /** * Make a deep copy of an object. * @param target */ export function deepCopy(target: T): T { if ( target === null ) return target if ( target instanceof Date ) return new Date(target.getTime()) as any if ( target instanceof Array ) { const copy = [] as any[] (target as any[]).forEach(item => { copy.push(item) }) return copy.map((item: any) => deepCopy(item)) as any } if ( typeof target === 'object' && target !== {} ) { const copy = { ...(target as {[key: string]: any }) } as { [key: string]: any } Object.keys(copy).forEach(key => { copy[key] = deepCopy(copy[key]) }) } return target } /** * Given a string of a value, try to infer the JavaScript type. * @param {string} val */ export function infer(val: string) { if ( !val ) return undefined else if ( val.toLowerCase() === 'true' ) return true else if ( val.toLowerCase() === 'false' ) return false else if ( !isNaN(Number(val)) ) return Number(val) else if ( isJSON(val) ) return JSON.parse(val) else if ( val.toLowerCase() === 'null' ) return null else if ( val.toLowerCase() === 'undefined' ) return undefined else return val } /** * Given an inferred value, try to convert back to the string form. * @param val */ export function uninfer(val: any) { if ( typeof val === 'undefined' ) return 'undefined' else if ( val === true ) return 'true' else if ( val === false ) return 'false' else if ( !isNaN(Number(val)) ) return `${Number(val)}` else if ( typeof val === 'string' ) return val else return JSON.stringify(val) } /** * Returns true if the given value is valid JSON. * @param {string} val */ export function isJSON(val: string): boolean { try { JSON.parse(val) return true } catch (e) { return false } } /** * Get a universally-unique ID string. * @return string */ export function uuid_v4() { return node_uuid.v4() } /** * Walks the given data object down to the given path, yielding subkeys. * This is NOT typesafe and is intended for framework use only. * @param data * @param path * @param defaultValue * @param currentPath */ export function* dataWalkUnsafe( data: {[key: string]: any} | any[] | undefined, path: string, defaultValue?: T, currentPath: string = '' ): IterableIterator<[T | T[] | undefined, string]> { if ( !data ) { yield [defaultValue, currentPath] return } else if ( !path ) { yield [data as T, currentPath] return } const [part, ...subpath] = path.split('.') if ( Array.isArray(data) ) { if ( part === '*' ) { if ( subpath.length ) { // @ts-ignore for ( const val of dataWalkUnsafe(data, subpath.join('.'), defaultValue, currentPath) ) { yield val } } else { for ( const key in data ) { yield [data[key], `${currentPath ? currentPath + '.' : ''}${key}`] } } } else if ( !isNaN(parseInt(part)) ) { const subdata = data[parseInt(part)] if ( subpath.length ) { // @ts-ignore for ( const val of dataWalkUnsafe(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${parseInt(part)}`) ) { yield val } } else { yield [subdata, `${currentPath ? currentPath + '.' : ''}${parseInt(part)}`] } } else { const subdata = data.map(x => x[part]) if ( subpath.length ) { // @ts-ignore for ( const val of dataWalkUnsafe(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) { yield val } } else { yield [subdata, `${currentPath ? currentPath + '.' : ''}*.${part}`] } } } else { if ( part === '*' ) { const subdata = Object.values(data) if ( subpath.length ) { // @ts-ignore for ( const val of dataWalkUnsafe(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}*`) ) { yield val } } else { yield [subdata, `${currentPath ? currentPath + '.' : ''}*`] } } else { const subdata = data[part] if ( subpath.length ) { // @ts-ignore for ( const val of dataWalkUnsafe(subdata, subpath, defaultValue, `${currentPath ? currentPath + '.' : ''}${part}`) ) { yield val } } else { yield [subdata, `${currentPath ? currentPath + '.' : ''}${part}`] } } } } /** * Set a deep-nested property on the given object. * This is NOT typesafe and is meant for internal use only. * @param path * @param value * @param data */ export function dataSetUnsafe(path: string, value: any, data: {[key: string]: any} | any[] | undefined) { data = data || ((!isNaN(parseInt(path.split('.')[0]))) ? [] : {}) let current = data const parts = path.split('.') parts.forEach((part, idx) => { if ( idx === path.split('.').length - 1 ) { current[Array.isArray(current) ? parseInt(part) : part] = value return } let next = Array.isArray(current) ? parseInt(parts[idx + 1]) : current?.[parts[idx + 1]] if ( !next ) { next = (!isNaN(parseInt(parts[idx + 1])) ? [] : {}) } if ( Array.isArray(current) ) { if ( isNaN(parseInt(part)) ) { throw new ErrorWithContext(`Invalid property name "${part}" of array-type.`, {part, path}) } current[parseInt(part)] = next current = next } else { current[part] = next current = next } }) return data } /** * Get a deep-nested property on the given data object. * This is NOT typesafe and is meant for internal use only. * @param data * @param path * @param defaultValue */ export function dataGetUnsafe(data: {[key: string]: any} | any[] | undefined, path: string, defaultValue?: T): T | T[] | undefined { if ( !data ) return if ( !path ) return data as T const [part, ...subpath] = path.split('.') if ( Array.isArray(data) ) { if ( part === '*' ) { return dataGetUnsafe(data, subpath.join('.'), defaultValue) } else if ( !isNaN(parseInt(part)) ) { const subdata = data[parseInt(part)] if ( subpath.length ) return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) else return subdata } else { const subdata = data.map(x => x[part]) if ( subpath.length ) return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) else return subdata } } else { if ( part === '*' ) { const subdata = Object.values(data) if ( subpath.length ) return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) else return subdata } else { const subdata = data[part] if ( subpath.length ) return dataGetUnsafe(subdata, subpath.join('.'), defaultValue) else return subdata } } } function dataGet< T, P1 extends keyof NonNullable >(obj: T, prop1: P1): NonNullable[P1] | undefined; function dataGet< T, P1 extends keyof NonNullable, P2 extends keyof NonNullable[P1]> >(obj: T, prop1: P1, prop2: P2): NonNullable[P1]>[P2] | undefined; function dataGet< T, P1 extends keyof NonNullable, P2 extends keyof NonNullable[P1]>, P3 extends keyof NonNullable[P1]>[P2]> >(obj: T, prop1: P1, prop2: P2, prop3: P3): NonNullable[P1]>[P2]>[P3] | undefined; function dataGet< T, P1 extends keyof NonNullable, P2 extends keyof NonNullable[P1]>, P3 extends keyof NonNullable[P1]>[P2]>, P4 extends keyof NonNullable[P1]>[P2]>[P3]> >(obj: T, prop1: P1, prop2: P2, prop3: P3, prop4: P4): NonNullable[P1]>[P2]>[P3]>[P4] | undefined; /** * A type-safe way to get the value of a key nested up to 4 levels deep in an object. * @param obj * @param props */ function dataGet(obj: any, ...props: string[]): any { return obj && props.reduce( (result, prop) => result == null ? undefined : result[prop], obj ); }