You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/util/support/data.ts

273 lines
8.8 KiB

import * as node_uuid from 'uuid'
import {ErrorWithContext} from "../error/ErrorWithContext";
/**
* Make a deep copy of an object.
* @param target
*/
export function deepCopy<T>(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<any>(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<any>(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<T>(
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<T>(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<T>(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<T>(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<T>(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<T>(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<T>(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<T>(data, subpath.join('.'), defaultValue)
} else if ( !isNaN(parseInt(part)) ) {
const subdata = data[parseInt(part)]
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
} else {
const subdata = data.map(x => x[part])
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
}
} else {
if ( part === '*' ) {
const subdata = Object.values(data)
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
} else {
const subdata = data[part]
if ( subpath.length ) return dataGetUnsafe<T>(subdata, subpath.join('.'), defaultValue)
else return subdata
}
}
}
function dataGet<
T,
P1 extends keyof NonNullable<T>
>(obj: T, prop1: P1): NonNullable<T>[P1] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>
>(obj: T, prop1: P1, prop2: P2): NonNullable<NonNullable<T>[P1]>[P2] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>,
P3 extends keyof NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>
>(obj: T, prop1: P1, prop2: P2, prop3: P3): NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>[P3] | undefined;
function dataGet<
T,
P1 extends keyof NonNullable<T>,
P2 extends keyof NonNullable<NonNullable<T>[P1]>,
P3 extends keyof NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>,
P4 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<T>[P1]>[P2]>[P3]>
>(obj: T, prop1: P1, prop2: P2, prop3: P3, prop4: P4): NonNullable<NonNullable<NonNullable<NonNullable<T>[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
);
}