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.
273 lines
8.8 KiB
273 lines
8.8 KiB
3 years ago
|
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
|
||
|
);
|
||
|
}
|