initial commit

working set, get, and del functions
master
Kebler Network System Administrator 2021-05-07 21:25:11 -07:00
parent cd9016d8f0
commit 958e46afd9
9 changed files with 529 additions and 0 deletions

15
.eslintrc.yml Normal file
View File

@ -0,0 +1,15 @@
env:
node: true
es2021: true
mocha: true
extends:
- standard
parserOptions:
ecmaVersion: 12
sourceType: module
rules:
indent: ["error", 2]
no-console: 0
semi: ["error", "never"]
# linebreak-style: ["error", "unix"]
quotes: ["error", "single"]

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules/

8
.npmignore Normal file
View File

@ -0,0 +1,8 @@
tests/
test/
*.test.js
testing/
node_modules
eslintrc*
examples/
example/

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.tokenColorCustomizations": {
"comments": "",
"textMateRules": []
}
}

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "@uci-utils/obj-nested-prop",
"version": "0.1.0",
"description": "Functions to get set, delete, and search nested properties of an object",
"main": "src/obj-nested-prop.js",
"type": "module",
"engines": {
"node": ">=16"
},
"scripts": {
"test": "./node_modules/.bin/mocha --timeout 30000",
"test:log": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log 0 npx run test || exit 0",
"test:dev": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha --timeout 30000' || exit 0",
"test:dev:trace": "UCI_LOG_LEVEL='trace' npm run test:dev",
"test:dev:debug": "UCI_LOG_LEVEL='debug' npm run test:dev",
"test:dev:error": "UCI_LOG_LEVEL='error' npm run test:dev"
},
"author": "David Kebler",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/uCOMmandIt/uci-utils.git"
},
"keywords": [
"node.js",
"object",
"nested",
"properties",
"utilities",
"helpers"
],
"bugs": {
"url": "https://github.com/uCOMmandIt/uci-utils/issues"
},
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
"dependencies": {
"@uci-utils/logger": "^0.1.0",
"@uci-utils/type": "^0.6.2"
},
"devDependencies": {
"eslint": "^7.25.0",
"eslint-config-standard": "^16.0.2",
"mocha": "^8.4.0",
"nodemon": "^2.0.7"
}
}

12
readme.md Normal file
View File

@ -0,0 +1,12 @@
# Object Nested Property Functions
#### a uCOMmandIt Utiltiy
Set, access and delete nested object properties with a path string and or array
exported set,get,del,getKeys,walkPath

147
src/obj-nested-prop.js Normal file
View File

@ -0,0 +1,147 @@
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 }

View File

