148 lines
4.4 KiB
JavaScript
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 }
|