├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── src ├── index.d.ts ├── merge.d.ts ├── index.js └── merge.js ├── bench ├── package.json ├── readme.md ├── mutable.js └── immutable.js ├── .editorconfig ├── test ├── suites │ ├── basics.js │ ├── preserve.js │ ├── assigns.js │ ├── arrays.js │ ├── objects.js │ └── pollution.js ├── index.js └── merge.js ├── license ├── package.json └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /index.d.ts 8 | /merge 9 | /dist 10 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export function dset(obj: T, keys: string | ArrayLike, value: V): void; 2 | -------------------------------------------------------------------------------- /src/merge.d.ts: -------------------------------------------------------------------------------- 1 | export function merge(foo: any, bar: any): any; // TODO 2 | export function dset(obj: T, keys: string | ArrayLike, value: V): void; 3 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "benchmark": "2.1.4", 5 | "clean-set": "1.1.2", 6 | "deep-set": "1.0.1", 7 | "klona": "2.0.4", 8 | "lodash": "4.17.20", 9 | "set-value": "3.0.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export function dset(obj, keys, val) { 2 | keys.split && (keys=keys.split('.')); 3 | var i=0, l=keys.length, t=obj, x, k; 4 | while (i < l) { 5 | k = ''+keys[i++]; 6 | if (k === '__proto__' || k === 'constructor' || k === 'prototype') break; 7 | t = t[k] = (i === l) ? val : (typeof(x=t[k])===typeof(keys)) ? x : (keys[i]*0 !== 0 || !!~(''+keys[i]).indexOf('.')) ? {} : []; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/suites/basics.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | export default function (dset) { 5 | const basics = suite('basics'); 6 | 7 | basics('should not give return value', () => { 8 | let output = dset({}, 'c', 3); // add c 9 | assert.is(output, undefined); 10 | }); 11 | 12 | basics('should mutate original object', () => { 13 | let item = { foo: 1 }; 14 | dset(item, 'bar', 123); 15 | assert.ok(item === item); 16 | assert.equal(item, { 17 | foo: 1, 18 | bar: 123 19 | }); 20 | }); 21 | 22 | basics.run(); 23 | } 24 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { dset } from '../src'; 4 | 5 | import arrays from './suites/arrays'; 6 | import assigns from './suites/assigns'; 7 | import pollution from './suites/pollution'; 8 | import preserve from './suites/preserve'; 9 | import objects from './suites/objects'; 10 | import basics from './suites/basics'; 11 | 12 | // --- 13 | 14 | const API = suite('API'); 15 | 16 | API('should export a function', () => { 17 | assert.type(dset, 'function'); 18 | }); 19 | 20 | API.run(); 21 | 22 | // --- 23 | 24 | basics(dset); 25 | assigns(dset); 26 | preserve(dset); 27 | pollution(dset); 28 | objects(dset); 29 | arrays(dset); 30 | -------------------------------------------------------------------------------- /src/merge.js: -------------------------------------------------------------------------------- 1 | export function merge(a, b, k) { 2 | if (typeof a === 'object' && typeof b === 'object')  { 3 | if (Array.isArray(a) && Array.isArray(b)) { 4 | for (k=0; k < b.length; k++) { 5 | a[k] = merge(a[k], b[k]); 6 | } 7 | } else { 8 | for (k in b) { 9 | if (k === '__proto__' || k === 'constructor' || k === 'prototype') break; 10 | a[k] = merge(a[k], b[k]); 11 | } 12 | } 13 | return a; 14 | } 15 | return b; 16 | } 17 | 18 | export function dset(obj, keys, val) { 19 | keys.split && (keys=keys.split('.')); 20 | var i=0, l=keys.length, t=obj, x, k; 21 | while (i < l) { 22 | k = ''+keys[i++]; 23 | if (k === '__proto__' || k === 'constructor' || k === 'prototype') break; 24 | t = t[k] = (i === l) ? merge(t[k],val) : (typeof(x=t[k])===typeof keys) ? x : (keys[i]*0 !== 0 || !!~(''+keys[i]).indexOf('.')) ? {} : []; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bench/readme.md: -------------------------------------------------------------------------------- 1 | ## Benchmarks – Node.js 2 | 3 | Below are the results while running this directory's suites on my machine with Node.js `v10.13.0`: 4 | 5 | #### Mutable 6 | 7 | > This is the default (and only) behavior 8 | 9 | ``` 10 | Validation: 11 | ✔ deep-set 12 | ✔ set-value 13 | ✔ lodash/set 14 | ✔ dset 15 | 16 | Benchmark: 17 | deep-set x 1,545,526 ops/sec ±1.49% (89 runs sampled) 18 | set-value x 1,704,871 ops/sec ±2.81% (92 runs sampled) 19 | lodash/set x 995,789 ops/sec ±1.66% (91 runs sampled) 20 | dset x 1,757,022 ops/sec ±0.12% (97 runs sampled) 21 | ``` 22 | 23 | #### Immutable 24 | 25 | > This combines `dset` with `klona`, as seen in the main README example 26 | 27 | ``` 28 | Validation: 29 | ✔ clean-set 30 | ✔ dset-klona 31 | 32 | Benchmark: 33 | clean-set x 2,631,929 ops/sec ±2.88% (89 runs sampled) 34 | dset-klona x 1,950,732 ops/sec ±2.11% (92 runs sampled) 35 | ``` 36 | -------------------------------------------------------------------------------- /bench/mutable.js: -------------------------------------------------------------------------------- 1 | const assert = require('uvu/assert'); 2 | const { Suite } = require('benchmark'); 3 | 4 | const contenders = { 5 | 'deep-set': require('deep-set'), 6 | 'set-value': require('set-value'), 7 | 'lodash/set': require('lodash/set'), 8 | 'dset': require('../dist').dset, 9 | }; 10 | 11 | console.log('Validation: '); 12 | Object.keys(contenders).forEach(name => { 13 | try { 14 | const input = {}; 15 | contenders[name](input, 'x.y.z', 'foobar'); 16 | assert.equal(input, { 17 | x: { 18 | y: { 19 | z: 'foobar' 20 | } 21 | } 22 | }); 23 | 24 | console.log(' ✔', name); 25 | } catch (err) { 26 | console.log(' ✘', name, `(FAILED)`); 27 | } 28 | }); 29 | 30 | 31 | console.log('\nBenchmark:'); 32 | const onCycle = e => console.log(' ' + e.target); 33 | const bench = new Suite({ onCycle }); 34 | 35 | Object.keys(contenders).forEach(name => { 36 | bench.add(name + ' '.repeat(12 - name.length), () => { 37 | contenders[name]({}, 'x.y.z', 'foobar'); 38 | contenders[name]({}, 'x.a.b.c', 'howdy'); 39 | }); 40 | }); 41 | 42 | bench.run(); 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12, 14] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | 18 | - name: Install 19 | run: npm install 20 | 21 | - name: (coverage) Install 22 | if: matrix.nodejs >= 14 23 | run: npm install -g c8 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Test 29 | run: npm test 30 | if: matrix.nodejs < 14 31 | 32 | - name: (coverage) Test 33 | run: c8 --include=src npm test 34 | if: matrix.nodejs >= 14 35 | 36 | - name: (coverage) Report 37 | if: matrix.nodejs >= 14 38 | run: | 39 | c8 report --reporter=text-lcov > coverage.lcov 40 | bash <(curl -s https://codecov.io/bash) 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/suites/preserve.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | export default function (dset) { 5 | const preserves = suite('preserves'); 6 | 7 | preserves('should preserve existing object structure', () => { 8 | let input = { 9 | a: { 10 | b: { 11 | c: 123 12 | } 13 | } 14 | }; 15 | 16 | dset(input, 'a.b.x.y', 456); 17 | 18 | assert.equal(input, { 19 | a: { 20 | b: { 21 | c: 123, 22 | x: { 23 | y:456 24 | } 25 | } 26 | } 27 | }); 28 | }); 29 | 30 | preserves('should overwrite existing non-object values as object', () => { 31 | let input = { 32 | a: { 33 | b: 123 34 | } 35 | }; 36 | 37 | dset(input, 'a.b.c', 'hello'); 38 | 39 | assert.equal(input, { 40 | a: { 41 | b: { 42 | c: 'hello' 43 | } 44 | } 45 | }); 46 | }); 47 | 48 | preserves('should preserve existing object tree w/ array value', () => { 49 | let input = { 50 | a: { 51 | b: { 52 | c: 123, 53 | d: { 54 | e: 5 55 | } 56 | } 57 | } 58 | }; 59 | 60 | dset(input, 'a.b.d.z', [1,2,3,4]); 61 | 62 | assert.equal(input.a.b.d, { 63 | e: 5, 64 | z: [1,2,3,4] 65 | }); 66 | }); 67 | 68 | preserves.run(); 69 | } 70 | -------------------------------------------------------------------------------- /bench/immutable.js: -------------------------------------------------------------------------------- 1 | const assert = require('uvu/assert'); 2 | const { Suite } = require('benchmark'); 3 | const { klona } = require('klona/json'); 4 | const { dset } = require('../dist'); 5 | 6 | const contenders = { 7 | 'clean-set': require('clean-set'), 8 | 'dset-klona': function (obj, key, val) { 9 | let copy = klona(obj); 10 | dset(copy, key, val); 11 | return copy; 12 | } 13 | }; 14 | 15 | console.log('Validation: '); 16 | Object.keys(contenders).forEach(name => { 17 | try { 18 | const input = {}; 19 | const output = contenders[name](input, 'x.y.z', 'foobar'); 20 | 21 | assert.is.not(output === input, 'new object'); 22 | assert.equal(output, { 23 | x: { 24 | y: { 25 | z: 'foobar' 26 | } 27 | } 28 | }, 'expected output'); 29 | 30 | input.foo = 'bar'; 31 | assert.is.not(output.foo, 'bar', 'detached clone'); 32 | 33 | console.log(' ✔', name); 34 | } catch (err) { 35 | console.log(' ✘', name, `(FAILED @ "${err.message}")`); 36 | } 37 | }); 38 | 39 | 40 | console.log('\nBenchmark:'); 41 | const onCycle = e => console.log(' ' + e.target); 42 | const bench = new Suite({ onCycle }); 43 | 44 | Object.keys(contenders).forEach(name => { 45 | bench.add(name + ' '.repeat(12 - name.length), () => { 46 | contenders[name]({}, 'x.y.z', 'foobar'); 47 | contenders[name]({}, 'x.a.b.c', 'howdy'); 48 | }); 49 | }); 50 | 51 | bench.run(); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dset", 3 | "version": "3.1.4", 4 | "repository": "lukeed/dset", 5 | "description": "A tiny (194B) utility for safely writing deep Object values~!", 6 | "unpkg": "dist/index.min.js", 7 | "umd:main": "dist/index.min.js", 8 | "module": "dist/index.mjs", 9 | "main": "dist/index.js", 10 | "types": "index.d.ts", 11 | "license": "MIT", 12 | "exports": { 13 | ".": { 14 | "types": "./index.d.ts", 15 | "import": "./dist/index.mjs", 16 | "require": "./dist/index.js" 17 | }, 18 | "./merge": { 19 | "types": "./merge/index.d.ts", 20 | "import": "./merge/index.mjs", 21 | "require": "./merge/index.js" 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "author": { 26 | "name": "Luke Edwards", 27 | "email": "luke.edwards05@gmail.com", 28 | "url": "https://lukeed.com" 29 | }, 30 | "engines": { 31 | "node": ">=4" 32 | }, 33 | "scripts": { 34 | "build": "bundt", 35 | "test": "uvu test -r esm -i suites" 36 | }, 37 | "files": [ 38 | "*.d.ts", 39 | "merge", 40 | "dist" 41 | ], 42 | "modes": { 43 | "merge": "src/merge.js", 44 | "default": "src/index.js" 45 | }, 46 | "keywords": [ 47 | "deepset", 48 | "values", 49 | "object", 50 | "write", 51 | "deep", 52 | "safe", 53 | "set" 54 | ], 55 | "devDependencies": { 56 | "bundt": "1.1.2", 57 | "esm": "3.2.25", 58 | "uvu": "0.5.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/suites/assigns.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | export default function (dset) { 5 | const assigns = suite('assigns'); 6 | 7 | assigns('should add value to key path :: shallow :: string', () => { 8 | let input = {}; 9 | dset(input, 'abc', 123); 10 | assert.equal(input, { abc: 123 }); 11 | }); 12 | 13 | assigns('should add value to key path :: shallow :: array', () => { 14 | let input = {}; 15 | dset(input, ['abc'], 123); 16 | assert.equal(input, { abc: 123 }); 17 | }); 18 | 19 | assigns('should add value to key path :: nested :: string', () => { 20 | let input = {}; 21 | dset(input, 'a.b.c', 123); 22 | assert.equal(input, { 23 | a: { 24 | b: { 25 | c: 123 26 | } 27 | } 28 | }); 29 | }); 30 | 31 | assigns('should add value to key path :: nested :: array', () => { 32 | let input = {}; 33 | dset(input, ['a', 'b', 'c'], 123); 34 | assert.equal(input, { 35 | a: { 36 | b: { 37 | c: 123 38 | } 39 | } 40 | }); 41 | }); 42 | 43 | assigns('should create Array via integer key :: string', () => { 44 | let input = {}; 45 | dset(input, ['foo', '0'], 123); 46 | assert.instance(input.foo, Array); 47 | assert.equal(input, { 48 | foo: [123] 49 | }) 50 | }); 51 | 52 | assigns('should create Array via integer key :: number', () => { 53 | let input = {}; 54 | dset(input, ['foo', 0], 123); 55 | assert.instance(input.foo, Array); 56 | assert.equal(input, { 57 | foo: [123] 58 | }) 59 | }); 60 | 61 | assigns.run(); 62 | } 63 | -------------------------------------------------------------------------------- /test/suites/arrays.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | export default function (dset) { 5 | const arrays = suite('arrays'); 6 | 7 | arrays('should create array instead of object via numeric key :: simple', () => { 8 | let input = { a: 1 }; 9 | dset(input, 'e.0', 2); 10 | assert.instance(input.e, Array); 11 | assert.is(input.e[0], 2); 12 | assert.equal(input, { 13 | a: 1, 14 | e: [2] 15 | }); 16 | }); 17 | 18 | arrays('should create array instead of object via numeric key :: nested', () => { 19 | let input = { a: 1 }; 20 | dset(input, 'e.0.0', 123); 21 | assert.instance(input.e, Array); 22 | assert.is(input.e[0][0], 123); 23 | assert.equal(input, { 24 | a: 1, 25 | e: [ [123] ] 26 | }); 27 | }); 28 | 29 | arrays('should be able to create object inside of array', () => { 30 | let input = {}; 31 | dset(input, ['x', '0', 'z'], 123); 32 | assert.instance(input.x, Array); 33 | assert.equal(input, { 34 | x: [{ z:123 }] 35 | }); 36 | }); 37 | 38 | arrays('should create arrays with hole(s) if needed', () => { 39 | let input = {}; 40 | dset(input, ['x', '1', 'z'], 123); 41 | assert.instance(input.x, Array); 42 | assert.equal(input, { 43 | x: [, { z:123 }] 44 | }); 45 | }); 46 | 47 | arrays('should create object from decimal-like key :: array :: zero :: string', () => { 48 | let input = {}; 49 | dset(input, ['x', '10.0', 'z'], 123); 50 | assert.not.instance(input.x, Array); 51 | assert.equal(input, { 52 | x: { 53 | '10.0': { 54 | z: 123 55 | } 56 | } 57 | }); 58 | }); 59 | 60 | arrays('should create array from decimal-like key :: array :: zero :: number', () => { 61 | let input = {}; 62 | dset(input, ['x', 10.0, 'z'], 123); 63 | assert.instance(input.x, Array); 64 | 65 | let x = Array(10); 66 | x.push({ z: 123 }); 67 | assert.equal(input, { x }); 68 | }); 69 | 70 | arrays('should create object from decimal-like key :: array :: nonzero', () => { 71 | let input = {}; 72 | dset(input, ['x', '10.2', 'z'], 123); 73 | assert.not.instance(input.x, Array); 74 | assert.equal(input, { 75 | x: { 76 | '10.2': { 77 | z: 123 78 | } 79 | } 80 | }); 81 | }); 82 | 83 | arrays.run(); 84 | } 85 | -------------------------------------------------------------------------------- /test/suites/objects.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | const prepare = x => ({ input: x, copy: JSON.parse(JSON.stringify(x)) }); 5 | 6 | export default function (dset, isMerge) { 7 | const objects = suite('objects'); 8 | const verb = isMerge ? 'merge' : 'overwrite'; 9 | 10 | objects(`should ${verb} existing object value :: simple`, () => { 11 | let { input } = prepare({ 12 | hello: { a: 1 } 13 | }); 14 | 15 | dset(input, 'hello', { foo: 123 }); 16 | 17 | if (isMerge) { 18 | assert.equal(input, { 19 | hello: { 20 | a: 1, 21 | foo: 123, 22 | } 23 | }); 24 | } else { 25 | assert.equal(input, { 26 | hello: { foo: 123 } 27 | }); 28 | } 29 | }); 30 | 31 | objects(`should ${verb} existing object value :: nested`, () => { 32 | let { input, copy } = prepare({ 33 | a: { 34 | b: { 35 | c: 123 36 | } 37 | } 38 | }); 39 | 40 | dset(input, 'a.b', { foo: 123 }); 41 | 42 | if (isMerge) { 43 | Object.assign(copy.a.b, { foo: 123 }); 44 | } else { 45 | copy.a.b = { foo: 123 }; 46 | } 47 | 48 | assert.equal(input, copy); 49 | }); 50 | 51 | objects(`should ${verb} existing array value :: simple`, () => { 52 | let { input } = prepare([ 53 | { foo: 1 }, 54 | ]); 55 | 56 | dset(input, '0', { bar: 2 }); 57 | 58 | if (isMerge) { 59 | assert.equal(input, [ 60 | { foo: 1, bar: 2 } 61 | ]); 62 | } else { 63 | assert.equal(input, [ 64 | { bar: 2 } 65 | ]); 66 | } 67 | }); 68 | 69 | objects(`should ${verb} existing array value :: nested`, () => { 70 | let { input } = prepare([ 71 | { name: 'bob', age: 56, friends: ['foobar'] }, 72 | { name: 'alice', age: 47, friends: ['mary'] }, 73 | ]); 74 | 75 | dset(input, '0', { age: 57, friends: ['alice', 'mary'] }); 76 | dset(input, '1', { friends: ['bob'] }); 77 | dset(input, '2', { name: 'mary', age: 49, friends: ['bob'] }); 78 | 79 | if (isMerge) { 80 | assert.equal(input, [ 81 | { name: 'bob', age: 57, friends: ['alice', 'mary'] }, 82 | { name: 'alice', age: 47, friends: ['bob'] }, 83 | { name: 'mary', age: 49, friends: ['bob'] }, 84 | ]); 85 | } else { 86 | assert.equal(input, [ 87 | { age: 57, friends: ['alice', 'mary'] }, 88 | { friends: ['bob'] }, 89 | { name: 'mary', age: 49, friends: ['bob'] }, 90 | ]); 91 | } 92 | }); 93 | 94 | objects.run(); 95 | } 96 | -------------------------------------------------------------------------------- /test/suites/pollution.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | export default function (dset) { 5 | const pollution = suite('pollution'); 6 | 7 | pollution('should protect against "__proto__" assignment', () => { 8 | let input = { abc: 123 }; 9 | let before = input.__proto__; 10 | dset(input, '__proto__.hello', 123); 11 | 12 | assert.equal(input.__proto__, before); 13 | assert.equal(input, { 14 | abc: 123 15 | }); 16 | 17 | assert.is.not({}.hello, 123); 18 | assert.is.not((new Object).hello, 123); 19 | assert.is.not(Object.create(null).hello, 123); 20 | }); 21 | 22 | pollution('should protect against "__proto__" assignment :: nested', () => { 23 | let input = { abc: 123 }; 24 | let before = input.__proto__; 25 | dset(input, ['xyz', '__proto__', 'hello'], 123); 26 | 27 | assert.equal(input.__proto__, before); 28 | assert.equal(input, { 29 | abc: 123, 30 | xyz: { 31 | // empty 32 | } 33 | }); 34 | 35 | assert.is({}.hello, undefined); 36 | assert.is(input.hello, undefined); 37 | assert.is((new Object).hello, undefined); 38 | assert.is(Object.create(null).hello, undefined); 39 | }); 40 | 41 | pollution('should protect against ["__proto__"] assignment :: implicit string', () => { 42 | let input = { abc: 123 }; 43 | let before = input.__proto__; 44 | 45 | dset(input, [['__proto__'], 'polluted'], true); 46 | 47 | assert.equal(input.__proto__, before); 48 | assert.equal(input, { abc: 123 }); 49 | 50 | assert.is({}.polluted, undefined); 51 | assert.is(input.polluted, undefined); 52 | assert.is((new Object).polluted, undefined); 53 | assert.is(Object.create(null).polluted, undefined); 54 | }); 55 | 56 | 57 | 58 | pollution('should ignore "prototype" assignment', () => { 59 | let input = { a: 123 }; 60 | dset(input, 'a.prototype.hello', 'world'); 61 | 62 | assert.is(input.a.prototype, undefined); 63 | assert.is(input.a.hello, undefined); 64 | 65 | assert.equal(input, { 66 | a: { 67 | // converted, then aborted 68 | } 69 | }); 70 | 71 | assert.is( 72 | JSON.stringify(input), 73 | '{"a":{}}' 74 | ); 75 | }); 76 | 77 | pollution('should ignore "constructor" assignment :: direct', () => { 78 | let input = { a: 123 }; 79 | 80 | function Custom() { 81 | // 82 | } 83 | 84 | dset(input, 'a.constructor', Custom); 85 | assert.is.not(input.a.constructor, Custom); 86 | assert.not.instance(input.a, Custom); 87 | 88 | assert.instance(input.a.constructor, Object, '~> 123 -> {}'); 89 | assert.is(input.a.hasOwnProperty('constructor'), false); 90 | assert.equal(input, { a: {} }); 91 | }); 92 | 93 | pollution('should ignore "constructor" assignment :: nested', () => { 94 | let input = {}; 95 | 96 | dset(input, 'constructor.prototype.hello', 'world'); 97 | assert.is(input.hasOwnProperty('constructor'), false); 98 | assert.is(input.hasOwnProperty('hello'), false); 99 | 100 | assert.equal(input, { 101 | // empty 102 | }); 103 | }); 104 | 105 | // Test for CVE-2022-25645 - CWE-1321 106 | pollution('should ignore JSON.parse crafted object with "__proto__" key', () => { 107 | let a = { b: { c: 1 } }; 108 | assert.is(a.polluted, undefined); 109 | assert.is({}.polluted, undefined); 110 | dset(a, "b", JSON.parse('{"__proto__":{"polluted":"Yes!"}}')); 111 | assert.is(a.polluted, undefined); 112 | assert.is({}.polluted, undefined); 113 | }); 114 | 115 | pollution.run(); 116 | } 117 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # dset [![CI](https://github.com/lukeed/dset/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/lukeed/dset/actions) [![codecov](https://badgen.net/codecov/c/github/lukeed/dset)](https://codecov.io/gh/lukeed/dset) 2 | 3 | > A tiny (197B) utility for safely writing deep Object values~! 4 | 5 | For _accessing_ deep object properties, please see [`dlv`](https://github.com/developit/dlv). 6 | 7 | > **Using GraphQL?** You may want `dset/merge` – see [Merging](#merging) for more info. 8 | 9 | ## Install 10 | 11 | ```sh 12 | $ npm install --save dset 13 | ``` 14 | 15 | ## Modes 16 | 17 | There are two "versions" of `dset` available: 18 | 19 | #### `dset` 20 | > **Size (gzip):** 197 bytes
21 | > **Availability:** [CommonJS](https://unpkg.com/dset/dist/index.js), [ES Module](https://unpkg.com/dset/dist/index.mjs), [UMD](https://unpkg.com/dset/dist/index.min.js) 22 | 23 | ```js 24 | import { dset } from 'dset'; 25 | ``` 26 | 27 | #### `dset/merge` 28 | > **Size (gzip):** 307 bytes
29 | > **Availability:** [CommonJS](https://unpkg.com/dset/merge/index.js), [ES Module](https://unpkg.com/dset/merge/index.mjs), [UMD](https://unpkg.com/dset/merge/index.min.js) 30 | 31 | ```js 32 | import { dset } from 'dset/merge'; 33 | ``` 34 | 35 | 36 | ## Usage 37 | 38 | ```js 39 | import { dset } from 'dset'; 40 | 41 | let foo = { abc: 123 }; 42 | dset(foo, 'foo.bar', 'hello'); 43 | // or: dset(foo, ['foo', 'bar'], 'hello'); 44 | console.log(foo); 45 | //=> { 46 | //=> abc: 123, 47 | //=> foo: { bar: 'hello' }, 48 | //=> } 49 | 50 | dset(foo, 'abc.hello', 'world'); 51 | // or: dset(foo, ['abc', 'hello'], 'world'); 52 | console.log(foo); 53 | //=> { 54 | //=> abc: { hello: 'world' }, 55 | //=> foo: { bar: 'hello' }, 56 | //=> } 57 | 58 | let bar = { a: { x: 7 }, b:[1, 2, 3] }; 59 | dset(bar, 'b.1', 999); 60 | // or: dset(bar, ['b', 1], 999); 61 | // or: dset(bar, ['b', '1'], 999); 62 | console.log(bar); 63 | //=> { 64 | //=> a: { x: 7 }, 65 | //=> b: [1, 999, 3], 66 | //=> } 67 | 68 | dset(bar, 'a.y.0', 8); 69 | // or: dset(bar, ['a', 'y', 0], 8); 70 | // or: dset(bar, ['a', 'y', '0'], 8); 71 | console.log(bar); 72 | //=> { 73 | //=> a: { 74 | //=> x: 7, 75 | //=> y: [8], 76 | //=> }, 77 | //=> b: [1, 999, 3], 78 | //=> } 79 | 80 | let baz = {}; 81 | dset(baz, 'a.0.b.0', 1); 82 | dset(baz, 'a.0.b.1', 2); 83 | console.log(baz); 84 | //=> { 85 | //=> a: [{ b: [1, 2] }] 86 | //=> } 87 | ``` 88 | 89 | ## Merging 90 | 91 | The main/default `dset` module forcibly writes values at the assigned key-path. However, in some cases, you may prefer to _merge_ values at the key-path. For example, when using [GraphQL's `@stream` and `@defer` directives](https://foundation.graphql.org/news/2020/12/08/improving-latency-with-defer-and-stream-directives/), you will need to merge the response chunks into a single object/list. This is why `dset/merge` exists~! 92 | 93 | Below is a quick illustration of the difference between `dset` and `dset/merge`: 94 | 95 | ```js 96 | let input = { 97 | hello: { 98 | abc: 123 99 | } 100 | }; 101 | 102 | dset(input, 'hello', { world: 123 }); 103 | console.log(input); 104 | 105 | // via `dset` 106 | //=> { 107 | //=> hello: { 108 | //=> world: 123 109 | //=> } 110 | //=> } 111 | 112 | // via `dset/merge` 113 | //=> { 114 | //=> hello: { 115 | //=> abc: 123, 116 | //=> world: 123 117 | //=> } 118 | //=> } 119 | ``` 120 | 121 | 122 | ## Immutability 123 | 124 | As shown in the examples above, all `dset` interactions mutate the source object. 125 | 126 | If you need immutable writes, please visit [`clean-set`](https://github.com/fwilkerson/clean-set) (182B).
127 | Alternatively, you may pair `dset` with [`klona`](https://github.com/lukeed/klona), a 366B utility to clone your source(s). Here's an example pairing: 128 | 129 | ```js 130 | import { dset } from 'dset'; 131 | import { klona } from 'klona'; 132 | 133 | export function deepset(obj, path, val) { 134 | let copy = klona(obj); 135 | dset(copy, path, val); 136 | return copy; 137 | } 138 | ``` 139 | 140 | 141 | ## API 142 | 143 | ### dset(obj, path, val) 144 | 145 | Returns: `void` 146 | 147 | #### obj 148 | 149 | Type: `Object` 150 | 151 | The Object to traverse & mutate with a value. 152 | 153 | #### path 154 | 155 | Type: `String` or `Array` 156 | 157 | The key path that should receive the value. May be in `x.y.z` or `['x', 'y', 'z']` formats. 158 | 159 | > **Note:** Please be aware that only the _last_ key actually receives the value! 160 | 161 | > **Important:** New Objects are created at each segment if there is not an existing structure.
However, when integers are encounted, Arrays are created instead! 162 | 163 | #### value 164 | 165 | Type: `Any` 166 | 167 | The value that you want to set. Can be of any type! 168 | 169 | 170 | ## Benchmarks 171 | 172 | For benchmarks and full results, check out the [`bench`](/bench) directory! 173 | 174 | ``` 175 | # Node 10.13.0 176 | 177 | Validation: 178 | ✔ set-value 179 | ✔ lodash/set 180 | ✔ dset 181 | 182 | Benchmark: 183 | set-value x 1,701,821 ops/sec ±1.81% (93 runs sampled) 184 | lodash/set x 975,530 ops/sec ±0.96% (91 runs sampled) 185 | dset x 1,797,922 ops/sec ±0.32% (94 runs sampled) 186 | ``` 187 | 188 | 189 | ## Related 190 | 191 | - [dlv](https://github.com/developit/dlv) - safely read from deep properties in 120 bytes 192 | - [dequal](https://github.com/lukeed/dequal) - safely check for deep equality in 247 bytes 193 | - [klona](https://github.com/lukeed/klona) - quickly "deep clone" data in 200 to 330 bytes 194 | - [clean-set](https://github.com/fwilkerson/clean-set) - fast, immutable version of `dset` in 182 bytes 195 | 196 | 197 | ## License 198 | 199 | MIT © [Luke Edwards](https://lukeed.com) 200 | -------------------------------------------------------------------------------- /test/merge.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { dset, merge } from '../src/merge'; 4 | 5 | import arrays from './suites/arrays'; 6 | import assigns from './suites/assigns'; 7 | import pollution from './suites/pollution'; 8 | import preserve from './suites/preserve'; 9 | import objects from './suites/objects'; 10 | import basics from './suites/basics'; 11 | 12 | // --- 13 | 14 | const API = suite('API'); 15 | 16 | API('should export `dset` function', () => { 17 | assert.type(dset, 'function'); 18 | }); 19 | 20 | API('should export `merge` function', () => { 21 | assert.type(merge, 'function'); 22 | }); 23 | 24 | API.run(); 25 | 26 | // --- 27 | 28 | basics(dset); 29 | assigns(dset); 30 | preserve(dset); 31 | pollution(dset); 32 | objects(dset, true); 33 | arrays(dset); 34 | 35 | // --- 36 | 37 | const merges = suite('merge()'); 38 | 39 | merges('should return `b` when either non-object', () => { 40 | let output = merge(123, { b: 2 }); 41 | assert.equal(output, { b: 2 }); 42 | 43 | output = merge(123, null); 44 | assert.equal(output, null); 45 | 46 | output = merge(123, undefined); 47 | assert.equal(output, undefined); 48 | 49 | output = merge(123, 456); 50 | assert.equal(output, 456); 51 | 52 | output = merge({ a: 1 }, 456); 53 | assert.is(output, 456); 54 | 55 | output = merge(123, { b: 2 }); 56 | assert.equal(output, { b: 2 }); 57 | }); 58 | 59 | merges('should merge objects together :: simple', () => { 60 | let input = { foo: 123 }; 61 | let output = merge(input, { bar: 456 }); 62 | assert.equal(input, { foo: 123, bar: 456 }); 63 | assert.equal(input, output); 64 | }); 65 | 66 | merges('should merge objects together :: nested', () => { 67 | let input = { 68 | a: { 69 | b: { 70 | c: 'hi' 71 | } 72 | } 73 | }; 74 | 75 | let output = merge(input, { 76 | a: { 77 | b: { 78 | d: 'hello' 79 | } 80 | } 81 | }); 82 | 83 | assert.equal(input, output); 84 | 85 | // mutates 86 | assert.equal(input, { 87 | a: { 88 | b: { 89 | c: 'hi', 90 | d: 'hello', 91 | } 92 | } 93 | }); 94 | }); 95 | 96 | merges('should merge arrays together :: simple', () => { 97 | let input = ['foo', /*hole*/, 'baz']; 98 | let output = merge(input, ['hello', 'world']); 99 | assert.equal(input, ['hello', 'world', 'baz']); 100 | assert.equal(input, output); 101 | }); 102 | 103 | merges('should merge arrays together :: nested', () => { 104 | let input = [ 105 | { name: 'bob', age: 56, friends: ['alice'] }, 106 | { name: 'alice', age: 47, friends: ['mary'] }, 107 | ]; 108 | 109 | let output = merge(input, [ 110 | { age: 57, friends: ['alice', 'mary'] }, 111 | { friends: ['bob'] }, 112 | { name: 'mary', age: 49, friends: ['bob'] }, 113 | ]); 114 | 115 | assert.equal(input, [ 116 | { name: 'bob', age: 57, friends: ['alice', 'mary'] }, 117 | { name: 'alice', age: 47, friends: ['bob'] }, 118 | { name: 'mary', age: 49, friends: ['bob'] }, 119 | ]); 120 | 121 | assert.equal(input, output); 122 | }); 123 | 124 | merges.run(); 125 | 126 | // --- 127 | 128 | const dsets = suite('dset()'); 129 | 130 | dsets('should merge object at path notation', () => { 131 | let input = { 132 | a: { 133 | b: { c: 'hi' } 134 | } 135 | }; 136 | 137 | dset(input, 'a.b.c', { d: 'howdy' }); 138 | 139 | assert.equal(input, { 140 | a: { 141 | b: { 142 | c: { 143 | d: 'howdy' 144 | } 145 | } 146 | } 147 | }); 148 | 149 | dset(input, 'a.b.c', { e: 'partner' }); 150 | 151 | assert.equal(input, { 152 | a: { 153 | b: { 154 | c: { 155 | d: 'howdy', 156 | e: 'partner', 157 | } 158 | } 159 | } 160 | }); 161 | }); 162 | 163 | dsets('should merge array at path notation', () => { 164 | let input = [ 165 | [{ foo: [1, 2, 3]}] 166 | ]; 167 | 168 | dset(input, '0.0.foo.0', 9); 169 | assert.equal(input, [ 170 | [{ foo: [9, 2, 3]}] 171 | ]); 172 | 173 | dset(input, '1.0.foo', [7, 8, 9]); 174 | assert.equal(input, [ 175 | [{ foo: [9, 2, 3]}], 176 | [{ foo: [7, 8, 9]}], 177 | ]); 178 | }); 179 | 180 | dsets.run(); 181 | 182 | // --- 183 | 184 | const kitchen = suite('kitchensink', { 185 | input: {} 186 | }); 187 | 188 | kitchen('greeting.0', ctx => { 189 | dset(ctx.input, 'greeting.0', { value: 'hello' }); 190 | 191 | assert.equal(ctx.input, { 192 | greeting: [ 193 | { value: 'hello' } 194 | ] 195 | }); 196 | }); 197 | 198 | kitchen('greeting.1', ctx => { 199 | dset(ctx.input, 'greeting.1', { value: 'world' }); 200 | 201 | assert.equal(ctx.input, { 202 | greeting: [ 203 | { value: 'hello' }, 204 | { value: 'world' }, 205 | ] 206 | }); 207 | }); 208 | 209 | kitchen('user { fname }', ctx => { 210 | dset(ctx.input, 'user', { fname: 'luke' }); 211 | 212 | assert.equal(ctx.input, { 213 | greeting: [ 214 | { value: 'hello' }, 215 | { value: 'world' } 216 | ], 217 | user: { fname: 'luke' } 218 | }); 219 | }); 220 | 221 | kitchen('user { lname }', ctx => { 222 | dset(ctx.input, 'user', { lname: 'edwards' }); 223 | 224 | assert.equal(ctx.input, { 225 | greeting: [ 226 | { value: 'hello' }, 227 | { value: 'world' } 228 | ], 229 | user: { 230 | fname: 'luke', 231 | lname: 'edwards' 232 | } 233 | }); 234 | }); 235 | 236 | kitchen('user.friends.0 { fname }', ctx => { 237 | dset(ctx.input, 'user.friends.0', { fname: 'foobar' }); 238 | 239 | assert.equal(ctx.input, { 240 | greeting: [ 241 | { value: 'hello' }, 242 | { value: 'world' } 243 | ], 244 | user: { 245 | fname: 'luke', 246 | lname: 'edwards', 247 | friends: [ 248 | { fname: 'foobar' } 249 | ] 250 | } 251 | }); 252 | }); 253 | 254 | kitchen('user.friends.0 { humanoid }', ctx => { 255 | dset(ctx.input, 'user.friends.0', { humanoid: false }); 256 | 257 | assert.equal(ctx.input, { 258 | greeting: [ 259 | { value: 'hello' }, 260 | { value: 'world' } 261 | ], 262 | user: { 263 | fname: 'luke', 264 | lname: 'edwards', 265 | friends: [ 266 | { 267 | fname: 'foobar', 268 | humanoid: false 269 | } 270 | ] 271 | } 272 | }); 273 | }); 274 | 275 | kitchen.run(); 276 | --------------------------------------------------------------------------------