├── .npmignore ├── .gitignore ├── test ├── eslint.spec.js ├── globals.js ├── .eslintrc ├── mocha.opts ├── dot-prop-immutable-toggle.spec.js ├── dot-prop-immutable-number.spec.js ├── dot-prop-immutable-merge.spec.js ├── examples.spec.js └── dot-prop-immutable.spec.js ├── .eslintrc ├── .travis.yml ├── index.d.ts ├── package.json ├── .github └── workflows │ └── codeql-analysis.yml ├── lib └── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .gitignore 3 | .eslintrc 4 | .travis.yml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /test/eslint.spec.js: -------------------------------------------------------------------------------- 1 | require('mocha-eslint')([ 2 | 'src', 3 | 'test' 4 | ]); -------------------------------------------------------------------------------- /test/globals.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | global.expect = chai.expect; 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@debitoor/eslint-config-debitoor", 3 | "env": { 4 | "node": true, 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 10 6 | - 12 7 | - lts/* # most recent lts 8 | - node # latest -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "expect": true 8 | } 9 | } -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/globals.js 2 | --reporter spec 3 | --ui bdd 4 | --recursive 5 | --colors 6 | --timeout 2000 7 | --slow 100 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface DotPropImmutable { 2 | get(source: I, path: number | string | (string | number)[], defaultValue?: any): T; 3 | set(source: I, path: number | string | (string | number)[], value: any): T; 4 | delete(source: I, path: number | string | (string | number)[]): Partial; 5 | toggle(source: I, path: number | string | (string | number)[]): T; 6 | merge(source: I, path: number | string | (string | number)[], value: any): T; 7 | } 8 | 9 | declare const dotPropImmutable: DotPropImmutable; 10 | 11 | export = dotPropImmutable; 12 | -------------------------------------------------------------------------------- /test/dot-prop-immutable-toggle.spec.js: -------------------------------------------------------------------------------- 1 | const dotProp = require('../lib'); 2 | 3 | describe('dot-prop-immutable.toggle.spec.js', () => { 4 | 5 | const arr = [1, { a: false }]; 6 | 7 | let result; 8 | describe('when have an array', () => { 9 | 10 | describe('toggle a value', () => { 11 | 12 | before(() => { 13 | result = dotProp.toggle(arr, '1.a'); 14 | }); 15 | 16 | 17 | it('should toggle prop', () => { 18 | expect(result).to.eql( 19 | [1, { a: true }]); 20 | }); 21 | 22 | it('invariant', arrInvariant); 23 | }); 24 | }); 25 | 26 | function arrInvariant() { 27 | expect(arr).to.eql( 28 | [1, { a: false }] 29 | ); 30 | } 31 | }); 32 | 33 | 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-prop-immutable", 3 | "version": "2.1.1", 4 | "description": "Immutable version of dot-prop with some extensions", 5 | "main": "lib/index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "postversion": "git push && git push --tags", 9 | "preversion": "npm test", 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/debitoor/dot-prop-immutable" 15 | }, 16 | "author": "Debitoor", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/debitoor/dot-prop-immutable/issues" 20 | }, 21 | "keywords": [ 22 | "immutable", 23 | "dot-prop", 24 | "react", 25 | "redux", 26 | "obj", 27 | "object", 28 | "prop", 29 | "property", 30 | "dot", 31 | "path", 32 | "get", 33 | "access", 34 | "notation" 35 | ], 36 | "homepage": "https://github.com/debitoor/dot-prop-immutable", 37 | "devDependencies": { 38 | "@debitoor/eslint-config-debitoor": "3.0.2", 39 | "chai": "4.2.0", 40 | "eslint": "6.8.0", 41 | "mocha": "6.2.0", 42 | "mocha-eslint": "5.0.0" 43 | }, 44 | "engines": { 45 | "node": ">=6.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/dot-prop-immutable-number.spec.js: -------------------------------------------------------------------------------- 1 | const dotProp = require('../lib'); 2 | 3 | describe('dot-prop-immutable.number.spec.js', () => { 4 | 5 | const arr = [1, { a: false }]; 6 | 7 | let result; 8 | describe('when have an array', () => { 9 | 10 | describe('when set prop using number as path', () => { 11 | 12 | before(() => { 13 | result = dotProp.set(arr, 1, 3); 14 | }); 15 | 16 | it('should replace prop', () => { 17 | expect(result).to.eql([ 18 | 1, 19 | 3 20 | ]); 21 | }); 22 | 23 | it('invariant', arrInvariant); 24 | }); 25 | 26 | 27 | describe('when get prop using number as path', () => { 28 | 29 | before(() => { 30 | result = dotProp.get(arr, 1); 31 | }); 32 | 33 | it('should get prop', () => { 34 | expect(result).to.eql({ a: false }); 35 | }); 36 | 37 | it('invariant', arrInvariant); 38 | }); 39 | 40 | describe('when delete prop using number as path', () => { 41 | 42 | before(() => { 43 | result = dotProp.delete(arr, 1); 44 | }); 45 | 46 | it('should delete prop', () => { 47 | expect(result).to.eql([ 48 | 1 49 | ]); 50 | }); 51 | it('invariant', arrInvariant); 52 | }); 53 | }); 54 | 55 | function arrInvariant() { 56 | expect(arr).to.eql( 57 | [1, { a: false }] 58 | ); 59 | } 60 | }); 61 | 62 | 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '27 8 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /test/dot-prop-immutable-merge.spec.js: -------------------------------------------------------------------------------- 1 | const dotProp = require('../lib'); 2 | 3 | describe('dot-prop-immutable.merge.spec.js', () => { 4 | 5 | const obj = { 6 | a: 1, 7 | b: { 8 | x: 1, 9 | y: 2 10 | }, 11 | c: [1, 2], 12 | d: null, 13 | 'b.x': 10 14 | }; 15 | 16 | const arr = [1, { a: [1, 2] }]; 17 | 18 | let result; 19 | describe('when have an object', () => { 20 | 21 | describe('merge an object value into object', () => { 22 | 23 | before(() => { 24 | result = dotProp.merge(obj, 'b', { z: 3 }); 25 | }); 26 | 27 | it('should merge prop', () => { 28 | expect(result).to.eql({ 29 | a: 1, 30 | b: { 31 | x: 1, 32 | y: 2, 33 | z: 3 34 | }, 35 | c: [1, 2], 36 | d: null, 37 | 'b.x': 10 38 | }); 39 | }); 40 | 41 | it('invariant', objInvariant); 42 | 43 | }); 44 | 45 | describe('merge an array value into array', () => { 46 | 47 | before(() => { 48 | result = dotProp.merge(obj, 'c', [3, 4]); 49 | }); 50 | 51 | it('should merge prop', () => { 52 | expect(result).to.eql({ 53 | a: 1, 54 | b: { 55 | x: 1, 56 | y: 2 57 | }, 58 | c: [1, 2, 3, 4], 59 | d: null, 60 | 'b.x': 10 61 | }); 62 | }); 63 | 64 | it('invariant', objInvariant); 65 | 66 | }); 67 | 68 | describe('merge an object value into null', () => { 69 | 70 | before(() => { 71 | result = dotProp.merge(obj, 'd', { foo: 'bar' }); 72 | }); 73 | 74 | it('should merge prop', () => { 75 | expect(result).to.eql({ 76 | a: 1, 77 | b: { 78 | x: 1, 79 | y: 2 80 | }, 81 | c: [1, 2], 82 | d: { foo: 'bar' }, 83 | 'b.x': 10 84 | }); 85 | }); 86 | 87 | it('invariant', objInvariant); 88 | 89 | }); 90 | 91 | describe('merge an object value into undefined', () => { 92 | 93 | before(() => { 94 | result = dotProp.merge(obj, 'z', { foo: 'bar' }); 95 | }); 96 | 97 | it('should merge prop', () => { 98 | expect(result).to.eql({ 99 | a: 1, 100 | b: { 101 | x: 1, 102 | y: 2 103 | }, 104 | c: [1, 2], 105 | d: null, 106 | z: { foo: 'bar' }, 107 | 'b.x': 10 108 | }); 109 | }); 110 | 111 | it('invariant', objInvariant); 112 | 113 | }); 114 | 115 | describe('when have an array', () => { 116 | it('invariant', arrInvariant); 117 | }); 118 | }); 119 | 120 | function objInvariant() { 121 | expect(obj).to.eql({ 122 | a: 1, 123 | b: { 124 | x: 1, 125 | y: 2 126 | }, 127 | c: [1, 2], 128 | d: null, 129 | 'b.x': 10 130 | }); 131 | } 132 | 133 | function arrInvariant() { 134 | expect(arr).to.eql( 135 | [1, { a: [1, 2] }] 136 | ); 137 | } 138 | }); 139 | 140 | 141 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set a value by a dot path. 3 | * @param obj The object to evaluate. 4 | * @param prop The path to be set. 5 | * @param value The value to set. 6 | */ 7 | function set(obj, prop, value) { 8 | prop = typeof prop === 'number' ? propToArray(prop.toString()) : typeof prop === 'string' ? propToArray(prop) : prop; 9 | 10 | const setPropImmutableRec = (obj, prop, value, i) => { 11 | let clone, head = prop[i]; 12 | 13 | if (prop.length > i) { 14 | if (Array.isArray(obj)) { 15 | head = getArrayIndex(head, obj); 16 | clone = obj.slice(); 17 | } else { 18 | clone = Object.assign({}, obj); 19 | } 20 | clone[head] = setPropImmutableRec(obj[head] !== undefined ? obj[head] : {}, prop, value, i + 1); 21 | return clone; 22 | } 23 | 24 | return typeof value === 'function' ? value(obj) : value; 25 | }; 26 | 27 | return setPropImmutableRec(obj, prop, value, 0); 28 | } 29 | 30 | /** 31 | * Get a value by a dot path. 32 | * @param obj The object to evaluate. 33 | * @param prop The path to value that should be returned. 34 | * @param [value] The default value that should be returned when the target doesn't exist. 35 | */ 36 | function get(obj, prop, value) { 37 | prop = typeof prop === 'number' ? propToArray(prop.toString()) : typeof prop === 'string' ? propToArray(prop) : prop; 38 | 39 | for (var i = 0; i < prop.length; i++) { 40 | if (obj === null || typeof obj !== 'object') { 41 | return value; 42 | } 43 | let head = prop[i]; 44 | if (Array.isArray(obj) && head === '$end') { 45 | head = obj.length - 1; 46 | } 47 | obj = obj[head]; 48 | } 49 | 50 | if (typeof obj === 'undefined') { 51 | return value; 52 | } 53 | 54 | return obj; 55 | } 56 | 57 | /** 58 | * Delete a property by a dot path. 59 | * If target container is an object, the property is deleted. 60 | * If target container is an array, the index is deleted. 61 | * If target container is undefined, nothing is deleted. 62 | * @param obj The object to evaluate. 63 | * @param prop The path to the property or index that should be deleted. 64 | */ 65 | function _delete(obj, prop) { 66 | prop = typeof prop === 'number' ? propToArray(prop.toString()) : typeof prop === 'string' ? propToArray(prop) : prop; 67 | 68 | const deletePropImmutableRec = (obj, prop, i) => { 69 | let clone, head = prop[i]; 70 | 71 | if (obj === null || typeof obj !== 'object' || 72 | !Array.isArray(obj) && obj[head] === undefined) { 73 | 74 | return obj; 75 | } 76 | 77 | if (prop.length - 1 > i) { 78 | if (Array.isArray(obj)) { 79 | head = getArrayIndex(head, obj); 80 | clone = obj.slice(); 81 | } else { 82 | clone = Object.assign({}, obj); 83 | } 84 | 85 | clone[head] = deletePropImmutableRec(obj[head], prop, i + 1); 86 | return clone; 87 | } 88 | 89 | if (Array.isArray(obj)) { 90 | head = getArrayIndex(head, obj); 91 | clone = [].concat(obj.slice(0, head), obj.slice(head + 1)); 92 | } else { 93 | clone = Object.assign({}, obj); 94 | delete clone[head]; 95 | } 96 | 97 | return clone; 98 | }; 99 | 100 | return deletePropImmutableRec(obj, prop, 0); 101 | } 102 | 103 | /** 104 | * Toggles a value. The target value is evaluated using Boolean(currentValue). The result will always be a JSON boolean. 105 | * Be careful with strings as target value, as "true" and "false" will toggle to false, but "0" will toggle to true. 106 | * Here is what Javascript considers false: 0, -0, null, false, NaN, undefined, and the empty string ("") 107 | * @param obj The object to evaluate. 108 | * @param prop The path to the value. 109 | */ 110 | function toggle(obj, prop) { 111 | const curVal = get(obj, prop); 112 | return set(obj, prop, !Boolean(curVal)); 113 | } 114 | 115 | /** 116 | * Merges a value. The target value must be an object, array, null, or undefined. 117 | * If target is an object, Object.assign({}, target, param) is used. 118 | * If target an array, target.concat(param) is used. 119 | * If target is null or undefined, the value is simply set. 120 | * @param obj The object to evaluate. 121 | * @param prop The path to the value. 122 | * @param val The value to merge into the target value. 123 | */ 124 | function merge(obj, prop, val) { 125 | const curVal = get(obj, prop); 126 | if (typeof curVal === 'object') { 127 | if (Array.isArray(curVal)) { 128 | return set(obj, prop, curVal.concat(val)); 129 | } else if (curVal === null) { 130 | return set(obj, prop, val); 131 | } 132 | else { 133 | let merged = Object.assign({}, curVal, val); 134 | return set(obj, prop, merged); 135 | } 136 | } else if (typeof curVal === 'undefined') { 137 | return set(obj, prop, val); 138 | } 139 | else { 140 | return obj; 141 | } 142 | } 143 | 144 | function getArrayIndex(head, obj) { 145 | if (head === '$end') { 146 | head = Math.max(obj.length - 1, 0); 147 | } 148 | if (!/^\+?\d+$/.test(head)) { 149 | throw new Error(`Array index '${head}' has to be an integer`); 150 | } 151 | return parseInt(head); 152 | } 153 | 154 | function propToArray(prop) { 155 | return prop.split('.').reduce((ret, el, index, list) => { 156 | const last = index > 0 && list[index - 1]; 157 | if (last && /(?:^|[^\\])\\$/.test(last)) { 158 | const prev = ret.pop(); 159 | ret.push(prev.slice(0, -1) + '.' + el); 160 | } else { 161 | ret.push(el); 162 | } 163 | return ret; 164 | }, []); 165 | } 166 | 167 | module.exports = { 168 | set, 169 | get, 170 | delete: _delete, 171 | toggle, 172 | merge 173 | }; 174 | -------------------------------------------------------------------------------- /test/examples.spec.js: -------------------------------------------------------------------------------- 1 | const dotProp = require('../lib'); 2 | 3 | describe('examples.spec.js', () => { 4 | 5 | describe('when get', () => { 6 | 7 | it('prop', () => { 8 | expect(dotProp.get({ foo: { bar: 'unicorn' } }, 'foo.bar')).to.eql('unicorn'); 9 | }); 10 | 11 | it('prop undefined', () => { 12 | expect(dotProp.get({ foo: { bar: 'a' } }, 'foo.notDefined.deep')).to.eql(undefined); 13 | }); 14 | 15 | it('prop with dot', () => { 16 | expect(dotProp.get({ foo: { 'dot.dot': 'unicorn' } }, 'foo.dot\\.dot')).to.eql('unicorn'); 17 | }); 18 | 19 | it('use an array as get path', () => { 20 | expect(dotProp.get({ foo: { 'dot.dot': 'unicorn' } }, ['foo', 'dot.dot'])).to.eql('unicorn'); 21 | }); 22 | 23 | it('index', () => { 24 | expect(dotProp.get({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.1')).to.eql('white-unicorn'); 25 | }); 26 | 27 | it('index deep', () => { 28 | expect(dotProp.get({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.0.bar')).to.eql('gold-unicorn'); 29 | }); 30 | 31 | it('array index', () => { 32 | expect(dotProp.get([{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'], '0.bar')).to.eql('gold-unicorn'); 33 | }); 34 | 35 | it('prop as number', () => { 36 | expect(dotProp.get([{ foo: 'silver-unicorn' }], 0)).to.eql({ foo: 'silver-unicorn' }); 37 | }); 38 | 39 | it('index $end', () => { 40 | expect(dotProp.get({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.$end')).to.eql('silver-unicorn'); 41 | expect(dotProp.get({ foo: [] }, 'foo.$end')).to.eql(undefined); 42 | expect(dotProp.set({ foo: [] }, 'foo.$end', 'bar')).to.eql({ foo: ['bar'] }); 43 | }); 44 | }); 45 | 46 | describe('when set', () => { 47 | const obj = { foo: { bar: 'a' } }; 48 | let obj1, obj2, obj3; 49 | 50 | describe('when prop', () => { 51 | 52 | before(() => { 53 | obj1 = dotProp.set(obj, 'foo.bar', 'b'); 54 | }); 55 | 56 | it('obj1', () => { 57 | expect(obj1).to.eql({ foo: { bar: 'b' } }); 58 | }); 59 | 60 | describe('when prop undefined', () => { 61 | 62 | before(() => { 63 | obj2 = dotProp.set(obj1, 'foo.baz', 'x'); 64 | }); 65 | 66 | it('obj2', () => { 67 | expect(obj2).to.eql({ foo: { bar: 'b', baz: 'x' } }); 68 | }); 69 | 70 | describe('when prop undefined', () => { 71 | 72 | before(() => { 73 | obj3 = dotProp.set(obj2, 'foo.dot\\.dot', 'unicorn'); 74 | }); 75 | 76 | it('obj3', () => { 77 | expect(obj3).to.eql({ foo: { bar: 'b', baz: 'x', 'dot.dot': 'unicorn' } }); 78 | }); 79 | 80 | it('obj !== obj1', () => { 81 | expect(obj).to.not.eql(obj1); 82 | }); 83 | 84 | it('obj1 !== obj2', () => { 85 | expect(obj1).to.not.eql(obj2); 86 | }); 87 | 88 | it('obj2 !== obj3', () => { 89 | expect(obj2).to.not.eql(obj3); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | it('Use an array as set path', () => { 96 | expect(dotProp.set({ foo: { bar: 'b', baz: 'x' } }, ['foo', 'dot.dot'], 'unicorn')).to.eql( 97 | { foo: { bar: 'b', baz: 'x', 'dot.dot': 'unicorn' } } 98 | ); 99 | }); 100 | 101 | it('Setter where value is a function', () => { 102 | expect(dotProp.set(obj, 'foo.bar', v => v + 'bc')).to.eql( 103 | { foo: { bar: 'abc' } } 104 | ); 105 | }); 106 | 107 | it('Index into array', () => { 108 | expect(dotProp.set({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.1', 'platin-unicorn')).to.eql( 109 | { foo: [{ bar: 'gold-unicorn' }, 'platin-unicorn', 'silver-unicorn'] } 110 | ); 111 | }); 112 | 113 | it('Index into array deep', () => { 114 | expect(dotProp.set({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.0.bar', 'platin-unicorn')).to.eql( 115 | { foo: [{ bar: 'platin-unicorn' }, 'white-unicorn', 'silver-unicorn'] } 116 | ); 117 | }); 118 | 119 | it('Array', () => { 120 | expect(dotProp.set([{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'], '0.bar', 'platin-unicorn')).to.eql( 121 | [{ bar: 'platin-unicorn' }, 'white-unicorn', 'silver-unicorn'] 122 | ); 123 | }); 124 | 125 | it('Index into array', () => { 126 | expect(dotProp.set({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.$end', 'platin-unicorn')).to.eql( 127 | { foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'platin-unicorn'] } 128 | ); 129 | }); 130 | }); 131 | 132 | describe('when delete', () => { 133 | 134 | it('Array element by index', () => { 135 | expect(dotProp.delete({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.1')).to.eql( 136 | { foo: [{ bar: 'gold-unicorn' }, 'silver-unicorn'] } 137 | ); 138 | }); 139 | 140 | it('Array element by $end', () => { 141 | expect(dotProp.delete({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.$end')).to.eql( 142 | { foo: [{ bar: 'gold-unicorn' }, 'white-unicorn'] } 143 | ); 144 | }); 145 | 146 | it('Out of array', () => { 147 | expect(dotProp.delete({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.10')).to.eql( 148 | { foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] } 149 | ); 150 | }); 151 | 152 | it('Array indexed by a property', () => { 153 | try { 154 | dotProp.delete({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.bar'); 155 | } catch (err) { 156 | expect(err.message).to.equal('Array index \'bar\' has to be an integer'); 157 | expect(err).to.be.an('error'); 158 | } 159 | }); 160 | 161 | it('Deep prop', () => { 162 | expect(dotProp.delete({ foo: [{ bar: 'gold-unicorn' }, 'white-unicorn', 'silver-unicorn'] }, 'foo.0.bar')).to.eql( 163 | { foo: [{}, 'white-unicorn', 'silver-unicorn'] } 164 | ); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dot-prop-immutable 2 | 3 | [![npm version](https://badge.fury.io/js/dot-prop-immutable.svg)](https://badge.fury.io/js/dot-prop-immutable) [![Build Status](https://travis-ci.com/debitoor/dot-prop-immutable.svg?branch=master)](https://travis-ci.com/debitoor/dot-prop-immutable) [![CodeQL](https://github.com/debitoor/dot-prop-immutable/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/debitoor/dot-prop-immutable/actions/workflows/codeql-analysis.yml) 4 | 5 | > [!IMPORTANT] 6 | > **Unmaintained lib and repository.** If anybody would like to take ownership of the library, please get in touch! 7 | 8 | Immutable version of dot-prop with some extensions. 9 | 10 | ```bash 11 | npm install dot-prop-immutable 12 | ``` 13 | 14 | The motivation for this module is to have a simple utility for changing state in a React-Redux application without mutating the existing state of plain JavaScript objects. 15 | If you are going for real immutable data collections take a look at the cool library [Immutable.js](https://github.com/facebook/immutable-js). 16 | A good practice is not to mix the immutable data collections with mutable objects because it can lead to confusion. Immutable objects are not accessed by the default semantics, but implemented by setters and getters. 17 | 18 | This library implements 3 helper functions: 19 | 20 | ```bash 21 | get(object, path) --> value 22 | set(object, path, value) --> object 23 | delete(object, path) --> object 24 | ``` 25 | 26 | None of the functions mutate the input object. For efficiency, the returned object is not a deep clone of the original, but a shallow copy of the objects in the mutated path. 27 | 28 | ## Usage 29 | 30 | ```javascript 31 | const dotProp = require('dot-prop-immutable'); 32 | let state = { todos: [] }, index = 0; 33 | 34 | // Add todo: 35 | state = dotProp.set(state, 'todos', list => [...list, {text: 'cleanup', complete: false}]) 36 | // or with destructuring assignment 37 | state = {...state, todos: [...state.todos, {text: 'cleanup', complete: false}]}; 38 | //=> { todos: [{text: 'cleanup', complete: false}] } 39 | 40 | // Complete todo: 41 | state = dotProp.set(state, `todos.${index}.complete`, true) 42 | // or with destructuring assignment 43 | state = {...state, todos: [ 44 | ...state.todos.slice(0, index), 45 | {...state.todos[index], complete: true}, 46 | ...state.todos.slice(index + 1) 47 | ]}; 48 | //=> { todos: [{text: 'cleanup', complete: true}] } 49 | 50 | // Delete todo: 51 | state = dotProp.delete(state, `todos.${index}`) 52 | // or with destructuring assignment 53 | state = {...state, todos: [ 54 | ...state.todos.slice(0, index), 55 | ...state.todos.slice(index + 1) 56 | ]}; 57 | //=> { todos: [] } 58 | ``` 59 | ### get 60 | 61 | Access a nested property by a dot path 62 | 63 | ```javascript 64 | // Getter 65 | dotProp.get({foo: {bar: 'unicorn'}}, 'foo.bar') 66 | //=> 'unicorn' 67 | 68 | dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep') 69 | //=> undefined 70 | 71 | dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value') 72 | //=> default value 73 | 74 | dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot') 75 | //=> 'unicorn' 76 | ``` 77 | 78 | 79 | or use a property array as a path. 80 | 81 | ```javascript 82 | // Use an array as get path 83 | dotProp.get({foo: {'dot.dot': 'unicorn'}}, ['foo', 'dot.dot']) 84 | //=> 'unicorn' 85 | ``` 86 | 87 | 88 | It is also possible to index into an array where the special index `$end` refers to the last element of the array. 89 | 90 | ```javascript 91 | const obj = {foo: [{ bar: 'gold-unicorn'}, 'white-unicorn', 'silver-unicorn']}; 92 | 93 | // Index into array 94 | dotProp.get(obj, 'foo.1') 95 | //=> 'white-unicorn' 96 | 97 | dotProp.get(obj, 'foo.0.bar') 98 | //=> 'gold-unicorn' 99 | 100 | // Index into array with $end 101 | dotProp.get(obj, 'foo.$end') 102 | //=> 'silver-unicorn' 103 | 104 | // If obj is an array 105 | dotProp.get([{ bar: 'gold-unicorn'}, 'white-unicorn', 'silver-unicorn'], '0.bar') 106 | //=> 'gold-unicorn' 107 | 108 | ``` 109 | 110 | 111 | ### set 112 | 113 | Modify a nested property by a dot path 114 | 115 | ```javascript 116 | // Setter 117 | const obj = {foo: {bar: 'a'}}; 118 | 119 | const obj1 = dotProp.set(obj, 'foo.bar', 'b'); 120 | //obj1 => {foo: {bar: 'b'}} 121 | 122 | const obj2 = dotProp.set(obj1 , 'foo.baz', 'x'); 123 | //obj2 => {foo: {bar: 'b', baz: 'x'}} 124 | ``` 125 | 126 | where `obj`, `obj1`, `obj2`, `obj3` all are different objects. 127 | 128 | 129 | 130 | Use a function to modify the selected property, where first argument is the old value. 131 | 132 | ```javascript 133 | // Setter where value is a function (get and set current value) 134 | dotProp.set({foo: {bar: 'a'}}, 'foo.bar', v => v + 'bc') 135 | //=> {foo: {bar: 'abc'}} 136 | ``` 137 | 138 | 139 | Modify a nested array 140 | 141 | ```javascript 142 | const obj = {foo: [{ bar: 'gold-unicorn'}, 'white-unicorn', 'silver-unicorn']}; 143 | 144 | // Index into array 145 | dotProp.set(obj, 'foo.1', 'platin-unicorn') 146 | //=> {foo: [{bar: 'gold-unicorn'}, 'platin-unicorn', 'silver-unicorn']} 147 | 148 | dotProp.set(obj, 'foo.0.bar', 'platin-unicorn') 149 | //=> {foo: [{bar: 'platin-unicorn'}, 'white-unicorn', 'silver-unicorn']} 150 | 151 | // Index into array with $end 152 | dotProp.set(obj, 'foo.$end', 'platin-unicorn') 153 | //=> {foo: [{ bar: 'gold-unicorn'}, 'white-unicorn', 'platin-unicorn']} 154 | 155 | ``` 156 | 157 | 158 | ### delete 159 | 160 | Delete a nested property/array by a dot path 161 | 162 | ```javascript 163 | const obj = {foo: [{ bar: 'gold-unicorn'}, 'white-unicorn', 'silver-unicorn']}; 164 | 165 | // delete 166 | dotProp.delete(obj, 'foo.$end'); 167 | //=> {foo: [{ bar: 'gold-unicorn'}, 'white-unicorn']} 168 | 169 | dotProp.delete(obj, 'foo.0.bar'); 170 | //=> {foo: [{}, 'white-unicorn', 'silver-unicorn']} 171 | ``` 172 | 173 | ### toggle 174 | 175 | Toggle a boolean a value by a dot path. 176 | 177 | ```javascript 178 | const obj = {foo: { bar: true } }; 179 | 180 | // toggle 181 | dotProp.toggle(obj, 'foo.bar'); 182 | //=> {foo: { bar: false } } 183 | ``` 184 | ### merge 185 | 186 | Merge a value by a dot path. 187 | > The target value must be an object, array, null, or undefined. 188 | 189 | * If target is an object, Object.assign({}, target, param) is used. 190 | * If target an array, target.concat(param) is used. 191 | * If target is null or undefined, the value is simply set. 192 | 193 | ```javascript 194 | const obj = {foo: { bar: {a:1, b:2 } }; 195 | 196 | // merge object 197 | dotProp.merge(obj, 'foo.bar', {c:3} ); 198 | //=> {foo: { bar:{ a:1, b:2, c:3} } } 199 | 200 | var arr = {foo: { bar: [1, 2] } }; 201 | 202 | // merge array 203 | dotProp.merge(arr, 'foo.bar', [3, 4] ); 204 | //=> {foo: { bar:[1, 2, 3, 4 ] } 205 | ``` 206 | ## License 207 | 208 | [MIT](http://opensource.org/licenses/MIT) 209 | -------------------------------------------------------------------------------- /test/dot-prop-immutable.spec.js: -------------------------------------------------------------------------------- 1 | const dotProp = require('../lib'); 2 | 3 | describe('dot-prop-immutable.spec.js', () => { 4 | 5 | const obj = { 6 | a: 1, 7 | b: { 8 | x: 1, 9 | y: 2 10 | }, 11 | c: [1, 2], 12 | 'b.x': 10 13 | }; 14 | 15 | const deeplyNestedObj = { 16 | flavour: { 17 | spicy: { 18 | pepper: { 19 | bell: 'mild', 20 | habanero: 'strong' 21 | } 22 | }, 23 | 'dotty.dot.flavours': { 24 | dalmatian: 'stracciatella' 25 | } 26 | }, 27 | 'www.example.com': 'domain' 28 | }; 29 | 30 | const arr = [1, { a: false }]; 31 | 32 | const arrWithUndefined = [1, undefined, 4]; 33 | 34 | let result; 35 | 36 | describe('when set', () => { 37 | 38 | describe('when have an object', () => { 39 | 40 | describe('when set prop', () => { 41 | 42 | before(() => { 43 | result = dotProp.set(obj, 'b', 3); 44 | }); 45 | 46 | it('should replace prop', () => { 47 | expect(result).to.eql({ 48 | a: 1, 49 | b: 3, 50 | c: [1, 2], 51 | 'b.x': 10 52 | }); 53 | }); 54 | 55 | it('invariant', objInvariant); 56 | }); 57 | 58 | describe('when set prop empty object', () => { 59 | 60 | before(() => { 61 | result = dotProp.set({}, 'b', 3); 62 | }); 63 | 64 | it('should replace prop', () => { 65 | expect(result).to.eql({ 66 | b: 3 67 | }); 68 | }); 69 | }); 70 | 71 | describe('when set prop empty path', () => { 72 | 73 | before(() => { 74 | result = dotProp.set({}, '', 3); 75 | }); 76 | 77 | it('should replace prop', () => { 78 | expect(result).to.eql({ 79 | '': 3 80 | }); 81 | }); 82 | }); 83 | 84 | describe('when set prop with function', () => { 85 | 86 | before(() => { 87 | result = dotProp.set(obj, 'a', v => v + 1); 88 | }); 89 | 90 | it('should replace prop', () => { 91 | expect(result).to.eql({ 92 | a: 2, 93 | b: { 94 | x: 1, 95 | y: 2 96 | }, 97 | c: [1, 2], 98 | 'b.x': 10 99 | }); 100 | }); 101 | 102 | it('invariant', objInvariant); 103 | }); 104 | 105 | describe('when set deep prop', () => { 106 | 107 | before(() => { 108 | result = dotProp.set(obj, 'b.x', 3); 109 | }); 110 | 111 | it('should replace prop', () => { 112 | expect(result).to.eql({ 113 | a: 1, 114 | b: { 115 | x: 3, 116 | y: 2 117 | }, 118 | c: [1, 2], 119 | 'b.x': 10 120 | }); 121 | }); 122 | 123 | it('invariant', objInvariant); 124 | }); 125 | 126 | describe('when set deep prop not defined', () => { 127 | 128 | before(() => { 129 | result = dotProp.set(obj, 'b.z.w', 3); 130 | }); 131 | 132 | it('should replace prop', () => { 133 | expect(result).to.eql({ 134 | a: 1, 135 | b: { 136 | x: 1, 137 | y: 2, 138 | z: { 139 | w: 3 140 | } 141 | }, 142 | c: [1, 2], 143 | 'b.x': 10 144 | }); 145 | }); 146 | 147 | it('invariant', objInvariant); 148 | }); 149 | 150 | describe('when set array[index]', () => { 151 | 152 | before(() => { 153 | result = dotProp.set(obj, 'c.0', 3); 154 | }); 155 | 156 | it('should replace prop', () => { 157 | expect(result).to.eql({ 158 | a: 1, 159 | b: { 160 | x: 1, 161 | y: 2 162 | }, 163 | c: [3, 2], 164 | 'b.x': 10 165 | }); 166 | }); 167 | 168 | it('invariant', objInvariant); 169 | }); 170 | 171 | describe('when set array[index] with function', () => { 172 | 173 | before(() => { 174 | result = dotProp.set(obj, 'c.0', v => v * 3); 175 | }); 176 | 177 | it('should replace prop', () => { 178 | expect(result).to.eql({ 179 | a: 1, 180 | b: { 181 | x: 1, 182 | y: 2 183 | }, 184 | c: [3, 2], 185 | 'b.x': 10 186 | }); 187 | }); 188 | 189 | it('invariant', objInvariant); 190 | }); 191 | 192 | describe('when set array[index] prop not defined', () => { 193 | 194 | before(() => { 195 | result = dotProp.set(obj, 'c.1.z.w', 3); 196 | }); 197 | 198 | it('should replace prop', () => { 199 | expect(result).to.eql({ 200 | a: 1, 201 | b: { 202 | x: 1, 203 | y: 2 204 | }, 205 | c: [1, { z: { w: 3 } }], 206 | 'b.x': 10 207 | }); 208 | }); 209 | 210 | it('invariant', objInvariant); 211 | }); 212 | 213 | describe('when set array[index] out of index', () => { 214 | 215 | before(() => { 216 | result = dotProp.set(obj, 'c.3', 3); 217 | }); 218 | 219 | it('should replace prop', () => { 220 | const c = [1, 2]; 221 | c[3] = 3; 222 | expect(result).to.eql({ 223 | a: 1, 224 | b: { 225 | x: 1, 226 | y: 2 227 | }, 228 | c: c, 229 | 'b.x': 10 230 | }); 231 | }); 232 | 233 | it('invariant', objInvariant); 234 | }); 235 | 236 | describe('when set array[$end]', () => { 237 | 238 | before(() => { 239 | result = dotProp.set(obj, 'c.$end', 3); 240 | }); 241 | 242 | it('should replace prop', () => { 243 | expect(result).to.eql({ 244 | a: 1, 245 | b: { 246 | x: 1, 247 | y: 2 248 | }, 249 | c: [1, 3], 250 | 'b.x': 10 251 | }); 252 | }); 253 | 254 | it('invariant', objInvariant); 255 | }); 256 | 257 | describe('when set array[index] and index not integer', () => { 258 | let error; 259 | 260 | before(() => { 261 | try { 262 | dotProp.set(obj, 'c.w', 3); 263 | } catch (err) { 264 | error = err; 265 | } 266 | }); 267 | 268 | it('should throw an error', () => { 269 | expect(error.message).to.eql('Array index \'w\' has to be an integer'); 270 | }); 271 | 272 | it('invariant', objInvariant); 273 | }); 274 | }); 275 | 276 | describe('when have an array', () => { 277 | 278 | describe('when set array[index]', () => { 279 | 280 | before(() => { 281 | result = dotProp.set(arr, '0', 3); 282 | }); 283 | 284 | it('should replace prop', () => { 285 | expect(result).to.eql( 286 | [3, { a: false }] 287 | ); 288 | }); 289 | 290 | it('invariant', arrInvariant); 291 | }); 292 | 293 | describe('when set array[index] deep prop', () => { 294 | 295 | before(() => { 296 | result = dotProp.set(arr, '1.a', v => !v); 297 | }); 298 | 299 | it('should replace prop', () => { 300 | expect(result).to.eql( 301 | [1, { a: true }] 302 | ); 303 | }); 304 | 305 | it('invariant', arrInvariant); 306 | }); 307 | }); 308 | }); 309 | 310 | describe('when get', () => { 311 | 312 | describe('when have an object', () => { 313 | 314 | describe('when get prop', () => { 315 | 316 | it('should get prop', () => { 317 | expect(dotProp.get(obj, 'b')).to.eql({ x: 1, y: 2 }); 318 | }); 319 | it('should get prop default', () => { 320 | expect(dotProp.get(obj, 'd', 'default')).to.eql('default'); 321 | }); 322 | }); 323 | 324 | describe('when get prop empty object', () => { 325 | 326 | it('should get prop', () => { 327 | expect(dotProp.get({}, 'b')).to.equal(undefined); 328 | }); 329 | it('should get prop default', () => { 330 | expect(dotProp.get({}, 'b', 'default')).to.equal('default'); 331 | }); 332 | }); 333 | 334 | describe('when get prop empty path', () => { 335 | 336 | it('should get prop', () => { 337 | expect(dotProp.get(obj, '')).to.equal(undefined); 338 | }); 339 | it('should get prop default', () => { 340 | expect(dotProp.get(obj, '', 'default')).to.equal('default'); 341 | }); 342 | }); 343 | 344 | describe('when get deep prop', () => { 345 | 346 | it('should get prop', () => { 347 | expect(dotProp.get(obj, 'b.x')).to.equal(1); 348 | }); 349 | it('should get prop default', () => { 350 | expect(dotProp.get(obj, 'b.z', 'default')).to.equal('default'); 351 | }); 352 | }); 353 | 354 | describe('when get dotted prop', () => { 355 | 356 | it('should get prop', () => { 357 | expect(dotProp.get(obj, 'b\\.x')).to.equal(10); 358 | }); 359 | it('should get prop default', () => { 360 | expect(dotProp.get(obj, 'b\\.y', 'default')).to.equal('default'); 361 | }); 362 | it('should get deeply nested key', () => { 363 | expect(dotProp.get(deeplyNestedObj, 'flavour.spicy.pepper.bell', 'default')).to.equal('mild'); 364 | }); 365 | it('should get deeply nested key with multiple dots in selector', () => { 366 | expect(dotProp.get(deeplyNestedObj, 'flavour.dotty\\.dot\\.flavours.dalmatian', 'default')).to.equal('stracciatella'); 367 | }); 368 | it('should get key with multiple dots in selector', () => { 369 | expect(dotProp.get(deeplyNestedObj, 'www\\.example\\.com', 'default')).to.equal('domain'); 370 | }); 371 | }); 372 | 373 | describe('when get deep prop not defined', () => { 374 | 375 | it('should return undefined', () => { 376 | expect(dotProp.get(obj, 'b.z.w')).to.equal(undefined); 377 | }); 378 | 379 | it('should return the default value', () => { 380 | expect(dotProp.get(obj, 'b.z.w', 'something')).to.equal('something'); 381 | }); 382 | }); 383 | 384 | describe('when get intermediate deep prop is null', () => { 385 | 386 | it('should return default value', () => { 387 | expect(dotProp.get({ b: { z: null } }, 'b.z.w')).to.equal(undefined); 388 | expect(dotProp.get({ b: { z: null } }, 'b.z.w', 'default')).to.equal('default'); 389 | }); 390 | }); 391 | 392 | describe('when get array[index]', () => { 393 | 394 | it('should get index', () => { 395 | expect(dotProp.get(obj, 'c.0')).to.equal(1); 396 | }); 397 | it('should get index default', () => { 398 | expect(dotProp.get(obj, 'c.2', 'default')).to.equal('default'); 399 | }); 400 | }); 401 | 402 | describe('when get array[index] prop not defined', () => { 403 | 404 | it('should return undefined', () => { 405 | expect(dotProp.get(obj, 'c.1.z.w')).to.equal(undefined); 406 | }); 407 | it('should return default', () => { 408 | expect(dotProp.get(obj, 'c.1.z.w', 'default')).to.equal('default'); 409 | }); 410 | }); 411 | 412 | describe('when get array[index] out of index', () => { 413 | 414 | it('should return undefined', () => { 415 | expect(dotProp.get(obj, 'c.3')).to.equal(undefined); 416 | }); 417 | }); 418 | 419 | describe('when get array[$end]', () => { 420 | 421 | it('should get end index', () => { 422 | expect(dotProp.get(obj, 'c.$end')).to.equal(2); 423 | }); 424 | }); 425 | 426 | describe('when get undefined prop on array', () => { 427 | 428 | it('should return undefined', () => { 429 | expect(dotProp.get(obj, 'c.w')).to.equal(undefined); 430 | }); 431 | it('should return default', () => { 432 | expect(dotProp.get(obj, 'c.w', 'default')).to.equal('default'); 433 | }); 434 | }); 435 | }); 436 | 437 | describe('when have an array', () => { 438 | 439 | describe('when get array[index]', () => { 440 | 441 | it('should get index', () => { 442 | expect(dotProp.get(arr, '0')).to.equal(1); 443 | }); 444 | it('should return default when index not present', () => { 445 | expect(dotProp.get(arr, '2', 'default')).to.equal('default'); 446 | }); 447 | }); 448 | 449 | describe('when get array[index] deep prop', () => { 450 | 451 | it('should replace prop', () => { 452 | expect(dotProp.get(arr, '1.a')).to.equal(false); 453 | }); 454 | it('should return default when not present', () => { 455 | expect(dotProp.get(arr, '1.b', 'default')).to.equal('default'); 456 | }); 457 | }); 458 | 459 | }); 460 | }); 461 | 462 | describe('when delete', () => { 463 | 464 | describe('when have an object', () => { 465 | 466 | describe('when delete prop', () => { 467 | 468 | before(() => { 469 | result = dotProp.delete(obj, 'b'); 470 | }); 471 | 472 | it('should prop', () => { 473 | expect(result).to.eql({ 474 | a: 1, 475 | c: [1, 2], 476 | 'b.x': 10 477 | }); 478 | }); 479 | 480 | it('invariant', objInvariant); 481 | }); 482 | 483 | describe('when delete prop empty object', () => { 484 | 485 | before(() => { 486 | result = dotProp.delete({}, 'b'); 487 | }); 488 | 489 | it('should delete prop', () => { 490 | expect(result).to.eql({}); 491 | }); 492 | }); 493 | 494 | describe('when delete prop empty path', () => { 495 | 496 | before(() => { 497 | result = dotProp.delete({}, ''); 498 | }); 499 | 500 | it('should delete prop', () => { }); 501 | }); 502 | 503 | describe('when delete deep prop', () => { 504 | 505 | before(() => { 506 | result = dotProp.delete(obj, 'b.x'); 507 | }); 508 | 509 | it('should delete prop', () => { 510 | expect(result).to.eql({ 511 | a: 1, 512 | b: { 513 | y: 2 514 | }, 515 | c: [1, 2], 516 | 'b.x': 10 517 | }); 518 | }); 519 | 520 | it('invariant', objInvariant); 521 | }); 522 | 523 | describe('when delete deep prop not defined', () => { 524 | 525 | before(() => { 526 | result = dotProp.delete(obj, 'b.z.w'); 527 | }); 528 | 529 | it('should delete prop', () => { 530 | expect(result).to.eql({ 531 | a: 1, 532 | b: { 533 | x: 1, 534 | y: 2 535 | }, 536 | c: [1, 2], 537 | 'b.x': 10 538 | }); 539 | }); 540 | 541 | it('invariant', objInvariant); 542 | }); 543 | 544 | describe('when delete deep prop is null', () => { 545 | 546 | before(() => { 547 | result = dotProp.delete({ b: { z: null } }, 'b.z.w'); 548 | }); 549 | 550 | it('should delete prop', () => { 551 | expect(result).to.eql({ b: { z: null } }); 552 | }); 553 | }); 554 | 555 | describe('when delete array[index]', () => { 556 | 557 | before(() => { 558 | result = dotProp.delete(obj, 'c.0'); 559 | }); 560 | 561 | it('should delete prop', () => { 562 | expect(result).to.eql({ 563 | a: 1, 564 | b: { 565 | x: 1, 566 | y: 2 567 | }, 568 | c: [2], 569 | 'b.x': 10 570 | }); 571 | }); 572 | 573 | it('invariant', objInvariant); 574 | }); 575 | 576 | describe('when delete array[index] with function', () => { 577 | 578 | before(() => { 579 | result = dotProp.set(obj, 'c.0', v => v * 3); 580 | }); 581 | 582 | it('should delete prop', () => { 583 | expect(result).to.eql({ 584 | a: 1, 585 | b: { 586 | x: 1, 587 | y: 2 588 | }, 589 | c: [3, 2], 590 | 'b.x': 10 591 | }); 592 | }); 593 | 594 | it('invariant', objInvariant); 595 | }); 596 | 597 | describe('when delete array[index] prop not defined', () => { 598 | 599 | before(() => { 600 | result = dotProp.delete(obj, 'c.1.z.w'); 601 | }); 602 | 603 | it('should delete prop', () => { 604 | expect(result).to.eql({ 605 | a: 1, 606 | b: { 607 | x: 1, 608 | y: 2 609 | }, 610 | c: [1, 2], 611 | 'b.x': 10 612 | }); 613 | }); 614 | 615 | it('invariant', objInvariant); 616 | }); 617 | 618 | describe('when delete array[index] out of index', () => { 619 | 620 | before(() => { 621 | result = dotProp.delete(obj, 'c.3'); 622 | }); 623 | 624 | it('should delete prop', () => { 625 | expect(result).to.eql({ 626 | a: 1, 627 | b: { 628 | x: 1, 629 | y: 2 630 | }, 631 | c: [1, 2], 632 | 'b.x': 10 633 | }); 634 | }); 635 | 636 | it('invariant', objInvariant); 637 | }); 638 | 639 | describe('when delete array[$end]', () => { 640 | 641 | before(() => { 642 | result = dotProp.delete(obj, 'c.$end'); 643 | }); 644 | 645 | it('should delete prop', () => { 646 | expect(result).to.eql({ 647 | a: 1, 648 | b: { 649 | x: 1, 650 | y: 2 651 | }, 652 | c: [1], 653 | 'b.x': 10 654 | }); 655 | }); 656 | 657 | it('invariant', objInvariant); 658 | }); 659 | 660 | describe('when delete array[index] and index not integer', () => { 661 | let error; 662 | 663 | before(() => { 664 | try { 665 | dotProp.delete(obj, 'c.w'); 666 | } catch (err) { 667 | error = err; 668 | } 669 | }); 670 | 671 | it('should throw an error', () => { 672 | expect(error.message).to.eql('Array index \'w\' has to be an integer'); 673 | }); 674 | 675 | it('invariant', objInvariant); 676 | }); 677 | }); 678 | 679 | describe('when have an array', () => { 680 | 681 | describe('when delete array[index]', () => { 682 | 683 | before(() => { 684 | result = dotProp.delete(arr, '0'); 685 | }); 686 | 687 | it('should delete prop', () => { 688 | expect(result).to.eql( 689 | [{ a: false }] 690 | ); 691 | }); 692 | 693 | it('invariant', arrInvariant); 694 | }); 695 | 696 | describe('when delete array[index] deep prop', () => { 697 | 698 | before(() => { 699 | result = dotProp.delete(arr, '1.a'); 700 | }); 701 | 702 | it('should delete prop', () => { 703 | expect(result).to.eql( 704 | [1, {}] 705 | ); 706 | }); 707 | 708 | it('invariant', arrInvariant); 709 | }); 710 | 711 | describe('when delete array[index] which element is undefined', () => { 712 | 713 | before(() => { 714 | result = dotProp.delete(arrWithUndefined, '1'); 715 | }); 716 | 717 | it('should delete the element', () => { 718 | expect(result).to.eql( 719 | [1, 4] 720 | ); 721 | }); 722 | }); 723 | }); 724 | }); 725 | 726 | function objInvariant() { 727 | expect(obj).to.eql({ 728 | a: 1, 729 | b: { 730 | x: 1, 731 | y: 2 732 | }, 733 | c: [1, 2], 734 | 'b.x': 10 735 | }); 736 | } 737 | 738 | function arrInvariant() { 739 | expect(arr).to.eql( 740 | [1, { a: false }] 741 | ); 742 | } 743 | }); 744 | 745 | 746 | --------------------------------------------------------------------------------