uci-utils-obj-nested-prop/src/obj-nested-prop.js

148 lines
4.4 KiB
JavaScript

import { check } from '@uci-utils/type'
import { logger } from '@uci-utils/logger'
const log = logger({ file: '/src/obj-prop.js', package: '@uci-utils/obj-prop' })
const 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
}
const 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 (typeof path === 'symbol') {
return [path]
}
// custom get keys
if (typeof opts.split === 'function') {
return opts.split(path, opts)
}
const regx = new RegExp(`(?<!\\\\)[${sep}]`, 'g')
const regxr = /\\/ig
const splitr = i => i.split(regx).map(e => e.replace(regxr, ''))
const keys = Array.isArray(path) ? path.filter(Boolean).map(e => typeof e === 'string' && opts.elSplit !== false ? splitr(e) : e).flat() : splitr(path)
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 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)
}
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' })
break
}
log.trace({ function: 'walkPath', curProp: last, msg: 'fetching... deep property' })
}
return last
}
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 }