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(`(? 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 }