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