@ -0,0 +1,293 @@
import assert from 'assert'
import { get, set, del } from '../src/obj-nested-prop.js'
import { check } from '@uci-utils/type'
const date = new Date(Date.now())
console.log('run:', date.getMinutes(), ':', date.getSeconds())
const obj = { one: { two: { three: 'test', four: ['el0', 'el1'] } } }
describe('get', function () {
it('Should get value deep in object', function () {
assert.strictEqual(get(obj, 'one.two.three'), 'test')
})
it('Should return undefined if path is not in object', function () {
assert.strictEqual(get(obj, 'one.seven.three'), undefined)
})
it('Should get value from array deep in object', function () {
assert.strictEqual(get(obj, 'one.two.four.1'), 'el1')
})
})
describe('del', function () {
it('deep property can be deleted', function () {
del(obj, 'one.two.three')
assert.deepStrictEqual(obj, { one: { two: { four: ['el0', 'el1'] } } })
})
})
describe('unsafe properties', () => {
it('should not allow setting constructor', () => {
// assert.deepStrictEqual(set({}, 'a.constructor.b', 'c'), {})
assert.throws(() => set({}, 'a.constructor', 'c'))
// assert.throws(() => set({}, 'constructor', 'c'))
})
it('should not allow setting prototype', () => {
assert.throws(() => set({}, 'a.prototype.b', 'c'))
assert.throws(() => set({}, 'a.prototype', 'c'))
assert.throws(() => set({}, 'prototype', 'c'))
})
it('should not allow setting __proto__', () => {
assert.throws(() => set({}, 'a.__proto__.b', 'c'))
assert.throws(() => set({}, 'a.__proto__', 'c'))
assert.throws(() => set({}, '__proto__', 'c'))
})
})
describe('set', () => {
it('should support immediate properties', () => {
const o = {}
set(o, 'a', 'b')
assert.strictEqual(o.a, 'b')
})
it('should return non-objects', () => {
const str = set('foo', 'a.b', 'c')
assert.strictEqual(str, 'foo')
const _null = set(null, 'a.b', 'c')
assert.strictEqual(_null, null)
})
it('should set when key is a symbol', () => {
const key = Symbol('foo')
const obj = {}
set(obj, key, 'bar')
assert.strictEqual(obj[key], 'bar')
})
it('should set on the root of the object', () => {
const o = {}
set(o, 'foo', 'bar')
assert.strictEqual(o.foo, 'bar')
})
it('deep property can be set', function () {
set(obj, 'one.two.three', 'test2')
assert.strictEqual(obj.one.two.three, 'test2')
})
it('should set the specified property.', () => {
assert.deepStrictEqual(set({ a: 'aaa', b: 'b' }, 'a', 'bbb'), { a: 'bbb', b: 'b' })
})
it('should set a nested property', () => {
const o = {}
set(o, 'a.b', 'c')
assert.strictEqual(o.a.b, 'c')
})
it('should set a nested property where the last key is a symbol', () => {
const o = {}
set(o, 'a.b', 'c')
assert.strictEqual(o.a.b, 'c')
})
it('should support passing an array as the key', () => {
const actual = set({ a: 'a', b: { c: 'd' } }, ['b', 'c', 'd'], 'eee')
assert.deepStrictEqual(actual, { a: 'a', b: { c: { d: 'eee' } } })
})
it('should set a deeply nested value.', () => {
const actual = set({ a: 'a', b: { c: 'd' } }, 'b.c.d', 'eee')
assert.deepStrictEqual(actual, { a: 'a', b: { c: { d: 'eee' } } })
})
it('should allow keys to be whitespace', () => {
const o = {}
set(o, 'a. .a', { y: 'z' })
assert.deepStrictEqual(o.a[' '].a, { y: 'z' })
})
it('should set an array element', () => {
const o = { a: ['zero', 'one'] }
set(o, 'a.0', { y: 'z' })
assert(Array.isArray(o.a))
assert.deepStrictEqual(o.a[0], { y: 'z' })
assert.deepStrictEqual(o.a[1], 'one')
})
it('should create an array if it does not already exist', () => {
const o = {}
set(o, 'a.0', 'zero')
set(o, 'a.1.b', { y: 'z' })
set(o, 'a.1.b.x', 'eggs')
set(o, 'c.1', 'see')
set(o, '0', 'not array')
assert(check.isArray(o.a))
assert.deepStrictEqual(o.a[0], 'zero')
assert.deepStrictEqual(o.a[1].b, { y: 'z', x: 'eggs' })
assert.deepStrictEqual(o.c[1], 'see')
assert.deepStrictEqual(o[0], 'not array')
})
it('should extend a function', () => {
const log = () => { }
const warning = () => { }
const o = {}
set(o, 'helpers.foo', log)
set(o, 'helpers.foo.warning', warning)
assert(check.isFunction(o.helpers.foo))
assert(check.isFunction(o.helpers.foo))
})
it('should extend an object in an array', () => {
const o = { a: [{}, {}, {}] }
set(o, 'a.0.a', { y: 'z' })
set(o, 'a.1.b', { y: 'z' })
set(o, 'a.2.c', { y: 'z' })
assert(Array.isArray(o.a))
assert.deepStrictEqual(o.a[0].a, { y: 'z' })
assert.deepStrictEqual(o.a[1].b, { y: 'z' })
assert.deepStrictEqual(o.a[2].c, { y: 'z' })
})
it('should not create a nested property if it does already exist', () => {
const first = { name: 'Halle' }
const o = { a: first }
set(o, 'a.b', 'c')
assert.strictEqual(o.a.b, 'c')
assert.strictEqual(o.a, first)
assert.strictEqual(o.a.name, 'Halle')
})
it('should use property paths to set nested values from the source object.', () => {
const o = {}
set(o, 'a.locals.name', { first: 'Brian' })
set(o, 'b.locals.name', { last: 'Woodward' })
set(o, 'b.locals.name.last', 'Woodward')
assert.deepStrictEqual(o, { a: { locals: { name: { first: 'Brian' } } }, b: { locals: { name: { last: 'Woodward' } } } })
})
it('should delete the property when value is undefined', () => {
const o = {}
assert.deepStrictEqual(set(o, 'a.locals.name', 'test'), o)
assert.deepStrictEqual(set(o, 'b.locals.name'), o)
assert.deepStrictEqual(set(o, 'a.locals.name'), { a: { locals: {} } })
assert.deepStrictEqual(set({ a: 'a', b: { c: 'd' } }, 'b.c'), { a: 'a', b: {} })
})
it('should return the entire object if no or improper path is passed.', () => {
assert.deepStrictEqual(set({ a: 'a', b: { c: 'd' } }), { a: 'a', b: { c: 'd' } })
})
it('should set non-plain objects', done => {
const o = {}
set(o, 'a.b', new Date())
const firstDate = o.a.b.getTime()
setTimeout(function () {
set(o, 'a.b', new Date())
const secondDate = o.a.b.getTime()
assert.notDeepStrictEqual(firstDate, secondDate)
done()
}, 10)
})
})
describe('escaping', () => {
it('should not split escaped dots. Works with set, get or del', () => {
const o = {}
set(o, 'a\\.b.c.d.e', 'test')
assert.strictEqual(o['a.b'].c.d.e, 'test')
assert.strictEqual(get(o, 'a\\.b.c.d.e'), 'test')
assert.deepStrictEqual(del(o, 'a\\.b.c.d.e'), { 'a.b': { c: { d: {} } } })
})
it('should work with multiple escaped dots', () => {
const obj1 = {}
set(obj1, 'e\\.f\\.g', 1)
assert.strictEqual(obj1['e.f.g'], 1)
const obj2 = {}
set(obj2, 'e\\.f.g\\.h\\.i.j', 1)
assert.deepStrictEqual(obj2, { 'e.f': { 'g.h.i': { j: 1 } } })
})
it('should work with multiple escaped any separator', () => {
const obj2 = {}
set(obj2, 'e\\.f.g\\/h\\%i.j', 1, { separator: './%' })
assert.deepStrictEqual(obj2, { 'e.f': { 'g/h%i': { j: 1 } } })
})
})
describe('options.merge', () => {
it('should merge an existing value with the given value', () => {
const o = { a: { b: { c: 'd' } } }
set(o, 'a.b', { y: 'z' }, { merge: true })
assert.deepStrictEqual(o.a.b, { c: 'd', y: 'z' })
const obj = { foo: { bar: { baz: 'qux' } } }
set(obj, 'foo.bar.fez', 'zzz', { merge: true })
assert.deepStrictEqual(obj, { foo: { bar: { baz: 'qux', fez: 'zzz' } } })
})
it('should update an object by merging values', () => {
const o = {}
set(o, 'a', { b: 'c' })
set(o, 'a', { c: 'd' }, { merge: true })
assert.deepStrictEqual(o, { a: { b: 'c', c: 'd' } })
set(o, 'a', 'b')
assert.strictEqual(o.a, 'b')
})
})
describe('options.separator', () => {
it('should allow a custom separator', () => {
const o = {}
set(o, 'a/b/c/d/e', 'c', { separator: '/' })
assert.strictEqual(o.a.b.c.d.e, 'c')
})
it('should allow multiple custom separators', () => {
const o = {}
set(o, 'a/b;c/d.e', 'c', { separator: '/;.' })
assert.strictEqual(o.a.b.c.d.e, 'c')
})
it('should accept "delimiter" in place of "separator"', () => {
const o = {}
set(o, 'a/b;c/d.e', 'c', { delimiter: '/;.' })
assert.strictEqual(o.a.b.c.d.e, 'c')
})
it('should split any string elements of an array path', () => {
const o = {}
const key = Symbol('key-1')
set(o, ['z.b', key, 'c'], 'd')
assert.strictEqual(o.z.b[key].c, 'd')
})
it('should not split any string elements of an array path if requested', () => {
const o = {}
const key = Symbol('key-1')
set(o, ['a.b', key, 'c'], 'd', { elSplit: false })
assert.strictEqual(o['a.b'][key].c, 'd')
})
})
describe('options.split', () => {
it('should use a custom function to split on , not .', () => {
const o = {}
const splitfn = (path) => {
return path.split(',')
}
set(o, 'a,b.c.d,e', 'c', { split: splitfn })
assert.strictEqual(o.a['b.c.d'].e, 'c')
})
})