183 lines
5.3 KiB
JavaScript
183 lines
5.3 KiB
JavaScript
import { check } from '@uci-utils/type'
|
|
import { logger } from '@uci-utils/logger'
|
|
|
|
const log = logger({ file: '/src/obj-nested-prop.js', package: '@uci-utils/obj-nested-prop' })
|
|
|
|
function set (obj, path, value, opts = {}) {
|
|
if (value === undefined) return del(obj, path)
|
|
const keys = validateArgs(obj, path, opts)
|
|
if (!keys) return obj
|
|
const target = obj
|
|
|
|
log.debug({ function: 'set', obj: obj, path: path, keys: keys, value: value, msg: 'setting value' })
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i]
|
|
if (!isValidKey(key)) {
|
|
break
|
|
}
|
|
const next = keys[i + 1]
|
|
|
|
log.trace({ function: 'set', obj: target, key: key, next: next, msg: 'iterate keys' })
|
|
|
|
// validateKey(key); ?? necessary
|
|
|
|
if (next === undefined) { // at a leaf, set
|
|
log.debug({ function: 'set', value: value, msg: 'no more props, setting value' })
|
|
if (opts.merge && check.isPlainObject(obj[key]) && check.isPlainObject(value)) {
|
|
const merge = opts.merge === true ? Object.assign : opts.merge
|
|
// Only merge plain objects
|
|
obj[key] = merge(obj[key], value)
|
|
} else {
|
|
obj[key] = value
|
|
}
|
|
break
|
|
}
|
|
|
|
if (check.isNumber(next) && !check.isArray(obj[key])) {
|
|
log.debug({ function: 'set', obj: obj, key: key, msg: 'adding array' })
|
|
obj[key] = [] // add array
|
|
} else if (!(check.isObject(obj[key]) || check.isFunction(obj[key]))) obj[key] = {} // add empty object
|
|
obj = obj[key]
|
|
} // next key
|
|
|
|
log.debug({ function: 'set', obj: target, path: path, value: value, msg: 'value was set' })
|
|
return target
|
|
}
|
|
|
|
function del (obj, path) {
|
|
const keys = validateArgs(obj, path)
|
|
if (!keys) return obj
|
|
log.debug({ function: 'del', obj: obj, path: path, keys: keys, msg: 'deleting property' })
|
|
const last = walkPath(obj, keys.slice(0, -1))
|
|
if (check.isObject(last)) {
|
|
delete last[keys.slice(-1)]
|
|
log.debug({ obj: obj, path: path, keys: keys, deleted: keys.slice(-1), msg: 'deleted property from object' })
|
|
}
|
|
return obj
|
|
}
|
|
|
|
function get (obj, path) {
|
|
const keys = validateArgs(obj, path)
|
|
if (!keys) return undefined
|
|
const value = walkPath(obj, keys)
|
|
log.debug({ obj: obj, path: path, keys: keys, value: value, msg: 'retrieved value from object' })
|
|
return value
|
|
}
|
|
|
|
function getKeys (path, opts = {}) {
|
|
if (!(check.isString(path) || check.isArray(path) || check.isSymbol(path))) {
|
|
log.warn({ function: 'getKeys', path: path, msg: 'path was not valid' })
|
|
return false
|
|
}
|
|
if (check.isFunction(opts.split)) return opts.split(path, opts)
|
|
|
|
const sep = opts.separator || opts.delimiter || '.'
|
|
if (check.isSymbol(path)) {
|
|
return [path]
|
|
}
|
|
|
|
const regx = new RegExp(`(?<!\\\\)[${sep}]`, 'g')
|
|
const regxr = /\\/ig
|
|
const splitr = str => {
|
|
return str.split(regx).map(e => e.replace(regxr, ''))
|
|
}
|
|
|
|
const keys = check.isArray(path)
|
|
? path.filter(Boolean)
|
|
.map(e => {
|
|
if (check.isArray(e)) {
|
|
e = e.filter(Boolean)
|
|
const splits = new Map()
|
|
let ne = []
|
|
e.forEach((en, i) => {
|
|
if (!check.isArray(en) && check.isString(en) && opts.elSplit !== false) {
|
|
splits.set(i, splitr(en))
|
|
}
|
|
})
|
|
e.forEach((el, j) => {
|
|
ne = splits.has(j) ? [...ne, ...splits.get(j)] : [...ne, el]
|
|
})
|
|
e = ne
|
|
}
|
|
if (check.isString(e) && opts.elSplit !== false) e = splitr(e)
|
|
return e
|
|
})
|
|
.flat()
|
|
: splitr(path)
|
|
|
|
if (keys.find(el => check.isArray(el))) return false // can have more than one nested array
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (!check.isString(keys[i])) break
|
|
const { is, number } = isNumber(keys[i])
|
|
|
|
if (is) {
|
|
keys[i] = number
|
|
continue
|
|
}
|
|
}
|
|
|
|
return keys
|
|
}
|
|
|
|
function pathToString (path, opts = {}) {
|
|
const keys = getKeys(path, opts)
|
|
if (keys.reduce((res, key) => res && !check.isSymbol(key), true)) return keys.join(opts.dot || '.')
|
|
else {
|
|
log.warn({ function: 'pathToString', path: path, keys: keys, opts: opts, msg: 'unable to make string of path' })
|
|
return null
|
|
}
|
|
}
|
|
|
|
function walkPath (obj, keys) {
|
|
let last = obj
|
|
if (!check.isArray(keys)) return null
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (!isValidKey(keys[i])) {
|
|
last = null
|
|
break
|
|
}
|
|
last = last[keys[i]]
|
|
if (!last) {
|
|
log.warn({ function: 'walkPath', obj: obj, keys: keys, key: keys[i], msg: 'key is not in object, aborting walk' })
|
|
// will return undefined
|
|
break
|
|
}
|
|
log.trace({ function: 'walkPath', curProp: last, msg: 'fetching... deep property' })
|
|
}
|
|
return last
|
|
}
|
|
|
|
// non exported local functions
|
|
|
|
const validateArgs = (obj, path, opts) => {
|
|
if (!check.isObject(obj)) {
|
|
log.warn({ function: 'del', obj: obj, msg: 'passed is not an object' })
|
|
return false
|
|
}
|
|
return getKeys(path, opts)
|
|
}
|
|
|
|
const isNumber = value => {
|
|
if (value.trim() !== '') {
|
|
const number = Number(value)
|
|
return { is: Number.isInteger(number), number }
|
|
}
|
|
return { is: false }
|
|
}
|
|
|
|
const isUnsafeKey = key => {
|
|
return key === '__proto__' || key === 'constructor' || key === 'prototype'
|
|
}
|
|
|
|
const isValidKey = key => {
|
|
if (isUnsafeKey(key)) {
|
|
throw new Error(`Cannot set unsafe key: "${key}"`)
|
|
}
|
|
// TODO custom key validation if needed
|
|
return true
|
|
}
|
|
|
|
export { set, get, del, getKeys, walkPath, pathToString }
|