├── .eslintrc.json
├── .gitignore
├── EXAMPLES.md
├── ImmutableComparison.md
├── ImmutableComparison
├── PerformanceComparison.gif
├── PerformanceComparison
│ ├── index.html
│ ├── package.json
│ └── src
│ │ ├── diffEqualObjects.js
│ │ └── index.js
├── SyntaxComparison.png
├── SyntaxComparison
│ ├── index.html
│ ├── package.json
│ └── src
│ │ ├── defaultObject.js
│ │ ├── diffEqualObjects.js
│ │ ├── exampleDeepMutation.js
│ │ ├── exampleImmutable.js
│ │ └── index.js
└── performanceResult.png
├── LICENSE.md
├── README.md
├── babel.config.json
├── deepMutationTodo
├── package.json
├── public
│ └── index.html
└── src
│ ├── App.js
│ ├── components
│ ├── Modal.jsx
│ ├── Steps.jsx
│ ├── TodoEditor.jsx
│ ├── TodoRemovingConfirmation.jsx
│ ├── TodoView.jsx
│ ├── TodosList.jsx
│ ├── modal.css
│ ├── steps.css
│ ├── todoEditor.css
│ ├── todoView.css
│ └── todosList.css
│ ├── index.js
│ └── styles.css
├── dist
└── index.js
├── jest.config.js
├── package-lock.json
├── package.json
└── src
├── index.js
└── tests
├── checkIsExists.test.js
├── deepToMutate.test.js
├── extToArray.test.js
├── extToTree.test.js
├── getObjectPaths.test.js
├── getPairValue.test.js
├── getRealIndex.test.js
├── getValue.test.js
├── index_data.test.js
├── isArrayElement.test.js
├── mutate.test.js
├── mutateDeep.test.js
├── mutate_function.test.js
├── object_immutable.test.js
├── path_array.test.js
├── separatePath.test.js
└── splitPath.test.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "env": {
4 | "browser": true,
5 | "jest":true
6 | },
7 | "extends": [
8 | "eslint:recommended"
9 | ],
10 | "plugins": [
11 | "babel"
12 | ],
13 | "rules": {
14 | "keyword-spacing": ["warn"],
15 | "key-spacing": ["warn"],
16 | "max-len": ["warn", 120, {
17 | "ignoreComments": true ,
18 | "ignoreStrings": true,
19 | "ignoreUrls": true,
20 | "ignoreTemplateLiterals": true,
21 | "ignoreRegExpLiterals": true
22 | }],
23 | "object-curly-spacing": ["warn", "always"],
24 | "arrow-parens": "off",
25 | "space-in-parens": "off",
26 | "no-case-declarations": ["warn"],
27 | "no-mixed-operators": "off",
28 | "eqeqeq": "off",
29 | "no-debugger": "warn",
30 | "no-empty": "off",
31 | "indent": ["error", 2, {"SwitchCase": 1}],
32 | "quotes": ["error", "single"],
33 | "semi": ["error", "always", {"omitLastInOneLineBlock": true}],
34 | "no-unused-vars": ["warn"],
35 | "no-const-assign": ["error"],
36 | "no-console": [ "warn", {
37 | "allow": [
38 | "warn",
39 | "error",
40 | "debug"
41 | ]
42 | }],
43 | "comma-dangle": ["error", "only-multiline"],
44 | "callback-return": "error"
45 | },
46 | "globals": {
47 | "module": true
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled binary addons (http://nodejs.org/api/addons.html)
2 | build
3 |
4 | # Dependency directory
5 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
6 | node_modules
7 |
8 | # Coverage directory used by tools like istanbul
9 | coverage
10 |
11 | .idea
12 |
--------------------------------------------------------------------------------
/EXAMPLES.md:
--------------------------------------------------------------------------------
1 | # Only examples
2 |
3 | ```javascript
4 | import mutate from 'deep-mutation';
5 |
6 | const cat = {
7 | size: {
8 | weight: 5,
9 | height: 20,
10 | },
11 | breed: 'siam',
12 | kitten: [
13 | 'Lian',
14 | 'Semion'
15 | ],
16 | sex: 'w'
17 | };
18 |
19 | return mutate(cat, {
20 | 'size.height': 22,
21 | 'kitten.[+1]': 'Lukan',
22 | 'kitten.[+2]': 'Munya'
23 | });
24 | ```
25 |
26 | ```javascript
27 | import mutate, { deepPatch } from 'deep-mutation';
28 |
29 | const todos = [
30 | {
31 | id: 998941425,
32 | title: 'Add useful somthing',
33 | description: 'I would like to make something interesting and useful'
34 | steps: [
35 | { id: 1, text: 'idea', isFinished: false },
36 | { id: 2, text: 'prepare', isFinished: false },
37 | { id: 3, text: 'make', isFinished: false }
38 | ]
39 | },
40 | ...
41 | ];
42 |
43 | ...
44 |
45 | function findTodo(todoId) {
46 | return todos.find(el => el.id == todoId);
47 | }
48 |
49 | function findStep(todo, stepId) {
50 | return todos.find(el => el.id == todoId);
51 | }
52 |
53 | function changeStepState(todoId, stepId, isFinished) {
54 | const todoPos = findTodo(todoId);
55 | const stepPos = findStep(todos[todoPos], stepId);
56 |
57 | return mutate(
58 | todos,
59 | { [`[${todoPos}].steps.[${stepPos}].isFinished`]: isFinished }
60 | );
61 | }
62 |
63 | function changeStepText(todoId, stepId, text) {
64 | const todoPos = findTodo(todoId);
65 | const stepPos = findStep(todos[todoPos], stepId);
66 |
67 | return mutate(
68 | todos,
69 | { [`[${todoPos}].steps.[${stepPos}].text`]: text }
70 | );
71 | }
72 |
73 | function addStep(todoId, text) {
74 | const todoPos = findTodo(todoId);
75 | const newStep = { id: Math.floor(Math.random() * 10000), text, isFinished: false };
76 | return mutate(
77 | todos,
78 | { [`[${todoPos}].steps.[]`]: newStep };
79 | )
80 | }
81 |
82 | function removeStep(todoId, stepId) {
83 | const todoPos = findTodo(todoId);
84 | const stepPos = findStep(todos[todoPos], stepId);
85 | return mutate(
86 | todos,
87 | { [`[${todoPos}].steps.[${stepPos}].text`]: undefined }
88 | )
89 | }
90 | ```
--------------------------------------------------------------------------------
/ImmutableComparison.md:
--------------------------------------------------------------------------------
1 | ## Performance comparison
2 | **Sandbox editor**: https://codesandbox.io/s/l9ovomzv99
3 |
4 | **Sandbox view**: https://l9ovomzv99.csb.app/
5 |
6 | 
7 |
8 | ## Syntax comparison
9 | **Sandbox editor**: https://codesandbox.io/s/j4wkq2znj5
10 |
11 | **Sandbox view**: https://j4wkq2znj5.codesandbox.io/
12 |
13 | 
--------------------------------------------------------------------------------
/ImmutableComparison/PerformanceComparison.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/PerformanceComparison.gif
--------------------------------------------------------------------------------
/ImmutableComparison/PerformanceComparison/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Parcel Sandbox
5 |
6 |
7 |
8 |
9 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ImmutableComparison/PerformanceComparison/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deep-mutation-vs-immutable",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.html",
6 | "scripts": {
7 | "start": "parcel index.html --open",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "deep-mutation": "1.0.3",
12 | "immutable": "3.8.2",
13 | "lodash.isequal": "4.5.0"
14 | },
15 | "devDependencies": {
16 | "parcel-bundler": "^1.6.1"
17 | },
18 | "keywords": []
19 | }
--------------------------------------------------------------------------------
/ImmutableComparison/PerformanceComparison/src/diffEqualObjects.js:
--------------------------------------------------------------------------------
1 | export function diffToHtml(diff) {
2 | return diff
3 | .map(el => `${el[0]}
`)
4 | .join('');
5 | }
6 |
7 | export default function deepDiffEqual(a, b, prefix = '') {
8 | if (!(a instanceof Object)) return null;
9 | return Object.keys(a).reduce((R, key) => {
10 | const currentKey = `${prefix}${prefix ? '.' : ''}${key}`;
11 | R.push([currentKey, a[key] === b[key]]);
12 | if (a[key] instanceof Object) {
13 | return R.concat(deepDiffEqual(a[key], b[key], currentKey));
14 | }
15 | return R;
16 | }, []);
17 | }
18 |
--------------------------------------------------------------------------------
/ImmutableComparison/PerformanceComparison/src/index.js:
--------------------------------------------------------------------------------
1 | import mutate from 'deep-mutation';
2 | import { OrderedMap } from 'immutable';
3 | import isEqual from 'lodash.isequal';
4 |
5 | const abc = 'abcdefghijkmnpqrstuvwxz-_0123456789';
6 | const keyLen = [10, 15];
7 | const deepLevel = 5;
8 |
9 | document.getElementById('runButton').addEventListener('click', onClickRun);
10 |
11 | function onClickRun() {
12 | runner(5);
13 | }
14 |
15 | function runner(runCount = 1) {
16 | let testData = null;
17 | let immutableResult = null;
18 | let mutateResult = null;
19 | let immutableMergeResult = null;
20 | const median = {
21 | immutable: [],
22 | immutableMerge: [],
23 | deepMutation: []
24 | };
25 |
26 | for (let i = 0; i < runCount; i++) {
27 | testData = generateData(generateDataLevel(3, ''), deepLevel, 3).filter(
28 | el => el[0].split('.').length > deepLevel
29 | );
30 |
31 | immutableResult = immutableTest(testData);
32 | immutableMergeResult = immutableMergeTest(immutableResult.result);
33 | mutateResult = mutateTest(testData);
34 |
35 | median.immutable.push(immutableResult.time);
36 | median.immutableMerge.push(immutableMergeResult.time);
37 | median.deepMutation.push(mutateResult.time);
38 | }
39 |
40 | const equals =
41 | isEqual(mutateResult.result, immutableResult.result) &&
42 | isEqual(mutateResult.result, immutableMergeResult.result);
43 |
44 | const medianImmutable =
45 | median.immutable.reduce((R, v) => R + v, 0) / runCount;
46 | const medianImmutableMerge =
47 | median.immutableMerge.reduce((R, v) => R + v, 0) / runCount;
48 | const medianDeepMutation =
49 | median.deepMutation.reduce((R, v) => R + v, 0) / runCount;
50 |
51 | document.getElementById('app').innerHTML = `
52 | Calls count: ${runCount}
53 |
54 | results are equal: ${equals}
55 |
56 | Changes count: ${testData.length}
57 |
58 |
59 | deep-mutation: ${medianDeepMutation} ms
60 |
${median.deepMutation.join(' ms
')}
61 |
62 |
63 |
64 | immutable: ${medianImmutable} ms
65 |
${median.immutable.join(' ms
')}
66 |
67 |
68 |
69 | immutable-merge: ${medianImmutableMerge} ms
70 |
${median.immutableMerge.join(' ms
')}
71 |
72 |
73 |
74 | `;
75 | }
76 |
77 | function mutateTest(pChanges) {
78 | const begin = performance.now();
79 | const result = mutate({}, pChanges);
80 | const end = performance.now();
81 | return {
82 | time: end - begin,
83 | result
84 | };
85 | }
86 |
87 | function immutableMergeTest(pChanges) {
88 | const begin = performance.now();
89 | let iMap = OrderedMap({});
90 | const result = iMap.mergeDeep(pChanges).toJS();
91 | const end = performance.now();
92 | return {
93 | time: end - begin,
94 | result
95 | };
96 | }
97 |
98 | function immutableTest(pChanges) {
99 | const begin = performance.now();
100 | let iMap = OrderedMap({});
101 | for (let i = 0; i < pChanges.length; i++) {
102 | iMap = iMap.setIn(pChanges[i][0].split('.'), pChanges[i][1]);
103 | }
104 | // pChanges.forEach(el => (iMap = iMap.setIn(el[0].split("."), el[1])));
105 | const result = iMap.toJS();
106 | const end = performance.now();
107 | return {
108 | time: end - begin,
109 | result
110 | };
111 | }
112 |
113 | function rnd(min, max) {
114 | return Math.floor(Math.random() * (max - min + 1)) + min;
115 | }
116 |
117 | function genKey() {
118 | const len = rnd(...keyLen);
119 | const result = [];
120 | for (let i = 0; i < len; i++) {
121 | result.push(abc[rnd(0, abc.length - 1)]);
122 | }
123 | return result.join('');
124 | }
125 |
126 | function generateDataLevel(count, path = '') {
127 | const result = [];
128 | const prefix = path ? `${path}.` : '';
129 |
130 | function generateData(val) {
131 | if (val > 0.3 && val < 0.5) {
132 | return {
133 | value: Math.random() * 100000000,
134 | key: genKey()
135 | };
136 | }
137 |
138 | if (val >= 0.5) {
139 | return [1, 2, 3, 4, 5];
140 | }
141 |
142 | return Math.random() * 100000000;
143 | }
144 |
145 | for (let i = 0; i < count; i++) {
146 | result.push([`${prefix}${genKey()}`, generateData(Math.random())]);
147 | }
148 | return result;
149 | }
150 |
151 | function generateData(parents, deep, count) {
152 | let result = [].concat(parents);
153 | for (let i = 0; i < parents.length; i++) {
154 | const data = generateDataLevel(count, parents[i][0]);
155 | if (deep > 0) {
156 | result = result.concat(generateData(data, deep - 1, count));
157 | }
158 | }
159 |
160 | return result;
161 | }
162 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/SyntaxComparison.png
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Parcel Sandbox
5 |
6 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deep-mutation-vs-immutable-syntax",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.html",
6 | "scripts": {
7 | "start": "parcel index.html --open",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "deep-mutation": "1.0.3",
12 | "immutable": "3.8.2",
13 | "lodash.isequal": "4.5.0"
14 | },
15 | "devDependencies": {
16 | "parcel-bundler": "^1.6.1"
17 | },
18 | "keywords": []
19 | }
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/src/defaultObject.js:
--------------------------------------------------------------------------------
1 | const defaultObject = {
2 | count: 0,
3 | offset: 0,
4 | searchText: '',
5 | items: null,
6 | requestId: null,
7 | status: {
8 | isError: false,
9 | isLoading: false,
10 | isSuccess: false
11 | },
12 | error: {},
13 | selectedItem: {},
14 | editedItem: {}
15 | };
16 |
17 | export default defaultObject;
18 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/src/diffEqualObjects.js:
--------------------------------------------------------------------------------
1 | export function diffToHtml(diff) {
2 | return diff
3 | .map(el => `${el[0]}
`)
4 | .join('');
5 | }
6 |
7 | export default function deepDiffEqual(a, b, prefix = '') {
8 | if (!(a instanceof Object)) return null;
9 | const isArray = Array.isArray(a);
10 | return (isArray ? a : Object.keys(a)).reduce((R, key) => {
11 | const currentKey = isArray
12 | ? `${prefix}${prefix ? '.' : ''}[${key}]`
13 | : `${prefix}${prefix ? '.' : ''}${key}`;
14 | R.push([currentKey, a[key] === b[key]]);
15 | if (a[key] instanceof Object) {
16 | return R.concat(deepDiffEqual(a[key], b[key], currentKey));
17 | }
18 | return R;
19 | }, []);
20 | }
21 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/src/exampleDeepMutation.js:
--------------------------------------------------------------------------------
1 | import mutate from 'deep-mutation';
2 | import defaultObject from './defaultObject';
3 |
4 | let byMutate = mutate(defaultObject, {
5 | count: 120,
6 | offset: 10,
7 | searchText: 'text',
8 | items: null,
9 | requestId: Math.floor(Math.random() * 1000000),
10 | status: {
11 | isLoading: true,
12 | isError: false,
13 | isSuccess: false
14 | },
15 | selectedItem: undefined,
16 | editedItem: undefined,
17 | error: undefined
18 | });
19 |
20 | // after response
21 | byMutate = mutate(byMutate, {
22 | items: [1, 2, 3, 4, 5],
23 | 'status.isLoading': false,
24 | 'status.isSuccess': true
25 | });
26 |
27 | // once item is selected
28 | byMutate = mutate(byMutate, { selectedItem: { value: 10, id: 1 } });
29 |
30 | // once selected item value is changed
31 | byMutate = mutate(byMutate, { 'selectedItem.value': [1, 2, 3] });
32 |
33 | // add new elements to selected item value
34 | byMutate = mutate(byMutate, {
35 | 'selectedItem.value.[+1]': 10,
36 | 'selectedItem.value.[+2]': 20,
37 | 'selectedItem.value.[+3]': 30
38 | });
39 |
40 | export default byMutate;
41 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/src/exampleImmutable.js:
--------------------------------------------------------------------------------
1 | import { OrderedMap, List } from 'immutable';
2 | import defaultObject from './defaultObject';
3 |
4 | let byImmutable = new OrderedMap(defaultObject)
5 | .mergeDeep({
6 | count: 120,
7 | offset: 10,
8 | searchText: 'text',
9 | items: null,
10 | requestId: Math.floor(Math.random() * 1000000),
11 | status: {
12 | isLoading: true,
13 | isError: false,
14 | isSuccess: false
15 | }
16 | })
17 | .remove('selectedItem')
18 | .remove('editedItem')
19 | .remove('error');
20 |
21 | // after response
22 | byImmutable = byImmutable
23 | .setIn(['status', 'isLoading'], false)
24 | .setIn(['status', 'isSuccess'], true)
25 | .set('items', [1, 2, 3, 4, 5]);
26 |
27 | // once item is selected
28 | byImmutable = byImmutable.set('selectedItem', OrderedMap({ value: 10, id: 1 }));
29 |
30 | // once selected item value is changed
31 | byImmutable = byImmutable.setIn(['selectedItem', 'value'], List([1, 2, 3]));
32 |
33 | // add new elements to selected item value
34 | byImmutable = byImmutable.setIn(
35 | ['selectedItem', 'value'],
36 | byImmutable.getIn(['selectedItem', 'value']).push(10, 20, 30)
37 | );
38 |
39 | export default byImmutable;
40 |
--------------------------------------------------------------------------------
/ImmutableComparison/SyntaxComparison/src/index.js:
--------------------------------------------------------------------------------
1 | import deepDiffEqual, { diffToHtml } from './diffEqualObjects';
2 | import byImmutable from './exampleImmutable';
3 | import byDMutation from './exampleDeepMutation';
4 |
5 | document.getElementById('app').innerHTML = `
6 | `;
7 |
8 | console.log(byImmutable.toJS(), byDMutation);
9 | const diff = deepDiffEqual(byImmutable.toJS(), byDMutation, '');
10 |
11 | document.getElementById('objectTree').innerHTML = diffToHtml(diff);
12 |
--------------------------------------------------------------------------------
/ImmutableComparison/performanceResult.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/performanceResult.png
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2018
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # deep-mutation
2 |
3 | 1. [What is it?](#what-is-it)
4 | 2. [Installation](#installation)
5 | 3. [What does it do?](#installation)
6 | * [Simple example](#simple-example)
7 | * [Array specific keys](#array-specific-keys)
8 | * [mutate.deep(..) or deepPatch(...)](#mutatedeep-or-deeppatch)
9 | * [Deep-mutation can return updater-function](#deep-mutation-can-return-updater-function)
10 | 4. [Immutable comparison](#immutable-comparison)
11 | 5. [Tests cases / code example](#tests-cases--code-example)
12 | 6. [Use cases for 'deep-mutation'](#use-cases-for-deep-mutation)
13 | 7. [Live TODO Example in Codesandbox](https://codesandbox.io/s/deep-mutation-todo-1w022)
14 | 8. [Examples](./EXAMPLES.md)
15 |
16 | ## What is it?
17 |
18 | It is a simple function which gets an object and a list of changes and returns a new updated object.
19 |
20 | **Since the version 2.0.0 `deep-mutation` returns a new object only when something was changed**
21 |
22 | ## Installation
23 |
24 | ```
25 | npm install --save deep-mutation
26 | ```
27 |
28 | ## What does it do?
29 |
30 | ### Getting a new object with changes
31 | #### with plain JavaScript
32 | ```javascript
33 | const obj = { a: 10, b: 20, c: { c1: 1, c2: 2, c3: { c31: 31 } }};
34 | const result = {
35 | ...obj,
36 | c: {
37 | ...obj.c,
38 | c3: {
39 | ...obj.c3,
40 | c32: 25
41 | }
42 | }
43 | };
44 | ```
45 |
46 | #### doing the same with `deep-mutation`
47 |
48 | ```javascript
49 | import mutate from 'deep-mutation';
50 | const resultMutate = mutate(obj, { 'c.c3.c32': 25 });
51 | // OR
52 | const resultMutate = mutate(obj, [['c.c3.c32', 25]]);
53 | // OR since v2.1.0
54 | const resultMutate = mutate(obj, [[['c', 'c3', 'c32'], 25]]);
55 | // OR since v3.0.0
56 | const resultMutate = mutate.deep(obj, { c: { c3: { c32: 25 } } });
57 | ```
58 |
59 | ### Simple example
60 | ```javascript
61 | import mutate from 'deep-mutation';
62 |
63 | const myObject = {
64 | a: 100,
65 | b: 200,
66 | c: {
67 | c1: 1,
68 | c2: 2
69 | },
70 | d: []
71 | };
72 |
73 | const changes = [
74 | ['a', 111],
75 | ['b.b1', 222],
76 | ['b.b2', 'text'],
77 | ['c.c1', 20],
78 | ['c.c2'],
79 | ['d.[]', 10],
80 | ['d.[]', 20],
81 | ['e', [1,2,3]]
82 | ];
83 |
84 | const result = mutate(myObject, changes);
85 | ```
86 |
87 | #### 'result' will be
88 | ```javascript
89 | const obj = {
90 | a: 111,
91 | b: {
92 | b1: 222,
93 | b2: 'text'
94 | },
95 | c: {
96 | c1: 20
97 | },
98 | d: [10,20],
99 | e: [1,2,3]
100 | };
101 | ```
102 |
103 | ### Changes can be specified as an array of arrays or an object where each key is a path or object converted by `deepPatch` function:
104 |
105 | ```javascript
106 | // array of arrays
107 | const changes = [
108 | ['a', 111],
109 | ['b.b1', 222],
110 | ['b.b2', 'text'],
111 | ['c.c1', 20],
112 | ['c.c2'],
113 | ['d.[]', 10],
114 | ['d.[]', 20],
115 | ['e', [1,2,3]]
116 | ];
117 | ```
118 |
119 | **OR**
120 |
121 | ```javascript
122 | // object
123 | const changes = {
124 | a: 111,
125 | 'b.b1': 222,
126 | 'b.b2': 'text',
127 | 'c.c1': 20,
128 | 'c.c2': undefined,
129 | 'd.[+123412]': 10,
130 | 'd.[+544555]': 20,
131 | e: [1,2,3]
132 | };
133 | ```
134 |
135 | **OR**
136 |
137 | ```javascript
138 | // deep patch
139 | import { deepPatch } from 'deep-mutation';
140 |
141 | const changes = deepPatch({
142 | a: 111,
143 | b: { b1: 222, b2: 'text' },
144 | c: { c1: 20, c2: undefined },
145 | d: { '[+123412]': 10, '[+544555]': 20 },
146 | e: [1,2,3]
147 | });
148 | ```
149 |
150 | ## Array specific keys
151 |
152 | ### a.[] (or a.[+242234])
153 |
154 | If a key for an array item starts from `+`, then the **value will be appended to the end of the array** like `[].push()`.
155 | ```javascript
156 | const patch = {
157 | 'arr.[+123312312]': 100,
158 | // OR
159 | 'arr.[+6]': 100,
160 | // will be equal to
161 | 'arr.[]': 100,
162 | };
163 | ```
164 |
165 | It is useful when you need to add some items to an array and use changes as an Object.
166 | ```javascript
167 | import muatate from 'deep-mutation';
168 | // ...
169 | return mutate(
170 | { arr: [] },
171 | {
172 | // It is an error because JS object can't have values with the same keys!
173 | // 'arr.[]': 1,
174 | // 'arr.[]': 2,
175 |
176 | //It is the correct way
177 | 'arr.[+1123]': 1,
178 | 'arr.[+232]': 2,
179 | 'arr.[+43534]': 3,
180 | 'arr.[+64]': 4,
181 | 'arr.[]': 5,
182 | }
183 | );
184 |
185 | // the result will be = { arr: [1,2,3,4,5] }
186 | ```
187 |
188 | ### arr.[=10] or arr.[=id=15]
189 |
190 | If a key for an array item starts from `=` ([=10] or [=data.id=99]), then index will be found by comparison item or item's property and value. `[=field.path=value]`.
191 | ```javascript
192 | import muatate from 'deep-mutation';
193 | // ...
194 | return mutate(
195 | { arr: [1,2,3,4,5,6,7,8] },
196 | {
197 | 'arr.[=2]': 200,
198 | // index for element with value `2` will be `1`
199 | 'arr.[=8]': 800,
200 | // index for element with value `8` will be `7`
201 | 'arr.[=100]': 'undefined',
202 | // `100` is not found in arr and will be ignored, index is `-1`
203 | }
204 | );
205 |
206 | // the result will be = { arr: [1,200,3,4,5,6,7,800] }
207 | ```
208 | `arr.[=]` or `arr.[=value=]` - to find empty string value in array (or item.value = '')
209 | `arr.[=false]` or `arr.[=value=]` - to find 'false' value in array (or item.value = false)
210 |
211 | Example for objects
212 | ```javascript
213 | import muatate from 'deep-mutation';
214 | // ...
215 | return mutate(
216 | { arr: [{ id: 10 }, { id: 20 }] },
217 | {
218 | 'arr.[=id=20].name': 'Name 20',
219 | // index for element with `id=20` will be `1`
220 | 'arr.[=id=999]': 'undefined',
221 | // it is not found, ignored
222 | }
223 | );
224 |
225 | // the result will be = { arr: [{ id: 10 }, { id: 20, name: 'Name 20' }] }
226 | ```
227 |
228 | Example with deep path
229 | ```javascript
230 | import muatate from 'deep-mutation';
231 | // ...
232 | return mutate(
233 | { arr: [{ data: { id: 12 }}, { data: { id: 30 }}] },
234 | {
235 | 'arr.[=data.id=12].data.v': 'value1',
236 | // index for element with `data.id=12` will be `0`
237 | 'arr.[=data.id=999]': 'undefined',
238 | // it is not found, ignored
239 | }
240 | );
241 |
242 | // the result will be = { arr: [{ data: { id: 12, v: 'value1' }}, { data: { id: 30 }}] }
243 | ```
244 |
245 | ### arr.[-1]
246 |
247 | It will be **ignored**.
248 |
249 | ```javascript
250 | import muatate from 'deep-mutation';
251 | // ...
252 | return mutate(
253 | { arr: [1,2,3,4] },
254 | { 'arr.[-1]': 999 }
255 | );
256 |
257 | // the result will be = { arr: [1,2,3,4] }
258 | ```
259 |
260 | ### arr.[>2]
261 |
262 | It will insert element onto provided position.
263 |
264 | ```javascript
265 | import muatate from 'deep-mutation';
266 | // ...
267 | return mutate(
268 | { arr: [1,2,3,4] },
269 | { 'arr.[>2]': 555 },
270 | { 'arr.[>3]': 999 },
271 | );
272 |
273 | // the result will be = { arr: [1,2,555,999,3,4] }
274 | ```
275 |
276 | ## mutate.deep(...) or deepPatch(...)
277 |
278 | `deepPatch(patchObject: Object): PatchObject`
279 |
280 | `mutate.deep(sourceObject: Object, patchObject: Object): Object`
281 |
282 | ```javascript
283 | import mutate from 'deep-mutation';
284 |
285 | return mutate.deep(
286 | { a: 10, b: { b1: 1, b2: 2 }}, // main object
287 | { c: 50, b: { b2: 100 } } // changes
288 | );
289 |
290 | // result = { a: 10, b: { b1: 1, b2: 100 }, c: 50}
291 | ```
292 |
293 | **OR**
294 |
295 | ```javascript
296 | import mutate, { deepPatch } from 'deep-mutation';
297 |
298 | return mutate(
299 | { a: 10, b: { b1: 1, b2: 2 }}, // main object
300 | deepPatch({ c: 50, b: { b2: 100 } }) // changes
301 | );
302 |
303 | // result = { a: 10, b: { b1: 1, b2: 100 }, c: 50}
304 | ```
305 |
306 | * [deepPatch test cases / examples](./src/tests/deepToMutate.test.js)
307 |
308 | * [mutate.deep test cases / examples](./src/tests/mutateDeep.test.js)
309 |
310 | ## Deep-mutation can return updater-function
311 |
312 | If `deep-mutation` function is called only with one argument (an object without changes) then it will return a **function** which can take one argument as changes. When called, it will save the changes and return an updated object.
313 |
314 |
315 | ```javascript
316 | import mutate from 'deep-mutation';
317 |
318 | const patch = mutate({ a: 1, b: 2});
319 |
320 | const result1 = patch({ c: 3 });
321 | // result1 === { a: 1, b: 2, c: 3}
322 |
323 | const result2 = patch({ d: 4 });
324 | // result2 === { a: 1, b: 2, c: 3, d: 4}
325 |
326 | const result3 = patch();
327 | // result3 === result2 === { a: 1, b: 2, c: 3, d: 4}
328 | ```
329 |
330 | ## `deep-mutation` supports dots in path since v2.1.0
331 |
332 | In order to use dots in the path of changes you should use the path as an Array of keys:
333 |
334 | `mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [[['a', 'a.b', 'a.b.c'], newValue]])`
335 |
336 | **OR**
337 |
338 | `mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [['a-a.b-a.b.c'.split('-'), newValue]])`
339 |
340 |
341 | ```javascript
342 | import mutate from 'deep-mutation';
343 |
344 | const obj = {
345 | a: {
346 | 'a.1': {
347 | 'a.1.1': 100
348 | }
349 | }
350 | };
351 |
352 | const changes = [['a-a.1-a.1.1'.split('-'), 15]]
353 |
354 | const result = mutate(obj, changes);
355 | // result === { a: { a.1: { a.1.1: 15 } } }
356 | ```
357 |
358 | ## Immutable comparison
359 | [ImmutableComparison.md](./ImmutableComparison.md)
360 |
361 | ## Performance comparison
362 | **Sandbox editor**: https://codesandbox.io/s/l9ovomzv99
363 |
364 | **Sandbox view**: https://l9ovomzv99.csb.app/
365 |
366 | 
367 |
368 | ## Syntax comparison
369 | **Sandbox editor**: https://codesandbox.io/s/j4wkq2znj5
370 |
371 | **Sandbox view**: https://j4wkq2znj5.codesandbox.io/
372 |
373 | 
374 |
375 | # Tests cases / code example
376 | ### [Go to tests](./src/tests)
377 |
378 |
379 | ```javascript
380 | mutate({ a: 10 }, [['a', 5]]); // { a: 5 }
381 | mutate({ a: 10 }, [['b', 5]]); // { a: 10, b: 5 }
382 | mutate({}, [['a', 10], ['b', 5]]); // { a: 10, b: 5 }
383 | mutate({ a: 10 }, [['a']]); // { }
384 | mutate({ a: 10 }, [null]); // { a: 10 }
385 | mutate({ a: 10 }, [['a']]); // ['b']]); // { }
386 | mutate({ a: 10 }, ['a', 'b']); // { }
387 | mutate({ a: 10 }, [['a']]); // ['b', 5]]); // { b: 5 }
388 | mutate({ a: 10 }, [['a', [1,2,3]]]); // { a: [1,2,3] }
389 | mutate({ a: 10 }, [['a', { aa: 1 }]]); // { a: { aa: 1 } }
390 | mutate({ a: 10 }, [['a', 5], ['b', { bb: 2 }]]); // { a: 5, b: { bb: 2 } }
391 | // extend an object
392 | mutate({ a: { aa: 10 } }, [['a.aa', 5]]); // { a: { aa: 5 } }
393 | mutate({ a: { aa: 10 } }, [['a.aa']]); // { a: { } }
394 | mutate({ a: { aa: { aaa: 10 } } }, [['a.aa'], ['a.aa.aaa']]) // { a: { } }
395 | mutate({ a: { aa: 10 } }, [['a.aa.[]', 1]]); // { a: { aa: [1] } }
396 | mutate({ a: { aa: 10 } }, [['a.aa'], ['a']]); // { }
397 | mutate({ a: { aa: 10 } }, ['a.aa', 'a']); // { }
398 | mutate({ a: 10 }, [['a.aa', 5]]); // { a: { aa: 5 } }
399 | mutate({ a: 10 }, [['a.aa.aaa', 5]]); // { a: { aa: { aaa: 5 } } }
400 | mutate({ a: 10 }, [['a.aa.aaa', 5], ['a.aa.aaa.aaaa', 2]]); // { a: { aa: { aaa: { aaaa: 2 } } } }
401 | mutate({ a: 10 }, [['a.aa', 5], ['a.aa2', 2]]); // { a: { aa: 5, aa2: 2 } }
402 | mutate({ a: 10 }, [['a.aa', 5], ['b.bb', 2]]); // { a: { aa: 5 }, b: { bb: 2 } }
403 | // extend an array
404 | mutate([], [['[]', 5]]); // [5]
405 | mutate({ a: [] }, [['a.[]', 5]]); // { a: [5] }
406 | mutate({ a: [] }, [['a.[0]', 5]]); // { a: [5] }
407 | mutate({ a: [] }, [['a[0]', 5]]); // { a: [5] }
408 | mutate({ a: [] }, [['a[][]', 5]]); // { a: [[5]] }
409 | mutate({ a: [] }, [['a.[].[]', 5]]); // { a: [[5]] }
410 | mutate({ a: [] }, [['a.[2]', 5]]); // { a: [undefined, undefined, 5] }
411 | mutate({ a: [1] }, [['a.[]', 5]]); // { a: [1, 5] }
412 | mutate({ a: [1] }, [['a.[]', 5],['a.[]', 7]]); // { a: [1, 5, 7] }
413 | mutate({ a: [1] }, [['a.[0]', 5]]); // { a: [5] }
414 | mutate({ a: [1] }, [['a.[0]']]); // { a: [] }
415 | // changes as an object
416 | mutate({ a: [] }, { 'a.[]': 5 }); // { a: [5] }
417 | mutate({ a: [] }, { 'a.[0]': 5 }); // { a: [5] }
418 | mutate({ a: [] }, { 'a.[2]': 5 }); // { a: [undefined, undefined, 5] }
419 | mutate({ a: [1] }, { 'a.[]': 5 }); // { a: [1, 5] }
420 | mutate({ a: [1] }, { 'a.[0]': 5 }); // { a: [5] }
421 | mutate({ a: { aa: 10 } }, { 'a.aa': 5 }); // { a: { aa: 5 } }
422 | mutate({ a: { aa: 10 } }, { 'a.aa': undefined, 'a.aaa': 99 }); // { a: { aaa: 99 } }
423 | mutate({ }, { 'a.aa.aaa': undefined }); // { }
424 | mutate({ }, [['a.aa.aaa']]); // { }
425 | mutate({ a: { 0: 'v0', 1: 'v1' } }, [['a.0']]); // { a: { 1: 'v1' } }
426 | mutate({ a: [1,2,3] }, [['a.[]']]); // { a: [1,2,3] }
427 | mutate({ a: [1,2,3] }, [['a.[0]']]); // { a: [2,3] }
428 | mutate({ a: [1,2,3] }, [['a.0']]); // { a: [undefined, 2,3] }
429 | // set the object, extend the object
430 | mutate({ }, [['a', { aa: 5 }]]) // { a: { aa: 5 } }
431 | mutate({ a: 10 }, [['a', { aa: 5 }]]); // { a: { aa: 5 } }
432 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]) // { a: { aa: { aaa: 5 } } }
433 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]); // { a: { aa: { aaa: 5 } } }
434 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }], ['a.aa.aaa2', 1]]); // { a: { aa: { aaa: 5, aaa2: 1 } } }
435 | mutate({ a: 10 }, [['a', { aa: { aaa: 5, aaa2: 1 } }], ['a.aa.aaa2']]); // { a: { aa: { aaa: 5 } } }
436 | mutate({ a: 10 }, [['a', { aa: 5 }], ['a', [1,2,3]]]) // { a: [1,2,3] }
437 | mutate({ a: 10 }, [['a', { aa: 5 }], ['a.aa', 12]]) // { a: { aa: 12 } }
438 | mutate({ b: 20 }, [['a', { aa: 5 }], ['a']]) // { b: 20 }
439 | mutate({ b: 20 }, [['a', { aa: 5 }], ['a.aa']]) // { a: { }, b: 20 }
440 | ```
441 | ### Tests for complex changes
442 | ```javascript
443 | mutate({ a: 10, b: [], c: {} }, { a: 50, b: { b1: 10 }, c: [1,2,3] })
444 | // { a: 50, b: { b1: 10 }, c: [1,2,3] }
445 |
446 | mutate(
447 | { a: 10, b: [], c: {}, d: { d1: 12 }, e: [9,8,7] },
448 | {
449 | a: 50,
450 | b: { b1: 10 },
451 | c: [1,2,3],
452 | 'c.[]': { cc: 22 },
453 | 'b.b2': 17,
454 | 'd.d2': 15,
455 | 'e.[0]': 1,
456 | 'e.[]': 3
457 | }
458 | )
459 | /*
460 | {
461 | a: 50,
462 | b: { b1: 10, b2: 17 },
463 | c: [1,2,3, { cc: 22 }],
464 | d: { d1: 12, d2: 15 },
465 | e: [1,8,7,3]
466 | }
467 | */
468 |
469 | mutate(
470 | { a: { a1: { a1_1: 22 } }, b: [{ b1: 10 }], c: [{ c1: 1 }] },
471 | {
472 | 'a.a1.a1_1': 33,
473 | 'a.a1.a1_2': 9,
474 | 'a.a2': 14,
475 | 'b.[0].b1': 11,
476 | 'b.[]': 15,
477 | 'b.[0].b2': null,
478 | 'c[0].c1': undefined,
479 | 'c[0]': 7
480 | }
481 | )
482 | /*
483 | {
484 | a: {
485 | a1: { a1_1: 33, a1_2: 9 },
486 | a2: 14
487 | },
488 | b: [{ b1: 11, b2: null }, 15],
489 | c: [7]
490 | }
491 | */
492 |
493 | mutate(
494 | { a: 10, b: 20 },
495 | {
496 | a: { a1: 1, a2: 2 },
497 | 'a.a3.a3_1': 20, b: [1,2,3,{ b1: 1 }],
498 | 'b.[]': 11,
499 | 'b[3].b2.b2_1.b2_1_1': 'b2_1_1 value',
500 | 'c.[]': 14
501 | }
502 | )
503 | /*
504 | {
505 | a: {
506 | a1: 1,
507 | a2: 2,
508 | a3: { a3_1: 20 }
509 | },
510 | b: [
511 | 1,2,3, {
512 | b1: 1,
513 | b2: {
514 | b2_1: { b2_1_1: 'b2_1_1 value' }
515 | }
516 | },
517 | 11
518 | ],
519 | c: [14]
520 | }
521 | */
522 | ```
523 |
524 | ### It returns the same object (**works since version 2.0.0**)
525 | ```javascript
526 | const obj = { a: 10 };
527 | const result = mutate(obj, []);
528 | expect(result).not.toBe(obj);
529 | ```
530 |
531 | # (!!!) Attention! Важно! Achtung!
532 | If you use an instance of `Object` (or `Array`) to construct a list of changes then this instance of `Object` (or `Array`) will be changed.
533 | ### Test cases
534 | ```javascript
535 | test('should change object value', () => {
536 | const obj = { b: [] };
537 | const patchObject = { b1: 1, b2: 2, b3: 3 };
538 | const changes = [
539 | ['b', patchObject],
540 | ['b.b4', 4]
541 | ];
542 | const resut = mutate(obj, changes);
543 |
544 | expect(resut.b).toEqual(patchObject);
545 | expect(resut.b).toBe(patchObject);
546 | expect(patchObject).toEqual({ b1: 1, b2: 2, b3: 3, b4: 4 });
547 | });
548 |
549 | test('should change array value', () => {
550 | const obj = { b: [5,6] };
551 | const patchArray = [1,2,3];
552 | const changes = [
553 | ['b', patchArray],
554 | ['b.[]', 4]
555 | ];
556 | const resut = mutate(obj, changes);
557 |
558 | expect(resut.b).toEqual(patchArray);
559 | expect(resut.b).toBe(patchArray);
560 | expect(patchArray).toEqual([1,2,3,4]);
561 | });
562 | ```
563 |
564 | # Use cases for 'deep-mutation'
565 |
566 | ## In redux
567 | ```javascript
568 | import mutate from 'deep-mutation';
569 |
570 | export default (state = {}, action) => {
571 | const { type, payload } = action;
572 | const { uid } = payload;
573 |
574 | switch (type) {
575 | case 'API_REQUEST':
576 | return mutate(state, [
577 | [`${uid}._status`, 'request']
578 | ]);
579 |
580 | case 'API_REQUEST__OK':
581 | return mutate(state, [
582 | [`${uid}._status`, 'success'],
583 | [`${uid}.result`, payload.body],
584 | ]);
585 |
586 | case 'API_REQUEST__FAIL':
587 | return mutate(state, [
588 | [`${uid}._status`, 'fail'],
589 | [`${uid}.result`],
590 | [`${uid}.error`, payload.error]
591 | ]);
592 |
593 | default:
594 | return state;
595 | }
596 | };
597 | // ...
598 | ```
599 |
600 | ## In component's state
601 | ```javascript
602 | import mutate from 'deep-mutation';
603 |
604 | class ExampleComponent extends Component {
605 | // ...
606 | onClick = () => this.setState(state =>
607 | mutate(state, {
608 | isFetching: true,
609 | 'data.value': 'default',
610 | 'data.key': null,
611 | validation: null
612 | })
613 | );
614 | // ...
615 | }
616 | ```
617 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | ],
4 | "presets": [
5 | ["@babel/preset-env", {
6 | "useBuiltIns": "entry",
7 | "corejs": "3.24",
8 | "loose": true,
9 | "forceAllTransforms": false,
10 | "targets": "ie 11"
11 | }]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/deepMutationTodo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deep-mutation-todo",
3 | "version": "1.0.0",
4 | "description": null,
5 | "keywords": [
6 | "deep-mutation",
7 | "immutable",
8 | "todo"
9 | ],
10 | "main": "src/index.js",
11 | "dependencies": {
12 | "classnames": "2.2.6",
13 | "deep-mutation": "2.1.0",
14 | "react": "16.12.0",
15 | "react-dom": "16.12.0",
16 | "react-scripts": "3.0.1"
17 | },
18 | "devDependencies": {
19 | "typescript": "3.3.3"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test --env=jsdom",
25 | "eject": "react-scripts eject"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ]
33 | }
--------------------------------------------------------------------------------
/deepMutationTodo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
27 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./styles.css";
3 |
4 | import TodosList from "./components/TodosList";
5 |
6 | export default function App() {
7 | return (
8 |
9 |
Hello deep-mutatuon Todo example
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import "./modal.css";
3 |
4 | export default class Modal extends PureComponent {
5 | render() {
6 | const { children } = this.props;
7 |
8 | return (
9 |
12 | );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/Steps.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import cn from "classnames";
3 | import "./steps.css";
4 |
5 | export default class Steps extends PureComponent {
6 | onChangeStep = ({ target }) => {
7 | const { toggleStep } = this.props;
8 | toggleStep(Number(target.dataset.id));
9 | };
10 |
11 | onRemoveStep = ({ target }) => {
12 | const { removeStep } = this.props;
13 | removeStep(Number(target.dataset.id));
14 | };
15 |
16 | render() {
17 | const { steps } = this.props;
18 | if (steps.length === 0) return null;
19 | return (
20 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/TodoEditor.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import Modal from "./Modal";
3 | import "./todoEditor.css";
4 |
5 | export default class TodoEditor extends PureComponent {
6 | state = {
7 | errors: []
8 | };
9 |
10 | onSubmit = event => {
11 | event.preventDefault();
12 | const { target } = event;
13 | const { onConfirm } = this.props;
14 | const errors = [];
15 | const title = target.title.value.trim();
16 | const description = target.description.value.trim();
17 |
18 | if (!title) errors.push("Title can not be empty");
19 | if (errors.length > 0) return this.setState({ errors });
20 |
21 | onConfirm(title, description);
22 | };
23 |
24 | render() {
25 | const { title, description, id, onCancel } = this.props;
26 | const { errors } = this.state;
27 |
28 | return (
29 |
30 |
56 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/TodoRemovingConfirmation.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import Modal from "./Modal";
3 |
4 | export default class TodoConfirmation extends PureComponent {
5 | render() {
6 | const { title, onConfirm, onCancel } = this.props;
7 |
8 | return (
9 |
10 | Do you want remove '{title}'?
11 |
12 |
15 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/TodoView.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import TodoEditor from "./TodoEditor";
3 | import TodoRemovingConfirmation from "./TodoRemovingConfirmation";
4 | import Steps from "./Steps";
5 | import "./todoView.css";
6 |
7 | export default class TodoView extends PureComponent {
8 | state = {
9 | errors: [],
10 | isEditing: false,
11 | isRemoving: false
12 | };
13 |
14 | onClickEdit = () => this.setState({ isEditing: true });
15 |
16 | onCancelEdit = () => this.setState({ isEditing: false });
17 |
18 | onClickRemove = () => this.setState({ isRemoving: true });
19 |
20 | onCancelRemove = () => this.setState({ isRemoving: false });
21 |
22 | onConfirmEdit = (title, description) => {
23 | const { save, id } = this.props;
24 | this.onCancelEdit();
25 | save(id, title, description);
26 | };
27 |
28 | onConfirmRemove = (title, description) => {
29 | const { remove, id } = this.props;
30 | this.onCancelRemove();
31 | remove(id);
32 | };
33 |
34 | onSubmitNewStep = event => {
35 | event.preventDefault();
36 | const value = event.target.stepText.value.trim();
37 | if (!value) return null;
38 | const { addStep, id } = this.props;
39 | addStep(id, value);
40 | event.target.stepText.value = "";
41 | };
42 |
43 | onChangeStep = idStep => {
44 | const { toggleStep, id } = this.props;
45 | toggleStep(id, idStep);
46 | };
47 |
48 | onRemoveStep = idStep => {
49 | const { removeStep, id } = this.props;
50 | removeStep(id, idStep);
51 | };
52 |
53 | render() {
54 | const { title, description, steps, id } = this.props;
55 | const { isEditing, isRemoving } = this.state;
56 |
57 | return (
58 |
59 |
{title}
60 |
{description}
61 |
62 |
63 |
66 |
69 |
70 |
71 |
76 |
77 |
81 |
82 | {isEditing && (
83 |
90 | )}
91 | {isRemoving && (
92 |
97 | )}
98 |
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/TodosList.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import mutate from "deep-mutation";
3 | import TodoEditor from "./TodoEditor";
4 | import TodoView from "./TodoView";
5 | import "./todosList.css";
6 |
7 | export default class TodosList extends PureComponent {
8 | state = {
9 | todos: [],
10 | isAdding: false
11 | };
12 |
13 | randomId() {
14 | return Math.floor(Math.random() * 1000000);
15 | }
16 |
17 | addTodo = (title, description) => {
18 | const { todos } = this.state;
19 | this.setState({
20 | // instead of
21 | // todos: todos.concat({ id: this.randomId(), title, description, steps: [] })
22 | // I know, it looks good =)
23 | todos: mutate(todos, [
24 | ["[]", { id: this.randomId(), title, description, steps: [] }]
25 | ]),
26 | isAdding: false
27 | });
28 | };
29 |
30 | saveTodo = (todoId, title, description) => {
31 | const { todos } = this.state;
32 | const n = todos.findIndex(el => el.id === todoId);
33 | if (n < 0) return;
34 | this.setState({
35 | // instead of
36 | // const newTodos = todos.slice(0, n)
37 | // .concat(
38 | // {...todos[n], title, description},
39 | // todos.slice(n+1)
40 | // )
41 | // it looks not so cool already, and little difficult to read and understend
42 | todos: mutate(todos, {
43 | [`[${n}].title`]: title,
44 | [`[${n}].description`]: description
45 | })
46 | });
47 | };
48 |
49 | removeTodo = todoId => {
50 | const { todos } = this.state;
51 | const n = todos.findIndex(el => el.id === todoId);
52 | if (n < 0) return;
53 | this.setState({
54 | // instead of
55 | // todos: todos.slice(0, n).concat(todos.slice(n+1))
56 | todos: mutate(todos, {
57 | [`[${n}]`]: undefined
58 | })
59 | });
60 | };
61 |
62 | addTodoStep = (todoId, text) => {
63 | const { todos } = this.state;
64 | const n = todos.findIndex(el => el.id === todoId);
65 | if (n < 0) return;
66 | const newStep = { id: this.randomId(), text, done: false };
67 | this.setState({
68 | // instead of
69 | // todos: todos.slice(0, n)
70 | // .concat(
71 | // {...todos[n], steps: todos[n].steps.concat(newStep)},
72 | // todos.slice(n+1)
73 | // )
74 | todos: mutate(todos, [[`[${n}].steps.[]`, newStep]]),
75 | isAdding: false
76 | });
77 | };
78 |
79 | toggleTodoStep = (todoId, stepId) => {
80 | const { todos } = this.state;
81 | const n = todos.findIndex(el => el.id === todoId);
82 | if (n < 0) return;
83 | const m = todos[n].steps.findIndex(el => el.id === stepId);
84 | if (m < 0) return;
85 | this.setState({
86 | // instead of
87 | // todos: todos.slice(0, n)
88 | // .concat(
89 | // {...todos[n], steps: todos[n].steps.slice(0, m).concat({ ...todos[n].steps[m], done: !todos[n].steps[m].done}, todos[n].steps.slice(m + 1))},
90 | // todos.slice(n+1)
91 | // )
92 | // What is easier to do?
93 | todos: mutate(todos, [
94 | [`[${n}].steps[${m}].done`, !todos[n].steps[m].done]
95 | ])
96 | });
97 | };
98 |
99 | removeTodoStep = (todoId, stepId) => {
100 | const { todos } = this.state;
101 | const n = todos.findIndex(el => el.id === todoId);
102 | if (n < 0) return;
103 | const m = todos[n].steps.findIndex(el => el.id === stepId);
104 | if (m < 0) return;
105 | this.setState({
106 | // instead of
107 | // todos: todos.slice(0, n)
108 | // .concat(
109 | // {...todos[n], steps: todos[n].steps.slice(0, m).concat(todos[n].steps.slice(m + 1))},
110 | // todos.slice(n+1)
111 | // )
112 | todos: mutate(todos, [[`[${n}].steps[${m}]`]])
113 | });
114 | };
115 |
116 | onClickAdd = () => this.setState({ isAdding: true });
117 |
118 | onClickCancelAdd = () => this.setState({ isAdding: false });
119 |
120 | render() {
121 | const { isAdding, todos } = this.state;
122 |
123 | return (
124 |
125 |
126 |
129 |
130 |
131 | {isAdding && (
132 |
133 |
137 |
138 | )}
139 |
140 |
141 | {todos.map(({ id, title, description, steps = [] }) => (
142 |
154 | ))}
155 |
156 |
157 | );
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/modal.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | background: #e1e1e1;
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | }
12 |
13 | .modalContent {
14 | background: white;
15 | flex: 0 1 0;
16 | border: 1px solid blueviolet;
17 | padding: 10px;
18 | margin-top: 30px;
19 | }
20 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/steps.css:
--------------------------------------------------------------------------------
1 | .steps {
2 | list-style: none;
3 | }
4 |
5 | .step button {
6 | margin-left: 10px;
7 | }
8 |
9 | .step:not(:last-child) {
10 | margin-bottom: 10px;
11 | }
12 |
13 | .step.m-done {
14 | opacity: 0.5;
15 | text-decoration: line-through;
16 | }
17 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/todoEditor.css:
--------------------------------------------------------------------------------
1 | .todoEditor input {
2 | display: block;
3 | margin-bottom: 10px;
4 | width: 100%;
5 | padding: 5px;
6 | }
7 |
8 | .todoEditor textarea {
9 | display: block;
10 | width: 100%;
11 | padding: 5px;
12 | }
13 |
14 | .todoEditor_controls {
15 | margin-top: 10px;
16 | }
17 |
18 | .todoEditor_errors {
19 | color: #a40917;
20 | margin: 10px 0 0 10px;
21 | padding: 0 0 0 5px;
22 | }
23 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/todoView.css:
--------------------------------------------------------------------------------
1 | .todoView {
2 | margin-top: 10px;
3 | border-bottom: 1px dotted green;
4 | padding: 5px;
5 | }
6 |
7 | .todoView_title {
8 | font-weight: 600;
9 | }
10 |
11 | .todoView_description {
12 | font-size: 75%;
13 | padding: 5px 10px;
14 | }
15 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/components/todosList.css:
--------------------------------------------------------------------------------
1 | .todos {
2 | padding: 10px;
3 | }
4 |
5 | .todos_controls {
6 | padding: 5px 0;
7 | border-bottom: 2px solid #e1e1e1;
8 | text-align: right;
9 | }
10 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import App from "./App";
5 |
6 | const rootElement = document.getElementById("root");
7 | ReactDOM.render(, rootElement);
8 |
--------------------------------------------------------------------------------
/deepMutationTodo/src/styles.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | }
4 |
5 | * {
6 | box-sizing: border-box;
7 | }
8 |
9 | button {
10 | cursor: pointer;
11 | }
12 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | exports.__esModule = true;
4 | exports.CONFIG = void 0;
5 | exports.XMutateLockedElementX = XMutateLockedElementX;
6 | exports.XMutateRemovedElementX = XMutateRemovedElementX;
7 | exports.checkIsExists = checkIsExists;
8 | exports.deepPatch = deepPatch;
9 | exports.default = void 0;
10 | exports.extToArray = extToArray;
11 | exports.extToTree = extToTree;
12 | exports.getObjectPaths = getObjectPaths;
13 | exports.getOptions = getOptions;
14 | exports.getRealIndex = getRealIndex;
15 | exports.getValue = getValue;
16 | exports.isArrayElement = isArrayElement;
17 | exports.separatePath = separatePath;
18 | exports.splitPath = splitPath;
19 | var CONFIG = exports.CONFIG = {
20 | reportFunctionMutation: false,
21 | reportIncompatibleObjectType: false
22 | };
23 | function XMutateRemovedElementX() {}
24 | function XMutateLockedElementX(value) {
25 | this.__value__ = value;
26 | }
27 | function XDeepPatchX(value) {
28 | this.__value__ = value;
29 | }
30 | function XIssetMarkerX() {}
31 | var ARRAY_REGEXP = new RegExp('^\\[([^\\[\\]]*)]$');
32 | var MUTATE_TYPES = {
33 | ARRAY: 'array',
34 | OBJECT: 'object',
35 | DEFAULT: ''
36 | };
37 | function mutateObj(point, vType) {
38 | if (!point) return vType === MUTATE_TYPES.ARRAY ? [] : {};
39 | if (Array.isArray(point)) return [].concat(point);
40 | if (vType === MUTATE_TYPES.ARRAY) return [];
41 | if (checkIsObject(point)) return Object.assign({}, point);
42 | if (vType === MUTATE_TYPES.OBJECT) return {};
43 | return point;
44 | }
45 | function getObjectPaths(obj, prefix, map) {
46 | if (prefix === void 0) {
47 | prefix = [];
48 | }
49 | if (map === void 0) {
50 | map = null;
51 | }
52 | if (!checkIsNativeObject(obj)) return [];
53 | var keys = Object.keys(obj);
54 | var isRoot = !map;
55 | var myMap = isRoot
56 | // eslint-disable-next-line no-undef
57 | ? new Map() : map;
58 |
59 | // ignore objects that were listened. It means that recursive links will be ignored
60 | if (myMap.has(obj)) return null;
61 | myMap.set(obj, new XIssetMarkerX());
62 | for (var i = 0; i < keys.length; i++) {
63 | var value = obj[keys[i]];
64 | var currentPath = prefix.concat([keys[i]]);
65 | if (checkIsNativeObject(value)) {
66 | getObjectPaths(value, currentPath, myMap);
67 | } else if (checkIsDeepPatch(value)) {
68 | getObjectPaths(value.__value__, currentPath, myMap);
69 | } else {
70 | var containsDot = currentPath.some(function (el) {
71 | return el.indexOf('.') >= 0;
72 | });
73 | myMap.set(containsDot ? currentPath : currentPath.join('.'), value);
74 | }
75 | }
76 | if (isRoot) {
77 | var result = [];
78 | myMap.forEach(function (val, key) {
79 | if (val instanceof XIssetMarkerX) return false;
80 | result.push([key, val]);
81 | });
82 | return result;
83 | }
84 | return null;
85 | }
86 | function extToArray(pExt) {
87 | var result = pExt;
88 | if (!Array.isArray(pExt)) {
89 | if (checkIsDeepPatch(pExt)) {
90 | result = [pExt];
91 | } else {
92 | if (!checkIsNativeObject(pExt)) {
93 | console.error(new Error('Changes should be Object or Array'));
94 | return [];
95 | }
96 | result = Object.keys(pExt || {}).map(function (key) {
97 | return checkIsUndefined(pExt[key]) ? [key] : [key, pExt[key]];
98 | });
99 | }
100 | }
101 | return result.reduce(function (R, pair) {
102 | var isDeep = checkIsDeepPatch(pair);
103 | if (!isDeep && (!pair || pair.length < 2)) {
104 | return R;
105 | }
106 | var pairVal = isDeep ? pair : pair[1];
107 | if (isDeep || checkIsDeepPatch(pairVal)) {
108 | var pairPath = isDeep || !pair[0] ? undefined : splitPath(pair[0]);
109 | var n = R.findIndex(function (el) {
110 | return el === pair;
111 | });
112 | if (n < 0) return R;
113 | return R.slice(0, n).concat(getObjectPaths(pairVal.__value__, pairPath), R.slice(n + 1));
114 | }
115 | return R;
116 | }, result);
117 | }
118 | function separatePath(path) {
119 | return checkIsString(path) ? path.replace(new RegExp('([^.])(\\[)', 'g'), function (match, p1, p2) {
120 | return p1 + "." + p2;
121 | }) : path;
122 | }
123 | function splitPath(path) {
124 | if (checkIsString(path)) return path.split('.');
125 | // .split(/(?');
134 | var isRemove = checkIsRemoved(value);
135 | if (isRemove && (isArrayInsert || !hasProperty(parent, key))) {
136 | return parent;
137 | }
138 | var realParent = checkIsLocked(parent) ? parent.__value__ : parent;
139 | if (isRemove) {
140 | if (Array.isArray(realParent)) realParent.splice(key, 1);else delete realParent[key];
141 | return parent;
142 | }
143 | if (isArrayInsert) {
144 | var index = parseInt(key.slice(2).replace(']', ''), 10);
145 | realParent.splice(index, 0, value);
146 | return parent;
147 | }
148 | realParent[key] = value;
149 | return parent;
150 | }
151 | function checkIsUndefined(value) {
152 | return typeof value === 'undefined';
153 | }
154 | function checkIsRemoved(pObj) {
155 | return pObj instanceof XMutateRemovedElementX;
156 | }
157 | function checkIsLocked(pObj) {
158 | return pObj instanceof XMutateLockedElementX;
159 | }
160 | function checkIsDeepPatch(value) {
161 | return value instanceof XDeepPatchX;
162 | }
163 | function checkIsObject(value) {
164 | return value && typeof value === 'object';
165 | }
166 | function checkIsString(value) {
167 | return typeof value === 'string';
168 | }
169 | function checkIsFunction(value) {
170 | return value && typeof value === 'function';
171 | }
172 | function checkIsNativeObject(value) {
173 | return checkIsObject(value) && value.__proto__.constructor.name === 'Object';
174 | }
175 | // It has been exported for tests, but you could use it if needed
176 | function checkIsExists(pObject, pPath) {
177 | return !checkIsUndefined(getValue(pObject, pPath));
178 | }
179 | function getValue(pObject, pPath) {
180 | if (!pObject || !checkIsObject(pObject)) return undefined;
181 | var pieces = splitPath(pPath);
182 | if (pieces.length === 0) return pObject;
183 |
184 | // function preparePiece(piece) {
185 | // return piece.replace(/(\[|\])+/g, '');
186 | // }
187 |
188 | var lastIndex = pieces.length - 1;
189 | var node = pObject;
190 | for (var i = 0; i < lastIndex; i += 1) {
191 | var piece = getRealIndex(node, pieces[i]);
192 | node = checkIsLocked(node[piece]) ? node[piece].__value__ : node[piece];
193 | if (!node || !checkIsObject(node) || checkIsRemoved(node)) {
194 | return undefined;
195 | }
196 | }
197 | return node[getRealIndex(node, pieces[lastIndex])];
198 | }
199 | function isArrayElement(key) {
200 | return checkIsString(key) && ARRAY_REGEXP.test(key);
201 | }
202 | function extToTree(pExt, pSource) {
203 | var arrayCounter = 100;
204 | // +++++++++++++++++++++++++++
205 | function getNewValue(pair, isMutated) {
206 | if (!pair || pair.length === 0) return undefined;
207 | if (pair.length === 1 || checkIsUndefined(pair[1])) {
208 | return new XMutateRemovedElementX();
209 | }
210 | if (!isMutated && checkIsObject(pair[1])) {
211 | return new XMutateLockedElementX(pair[1]);
212 | }
213 | return pair[1];
214 | }
215 | // +++++++++++++++++++++++++++
216 | if (!checkIsObject(pExt)) {
217 | throw new Error('Changes should be Object or Array');
218 | }
219 | var values = extToArray(pExt);
220 | return values.reduce(function (FULL_RESULT, PAIR) {
221 | if (!PAIR) return FULL_RESULT;
222 | if (checkIsString(PAIR)) PAIR = [PAIR];
223 | if (!PAIR[0] && PAIR[0] !== 0) {
224 | throw new Error('Path should not be empty');
225 | }
226 | var pathPieces = splitPath(separatePath(PAIR[0]));
227 | if (PAIR.length < 2 || checkIsUndefined(PAIR[1])) {
228 | if (!(checkIsExists(pSource, pathPieces) || checkIsExists(FULL_RESULT, pathPieces))) {
229 | return FULL_RESULT;
230 | }
231 | } else if (getValue(pSource, pathPieces) === PAIR[1]) {
232 | return FULL_RESULT;
233 | }
234 | var isLockedPath = false;
235 | // console.log('--------------------');
236 | pathPieces.reduce(function (parent, currentKey, currentI) {
237 | var isLastPiece = currentI >= pathPieces.length - 1;
238 | var actualKey = currentKey === '[]' ? "[+" + ++arrayCounter + "]" : currentKey;
239 | var newKey = isLockedPath ? getOptions(parent, actualKey).realKey : actualKey;
240 | var isLockedCurrent = !isLockedPath && hasProperty(parent, newKey) && checkIsLocked(parent[newKey]);
241 | isLockedPath = isLockedPath || isLockedCurrent;
242 | if (isLastPiece) {
243 | var newValue = getNewValue(PAIR, isLockedPath);
244 | if (isLockedPath) setValue(parent, newKey, newValue);else parent[newKey] = newValue;
245 | // return ROOT of changes
246 | return FULL_RESULT;
247 | }
248 | var currentValue = isLockedCurrent ? parent[newKey].__value__ : parent[newKey];
249 | if (!checkIsObject(currentValue)) {
250 | if (currentValue && CONFIG.reportIncompatibleObjectType) {
251 | console.error(new Error("Warning: In \"" + PAIR[0] + "\", bad value for \"" + currentKey + "\", it will be replaced by empty Object ({})"));
252 | }
253 | var _newValue = {};
254 | if (isLockedPath) setValue(parent, newKey, _newValue);else parent[newKey] = _newValue;
255 |
256 | // return new position in tree
257 | return _newValue;
258 | }
259 |
260 | // return current position in tree
261 | return currentValue;
262 | }, FULL_RESULT);
263 | return FULL_RESULT;
264 | }, {});
265 | }
266 | function updateSection(point, tree) {
267 | if (!tree || Array.isArray(tree) || !checkIsObject(tree)) {
268 | return tree;
269 | }
270 | if (checkIsFunction(tree)) {
271 | if (CONFIG.reportFunctionMutation) {
272 | console.error(new Error('Function mutation'));
273 | }
274 | return tree(point);
275 | }
276 | if (checkIsLocked(tree)) return tree.__value__;
277 | var pieces = Object.keys(tree);
278 | var needArray = pieces.some(isArrayElement);
279 | var result = mutateObj(point, needArray ? MUTATE_TYPES.ARRAY : MUTATE_TYPES.OBJECT);
280 | pieces.forEach(function (key) {
281 | var opt = getOptions(result, key);
282 | var k = opt.realKey;
283 | if (checkIsRemoved(tree[key])) {
284 | if (opt.isArray) result.splice(k, 1);else delete result[k];
285 | return;
286 | }
287 | if (key && String(key).startsWith('[>')) {
288 | var index = parseInt(key.slice(2).replace(']', ''), 10);
289 | result.splice(index, 0, updateSection(result[index], tree[key]));
290 | return;
291 | }
292 | if (!opt.isArray || k >= 0) {
293 | result[k] = updateSection(result[k], tree[key]);
294 | }
295 | });
296 | return result;
297 | }
298 | function getRealIndex(items, key) {
299 | var parse = key ? ARRAY_REGEXP.exec(key) : null;
300 | if (!parse) return key;
301 | var k = parse[1].trim();
302 | if (k.startsWith('>')) {
303 | // [>2] || [>2...]
304 | return k;
305 | }
306 | var arrayItems = Array.isArray(items) ? items : [];
307 | if (k.length == 0 || k.startsWith('+')) {
308 | return arrayItems.length;
309 | }
310 | if (k.startsWith('=')) {
311 | // [=10] || [=id=99]
312 | var parseCompare = /^=(?:([^=\s]*)=)?(.*)$/.exec(k);
313 | if (parseCompare) {
314 | var _k = parseCompare[1],
315 | v = parseCompare[2];
316 | return arrayItems.findIndex(function (el) {
317 | return String(_k && el ? getValue(el, _k) : el) === String(v);
318 | });
319 | }
320 | }
321 | var index = parseInt(k, 10);
322 | return Number.isNaN(index) ? items.length : index;
323 | }
324 | function getOptions(parentValue, key) {
325 | var realParentValue = checkIsLocked(parentValue) ? parentValue.__value__ : parentValue;
326 | return {
327 | key: key,
328 | realKey: getRealIndex(realParentValue, key),
329 | isArray: isArrayElement(key),
330 | length: Array.isArray(realParentValue) ? realParentValue.length : 0
331 | };
332 | }
333 | function mutate(pObj, pExt) {
334 | if (!checkIsObject(pObj)) {
335 | throw new Error('Type of variable should be Object or Array');
336 | }
337 | if (checkIsUndefined(pExt)) {
338 | return toFunction(pObj);
339 | }
340 | var tree = extToTree(pExt, pObj);
341 | if (Object.getOwnPropertyNames(tree).length === 0) {
342 | return pObj;
343 | }
344 | return updateSection(pObj, tree);
345 | }
346 | function deepPatch(pExt) {
347 | if (checkIsDeepPatch(pExt)) return pExt;
348 | return new XDeepPatchX(pExt);
349 | }
350 | mutate.deep = function (pObj, pExt) {
351 | var newExt = null;
352 | if (Array.isArray(pExt)) {
353 | newExt = pExt.map(function (el) {
354 | return deepPatch(el);
355 | });
356 | } else newExt = deepPatch(pExt);
357 | return mutate(pObj, newExt);
358 | };
359 | function toFunction(pObj) {
360 | var result = pObj;
361 | return function (pExt) {
362 | if (checkIsUndefined(pExt)) return result;
363 | result = mutate(result, pExt);
364 | return result;
365 | };
366 | }
367 | var _default = exports.default = mutate;
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after the first failure
9 | // bail: false,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "E:\\@TEMP\\mrBear\\jest",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | collectCoverage: true,
22 |
23 | // The directory where Jest should output its coverage files
24 | coverageDirectory: 'coverage',
25 |
26 | // An array of regexp pattern strings used to skip coverage collection
27 | // coveragePathIgnorePatterns: [
28 | // "\\\\node_modules\\\\"
29 | // ],
30 |
31 | // A list of reporter names that Jest uses when writing coverage reports
32 | // coverageReporters: [
33 | // "json",
34 | // "text",
35 | // "lcov",
36 | // "clover"
37 | // ],
38 |
39 | coverageReporters: [
40 | 'text',
41 | 'lcov'
42 | ],
43 |
44 | // An object that configures minimum threshold enforcement for coverage results
45 | // coverageThreshold: null,
46 |
47 | // Make calling deprecated APIs throw helpful error messages
48 | // errorOnDeprecated: false,
49 |
50 | // Force coverage collection from ignored files usin a array of glob patterns
51 | // forceCoverageMatch: [],
52 |
53 | // A path to a module which exports an async function that is triggered once before all test suites
54 | // globalSetup: null,
55 |
56 | // A path to a module which exports an async function that is triggered once after all test suites
57 | // globalTeardown: null,
58 |
59 | // A set of global variables that need to be available in all test environments
60 | // globals: {},
61 |
62 | // An array of directory names to be searched recursively up from the requiring module's location
63 | // moduleDirectories: [
64 | // "node_modules"
65 | // ],
66 |
67 | // An array of file extensions your modules use
68 | // moduleFileExtensions: [
69 | // "js",
70 | // "json",
71 | // "jsx",
72 | // "node"
73 | // ],
74 |
75 | // A map from regular expressions to module names that allow to stub out resources with a single module
76 | // moduleNameMapper: {},
77 |
78 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
79 | // modulePathIgnorePatterns: [],
80 |
81 | // Activates notifications for test results
82 | // notify: false,
83 |
84 | // An enum that specifies notification mode. Requires { notify: true }
85 | // notifyMode: "always",
86 |
87 | // A preset that is used as a base for Jest's configuration
88 | // preset: null,
89 |
90 | // Run tests from one or more projects
91 | // projects: null,
92 |
93 | // Use this configuration option to add custom reporters to Jest
94 | // reporters: undefined,
95 |
96 | // Automatically reset mock state between every test
97 | // resetMocks: false,
98 |
99 | // Reset the module registry before running each individual test
100 | // resetModules: false,
101 |
102 | // A path to a custom resolver
103 | // resolver: null,
104 |
105 | // Automatically restore mock state between every test
106 | // restoreMocks: false,
107 |
108 | // The root directory that Jest should scan for tests and modules within
109 | // rootDir: null,
110 |
111 | // A list of paths to directories that Jest should use to search for files in
112 | // roots: [
113 | // ""
114 | // ],
115 |
116 | // Allows you to use a custom runner instead of Jest's default test runner
117 | // runner: "jest-runner",
118 | // runner: 'jest-runner-eslint',
119 | // displayName: 'lint',
120 |
121 | // The paths to modules that run some code to configure or set up the testing environment before each test
122 | // setupFiles: [],
123 |
124 | // The path to a module that runs some code to configure or set up the testing framework before each test
125 | // setupTestFrameworkScriptFile: null,
126 |
127 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
128 | // snapshotSerializers: [],
129 |
130 | // The test environment that will be used for testing
131 | testEnvironment: 'node',
132 |
133 | // Options that will be passed to the testEnvironment
134 | // testEnvironmentOptions: {},
135 |
136 | // Adds a location field to test results
137 | // testLocationInResults: false,
138 |
139 | // The glob patterns Jest uses to detect test files
140 | // testMatch: [
141 | // "**/__tests__/**/*.js?(x)",
142 | // "**/?(*.)+(spec|test).js?(x)"
143 | // ],
144 |
145 | testMatch: [
146 | '/src/**/*.test.js'
147 | ],
148 |
149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
150 | // testPathIgnorePatterns: [
151 | // "\\\\node_modules\\\\"
152 | // ],
153 |
154 | // The regexp pattern Jest uses to detect test files
155 | // testRegex: "",
156 |
157 | // This option allows the use of a custom results processor
158 | // testResultsProcessor: null,
159 |
160 | // This option allows use of a custom test runner
161 | // testRunner: "jasmine2",
162 |
163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
164 | // testURL: "about:blank",
165 |
166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
167 | // timers: "real",
168 |
169 | // A map from regular expressions to paths to transformers
170 | // transform: null,
171 |
172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
173 | // transformIgnorePatterns: [
174 | // "\\\\node_modules\\\\"
175 | // ],
176 |
177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
178 | // unmockedModulePathPatterns: undefined,
179 |
180 | // Indicates whether each individual test should be reported during the run
181 | // verbose: null,
182 |
183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
184 | // watchPathIgnorePatterns: [],
185 |
186 | // Whether to use watchman for file crawling
187 | // watchman: true,
188 | };
189 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deep-mutation",
3 | "version": "3.3.0",
4 | "author": {
5 | "name": "Shelest Denis",
6 | "email": "axules@gmail.com"
7 | },
8 | "bugs": {
9 | "url": "https://github.com/axules/deep-mutation/issues"
10 | },
11 | "contributors": [
12 | {
13 | "name": "Denis Shelest",
14 | "email": "axules@gmail.com"
15 | }
16 | ],
17 | "main": "./dist/index.js",
18 | "deprecated": false,
19 | "description": "The mutate method returns new object with changes",
20 | "keywords": [
21 | "assign",
22 | "object",
23 | "deep",
24 | "state",
25 | "mutable",
26 | "redux",
27 | "immutable"
28 | ],
29 | "license": "MIT",
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/axules/deep-mutation.git"
33 | },
34 | "scripts": {
35 | "test": "jest",
36 | "build": "npm test && babel -d dist ./src/index.js",
37 | "prepack": "npm run build"
38 | },
39 | "devDependencies": {
40 | "@babel/cli": "^7.24.8",
41 | "@babel/core": "^7.24.9",
42 | "@babel/eslint-parser": "^7.25.1",
43 | "@babel/node": "^7.25.0",
44 | "@babel/preset-env": "^7.25.0",
45 | "@babel/runtime-corejs3": "^7.25.0",
46 | "babel-jest": "^29.7.0",
47 | "babel-loader": "^9.1.3",
48 | "core-js": "^3.24.1",
49 | "eslint": "^8.57.0",
50 | "eslint-plugin-babel": "^5.3.1",
51 | "eslint-plugin-flowtype": "^8.0.3",
52 | "eslint-plugin-import": "^2.29.1",
53 | "jest": "^29.7.0",
54 | "jest-runner-eslint": "^2.2.0"
55 | },
56 | "files": [
57 | "dist",
58 | "src"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export const CONFIG = {
2 | reportFunctionMutation: false,
3 | reportIncompatibleObjectType: false,
4 | };
5 |
6 | export function XMutateRemovedElementX() {}
7 | export function XMutateLockedElementX(value) {
8 | this.__value__ = value;
9 | }
10 | function XDeepPatchX(value) {
11 | this.__value__ = value;
12 | }
13 | function XIssetMarkerX() {}
14 |
15 | const ARRAY_REGEXP = new RegExp('^\\[([^\\[\\]]*)]$');
16 |
17 | const MUTATE_TYPES = {
18 | ARRAY: 'array',
19 | OBJECT: 'object',
20 | DEFAULT: ''
21 | };
22 |
23 | function mutateObj(point, vType) {
24 | if (!point) return vType === MUTATE_TYPES.ARRAY ? [] : {};
25 | if (Array.isArray(point)) return [].concat(point);
26 | if (vType === MUTATE_TYPES.ARRAY) return [];
27 | if (checkIsObject(point)) return Object.assign({}, point);
28 | if (vType === MUTATE_TYPES.OBJECT) return {};
29 | return point;
30 | }
31 |
32 | export function getObjectPaths(obj, prefix = [], map = null) {
33 | if (!checkIsNativeObject(obj)) return [];
34 |
35 | const keys = Object.keys(obj);
36 | const isRoot = !map;
37 | const myMap = isRoot
38 | // eslint-disable-next-line no-undef
39 | ? new Map()
40 | : map;
41 |
42 | // ignore objects that were listened. It means that recursive links will be ignored
43 | if (myMap.has(obj)) return null;
44 | myMap.set(obj, new XIssetMarkerX());
45 |
46 | for (let i = 0; i < keys.length; i++) {
47 | const value = obj[keys[i]];
48 | const currentPath = prefix.concat([keys[i]]);
49 | if (checkIsNativeObject(value)) {
50 | getObjectPaths(value, currentPath, myMap);
51 | } else if (checkIsDeepPatch(value)) {
52 | getObjectPaths(value.__value__, currentPath, myMap);
53 | } else {
54 | const containsDot = currentPath.some(function (el) { return el.indexOf('.') >= 0 });
55 | myMap.set(containsDot ? currentPath : currentPath.join('.'), value);
56 | }
57 | }
58 |
59 | if (isRoot) {
60 | const result = [];
61 | myMap.forEach(function(val, key) {
62 | if (val instanceof XIssetMarkerX) return false;
63 | result.push([key, val]);
64 | });
65 | return result;
66 | }
67 | return null;
68 | }
69 |
70 | export function extToArray(pExt) {
71 | let result = pExt;
72 | if (!Array.isArray(pExt)) {
73 | if (checkIsDeepPatch(pExt)) {
74 | result = [pExt];
75 | } else {
76 | if (!checkIsNativeObject(pExt)) {
77 | console.error(new Error('Changes should be Object or Array'));
78 | return [];
79 | }
80 |
81 | result = Object.keys(pExt || {})
82 | .map((key) => (checkIsUndefined(pExt[key]) ? [key] : [key, pExt[key]]));
83 | }
84 | }
85 |
86 | return result.reduce(function (R, pair) {
87 | const isDeep = checkIsDeepPatch(pair);
88 | if (!isDeep && (!pair || pair.length < 2)) {
89 | return R;
90 | }
91 |
92 | const pairVal = isDeep ? pair : pair[1];
93 | if (isDeep || checkIsDeepPatch(pairVal)) {
94 | const pairPath = isDeep || !pair[0] ? undefined : splitPath(pair[0]);
95 | const n = R.findIndex((el) => el === pair);
96 | if (n < 0) return R;
97 |
98 | return R.slice(0, n).concat(getObjectPaths(pairVal.__value__, pairPath), R.slice(n + 1));
99 | }
100 | return R;
101 | }, result);
102 | }
103 |
104 | export function separatePath(path) {
105 | return checkIsString(path)
106 | ? path.replace(new RegExp('([^.])(\\[)', 'g'), (match, p1, p2) => `${p1}.${p2}`)
107 | : path;
108 | }
109 |
110 | export function splitPath(path) {
111 | if (checkIsString(path)) return path.split('.');
112 | // .split(/(?');
123 | const isRemove = checkIsRemoved(value);
124 |
125 | if (isRemove && (isArrayInsert || !hasProperty(parent, key))) {
126 | return parent;
127 | }
128 |
129 | const realParent = checkIsLocked(parent) ? parent.__value__ : parent;
130 |
131 | if (isRemove) {
132 | if (Array.isArray(realParent)) realParent.splice(key, 1);
133 | else delete realParent[key];
134 | return parent;
135 | }
136 |
137 | if (isArrayInsert) {
138 | const index = parseInt(key.slice(2).replace(']', ''), 10);
139 | realParent.splice(index, 0, value);
140 | return parent;
141 | }
142 |
143 | realParent[key] = value;
144 |
145 | return parent;
146 | }
147 |
148 | function checkIsUndefined(value) {
149 | return typeof(value) === 'undefined';
150 | }
151 |
152 | function checkIsRemoved(pObj) {
153 | return pObj instanceof XMutateRemovedElementX;
154 | }
155 |
156 | function checkIsLocked(pObj) {
157 | return pObj instanceof XMutateLockedElementX;
158 | }
159 |
160 | function checkIsDeepPatch(value) {
161 | return value instanceof XDeepPatchX;
162 | }
163 |
164 | function checkIsObject(value) {
165 | return value && typeof(value) === 'object';
166 | }
167 |
168 | function checkIsString(value) {
169 | return typeof(value) === 'string';
170 | }
171 |
172 | function checkIsFunction(value) {
173 | return value && typeof(value) === 'function';
174 | }
175 |
176 | function checkIsNativeObject(value) {
177 | return checkIsObject(value) && value.__proto__.constructor.name === 'Object';
178 | }
179 | // It has been exported for tests, but you could use it if needed
180 | export function checkIsExists(pObject, pPath) {
181 | return !checkIsUndefined(getValue(pObject, pPath));
182 | }
183 |
184 | export function getValue(pObject, pPath) {
185 | if (!pObject || !checkIsObject(pObject)) return undefined;
186 |
187 | const pieces = splitPath(pPath);
188 | if (pieces.length === 0) return pObject;
189 |
190 | // function preparePiece(piece) {
191 | // return piece.replace(/(\[|\])+/g, '');
192 | // }
193 |
194 | const lastIndex = pieces.length - 1;
195 | let node = pObject;
196 | for (let i = 0; i < lastIndex; i += 1) {
197 | const piece = getRealIndex(node, pieces[i]);
198 | node = checkIsLocked(node[piece]) ? node[piece].__value__ : node[piece];
199 | if (!node || !checkIsObject(node) || checkIsRemoved(node)) {
200 | return undefined;
201 | }
202 | }
203 |
204 | return node[getRealIndex(node, pieces[lastIndex])];
205 | }
206 |
207 | export function isArrayElement(key) {
208 | return checkIsString(key) && ARRAY_REGEXP.test(key);
209 | }
210 |
211 | export function extToTree(pExt, pSource) {
212 | let arrayCounter = 100;
213 | // +++++++++++++++++++++++++++
214 | function getNewValue(pair, isMutated) {
215 | if (!pair || pair.length === 0) return undefined;
216 |
217 | if (pair.length === 1 || checkIsUndefined(pair[1])) {
218 | return new XMutateRemovedElementX();
219 | }
220 |
221 | if (!isMutated && checkIsObject(pair[1])) {
222 | return new XMutateLockedElementX(pair[1]);
223 | }
224 |
225 | return pair[1];
226 | }
227 | // +++++++++++++++++++++++++++
228 | if (!checkIsObject(pExt)) {
229 | throw new Error('Changes should be Object or Array');
230 | }
231 | const values = extToArray(pExt);
232 |
233 | return values.reduce(function (FULL_RESULT, PAIR) {
234 | if (!PAIR) return FULL_RESULT;
235 | if (checkIsString(PAIR)) PAIR = [PAIR];
236 | if (!PAIR[0] && PAIR[0] !== 0) {
237 | throw new Error('Path should not be empty');
238 | }
239 |
240 | const pathPieces = splitPath(separatePath(PAIR[0]));
241 | if (PAIR.length < 2 || checkIsUndefined(PAIR[1])) {
242 | if (!(checkIsExists(pSource, pathPieces) || checkIsExists(FULL_RESULT, pathPieces))) {
243 | return FULL_RESULT;
244 | }
245 | } else if (getValue(pSource, pathPieces) === PAIR[1]) {
246 | return FULL_RESULT;
247 | }
248 |
249 | let isLockedPath = false;
250 | // console.log('--------------------');
251 | pathPieces.reduce(function (parent, currentKey, currentI) {
252 | const isLastPiece = currentI >= pathPieces.length - 1;
253 | const actualKey = currentKey === '[]' ? `[+${++arrayCounter}]` : currentKey;
254 | const newKey = isLockedPath ? getOptions(parent, actualKey).realKey : actualKey;
255 |
256 | const isLockedCurrent = !isLockedPath
257 | && hasProperty(parent, newKey)
258 | && checkIsLocked(parent[newKey]);
259 |
260 | isLockedPath = isLockedPath || isLockedCurrent;
261 |
262 | if (isLastPiece) {
263 | const newValue = getNewValue(PAIR, isLockedPath);
264 | if (isLockedPath) setValue(parent, newKey, newValue);
265 | else parent[newKey] = newValue;
266 | // return ROOT of changes
267 | return FULL_RESULT;
268 | }
269 |
270 | const currentValue = isLockedCurrent ? parent[newKey].__value__ : parent[newKey];
271 |
272 | if (!checkIsObject(currentValue)) {
273 | if (currentValue && CONFIG.reportIncompatibleObjectType) {
274 | console.error(new Error(`Warning: In "${PAIR[0]}", bad value for "${currentKey}", it will be replaced by empty Object ({})`));
275 | }
276 | const newValue = {};
277 | if (isLockedPath) setValue(parent, newKey, newValue);
278 | else parent[newKey] = newValue;
279 |
280 | // return new position in tree
281 | return newValue;
282 | }
283 |
284 | // return current position in tree
285 | return currentValue;
286 | }, FULL_RESULT);
287 |
288 | return FULL_RESULT;
289 | }, {});
290 | }
291 |
292 | function updateSection(point, tree) {
293 | if (
294 | !tree
295 | || Array.isArray(tree)
296 | || !checkIsObject(tree)
297 | ) {
298 | return tree;
299 | }
300 |
301 | if (checkIsFunction(tree)) {
302 | if (CONFIG.reportFunctionMutation) {
303 | console.error(new Error('Function mutation'));
304 | }
305 | return tree(point);
306 | }
307 |
308 | if (checkIsLocked(tree)) return tree.__value__;
309 |
310 | const pieces = Object.keys(tree);
311 | const needArray = pieces.some(isArrayElement);
312 | const result = mutateObj(point, needArray ? MUTATE_TYPES.ARRAY : MUTATE_TYPES.OBJECT);
313 |
314 | pieces.forEach(function (key) {
315 | const opt = getOptions(result, key);
316 | const k = opt.realKey;
317 |
318 | if (checkIsRemoved(tree[key])) {
319 | if (opt.isArray) result.splice(k, 1);
320 | else delete result[k];
321 | return;
322 | }
323 |
324 | if (key && String(key).startsWith('[>')) {
325 | const index = parseInt(key.slice(2).replace(']',''), 10);
326 | result.splice(index, 0, updateSection(result[index], tree[key]));
327 | return;
328 | }
329 |
330 | if (!opt.isArray || k >= 0) {
331 | result[k] = updateSection(result[k], tree[key]);
332 | }
333 | });
334 | return result;
335 | }
336 |
337 | export function getRealIndex(items, key) {
338 | const parse = key ? ARRAY_REGEXP.exec(key) : null;
339 | if (!parse) return key;
340 |
341 | const k = parse[1].trim();
342 |
343 | if (k.startsWith('>')) {
344 | // [>2] || [>2...]
345 | return k;
346 | }
347 |
348 | const arrayItems = Array.isArray(items) ? items : [];
349 |
350 | if (k.length == 0 || k.startsWith('+')) {
351 | return arrayItems.length;
352 | }
353 |
354 | if (k.startsWith('=')) {
355 | // [=10] || [=id=99]
356 | const parseCompare = /^=(?:([^=\s]*)=)?(.*)$/.exec(k);
357 | if (parseCompare) {
358 | const [, k, v] = parseCompare;
359 | return arrayItems.findIndex((el) => String(k && el ? getValue(el, k) : el) === String(v));
360 | }
361 | }
362 |
363 | const index = parseInt(k, 10);
364 | return Number.isNaN(index) ? items.length : index;
365 | }
366 |
367 | export function getOptions(parentValue, key) {
368 | const realParentValue = checkIsLocked(parentValue)
369 | ? parentValue.__value__
370 | : parentValue;
371 | return {
372 | key: key,
373 | realKey: getRealIndex(realParentValue, key),
374 | isArray: isArrayElement(key),
375 | length: Array.isArray(realParentValue) ? realParentValue.length : 0
376 | };
377 | }
378 |
379 | function mutate(pObj, pExt) {
380 | if (!checkIsObject(pObj)) {
381 | throw new Error('Type of variable should be Object or Array');
382 | }
383 |
384 | if (checkIsUndefined(pExt)) {
385 | return toFunction(pObj);
386 | }
387 |
388 | const tree = extToTree(pExt, pObj);
389 | if (Object.getOwnPropertyNames(tree).length === 0) {
390 | return pObj;
391 | }
392 |
393 | return updateSection(pObj, tree);
394 | }
395 |
396 | export function deepPatch(pExt) {
397 | if (checkIsDeepPatch(pExt)) return pExt;
398 | return new XDeepPatchX(pExt);
399 | }
400 |
401 | mutate.deep = function (pObj, pExt) {
402 | let newExt = null;
403 | if (Array.isArray(pExt)) {
404 | newExt = pExt.map(function (el) { return deepPatch(el) });
405 | } else newExt = deepPatch(pExt);
406 |
407 | return mutate(pObj, newExt);
408 | };
409 |
410 | function toFunction(pObj) {
411 | var result = pObj;
412 |
413 | return function(pExt) {
414 | if (checkIsUndefined(pExt)) return result;
415 | result = mutate(result, pExt);
416 | return result;
417 | };
418 | }
419 |
420 | export default mutate;
--------------------------------------------------------------------------------
/src/tests/checkIsExists.test.js:
--------------------------------------------------------------------------------
1 | import { checkIsExists } from '../index';
2 |
3 |
4 | const checkIsExistsData = [
5 | [null, 'a', false],
6 | [[10], '0', true],
7 | [[{ a: 10 }], '0.a', true],
8 | [[{ a: 10 }], '1.a', false],
9 | [{ a: 10 }, 'a', true],
10 | [{ a: 10 }, 'a.aa', false],
11 | [{ a: 10 }, 'a2', false],
12 | [{ }, 'a', false],
13 | [{ }, 'a.aa', false],
14 | [{ a: { aa: { aaa: 10 } } }, 'a', true],
15 | [{ a: { aa: { aaa: 10 } } }, 'a.aa', true],
16 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa', true],
17 | [{ a: { aa: { aaa: 10 } } }, '[a]', false],
18 | [{ a: { aa: { aaa: 10 } } }, 'a.[aa]', false],
19 | [{ a: { aa: { aaa: 10 } } }, '[a].[aa].[aaa]', false],
20 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa2', false],
21 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2', false],
22 | [{ a: { aa: { aaa: 10 } } }, 'a2', false],
23 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa.aaaa', false],
24 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2.aaa', false],
25 | [{ a: { aa: { aaa: 10 } } }, 'a2.aa.aaa', false],
26 |
27 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[0]', true],
28 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[2]', true],
29 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[3]', false],
30 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[]', false],
31 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[+5454]', false],
32 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[].bbb', false],
33 |
34 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[=3]', true],
35 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[=99]', false],
36 |
37 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].id', true],
38 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.1.id', true],
39 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].data', false],
40 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].data.value', false],
41 | [{ a: [{ id: 10 }, { id: 20, data: {} }] }, 'a.[1].data', true],
42 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[=id=20]', true],
43 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[=id=20].id', true],
44 | ];
45 |
46 | const checkIsExistsData2 = [
47 | [{ a: { aa: { aaa: 10 } } }, 'a'.split('.'), true],
48 | [{ a: { aa: { aaa: 10 } } }, 'a.aa'.split('.'), true],
49 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa'.split('.'), true],
50 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa2'.split('.'), false],
51 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2'.split('.'), false],
52 | [{ a: { aa: { aaa: 10 } } }, 'a2'.split('.'), false],
53 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa.aaaa'.split('.'), false],
54 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2.aaa'.split('.'), false],
55 | [{ a: { aa: { aaa: 10 } } }, 'a2.aa.aaa'.split('.'), false],
56 |
57 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.0'.split('.'), true],
58 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.2'.split('.'), true],
59 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.3'.split('.'), false],
60 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.'.split('.'), false],
61 | ];
62 |
63 | describe('checkIsExists', () => {
64 | test.each(checkIsExistsData)('checkIsExists(%j, %s) === %j', (obj, path, expected) => {
65 | const result = checkIsExists(obj, path);
66 | expect(result).toEqual(expected);
67 | });
68 |
69 | test.each(checkIsExistsData2)('checkIsExists(%j, %j) === %j', (obj, path, expected) => {
70 | const result = checkIsExists(obj, path);
71 | expect(result).toEqual(expected);
72 | });
73 | });
--------------------------------------------------------------------------------
/src/tests/deepToMutate.test.js:
--------------------------------------------------------------------------------
1 | import mutate, { deepPatch } from '../index';
2 |
3 | const data = [
4 | [
5 | { a: { a1: { a11: 10 }, a2: 20 }, b: 99 },
6 | { a: deepPatch({ a2: 18, a3: 30, a4: { a41: 41 } }), 'a.a4.a41': 98 },
7 | { a: { a1: { a11: 10 }, a2: 18, a3: 30, a4: { a41: 98 } }, b: 99 }
8 | ],
9 |
10 | [
11 | { a: { a1: 1 } },
12 | { 'a.a4': { a41: 1, a42: 5 }, a: deepPatch({ a4: { a41: 41 } }) },
13 | { a: { a1: 1, a4: { a41: 41, a42: 5 } } }
14 | ],
15 |
16 | [
17 | { a: { a1: 1, a4: { a41: 1, a42: 5 } } },
18 | deepPatch({ a: { a4: { a41: 41 } } }),
19 | { a: { a1: 1, a4: { a41: 41, a42: 5 } } }
20 | ],
21 |
22 | [
23 | { a: { a1: 1 } },
24 | { a: deepPatch({ a4: { a41: 41, a45: 1 } }), 'a.a4': deepPatch({ a41: 12, a42: 10, a43: [1,2,3] }) },
25 | { a: { a1: 1, a4: { a41: 12, a45: 1, a42: 10, a43: [1,2,3] } } }
26 | ],
27 |
28 | [
29 | { a: { a1: 1, a4: { a41: 1, a42: 5 } } },
30 | [deepPatch({ a: { a4: { a41: 41 } } }), deepPatch({ a: { a4: { a42: [1, 2, 3] } } })],
31 | { a: { a1: 1, a4: { a41: 41, a42: [1, 2, 3] } } }
32 | ],
33 |
34 | [
35 | { a: [{ z: 100, z2: [1,2,3] }] },
36 | deepPatch({ a: { '[0]': { z: 9, z2: ['q'] } } }),
37 | { a: [{ z: 9, z2: ['q'] }] },
38 | ],
39 |
40 | [
41 | { a: [1, 2] },
42 | deepPatch({ a: { '[]': 3, '[+123]': 4 } }),
43 | { a: [1, 2, 3, 4] },
44 | ],
45 |
46 | [
47 | { a: { 'a1.1': 'Hi', 'a1.2': 'world' } },
48 | deepPatch({ a: { 'a1.1': 'Hello' } }),
49 | { a: { 'a1.1': 'Hello', 'a1.2': 'world' } },
50 | ],
51 | ];
52 |
53 | describe('mutate with deep', () => {
54 | test.each(data)('mutate(%j, %j) === %j', (obj, ext, expected) => {
55 | expect(mutate(obj, ext)).toEqual(expected);
56 | });
57 |
58 | test('not Objects should be as it is', () => {
59 | const err = new Error('example');
60 | const result = mutate({ a: 10 }, deepPatch({ a: err }));
61 | expect(result).toEqual({ a: err });
62 | expect(result.a).toBe(err);
63 | });
64 | });
--------------------------------------------------------------------------------
/src/tests/extToArray.test.js:
--------------------------------------------------------------------------------
1 | import { extToArray, deepPatch } from '../index';
2 |
3 | const testCases = [
4 | [[['a.b.c', 25]], [['a.b.c', 25]]],
5 | [[['a.b.c']], [['a.b.c']]],
6 | [{}, []],
7 | [[], []],
8 |
9 | [{ 'a.a1': 100, 10: 1000 }, [['10', 1000], ['a.a1', 100]]],
10 | [{ 'a.a1': { z: 10, j: 50 } }, [['a.a1', { z: 10, j: 50 }]]],
11 | [{ 'a.a1': undefined }, [['a.a1']]],
12 | [{ 'a.a1': null }, [['a.a1', null]]],
13 |
14 | [{ 'a.a1': deepPatch({ z: 10, z2: { z21: 100 } }) }, [['a.a1.z', 10], ['a.a1.z2.z21', 100]]],
15 | [[['a.a1', deepPatch({ z: 10, z2: { z21: 100 } })]], [['a.a1.z', 10], ['a.a1.z2.z21', 100]]],
16 | [[[['a', 'a1'], deepPatch({ z: 10, z2: { z21: 100 } })]], [['a.a1.z', 10], ['a.a1.z2.z21', 100]]],
17 |
18 | [{ '': deepPatch({ z: 10, z2: { z21: 100 } }) }, [['z', 10], ['z2.z21', 100]]],
19 | [[['', deepPatch({ z: 10, z2: { z21: 100 } })]], [['z', 10], ['z2.z21', 100]]],
20 | [[[null, deepPatch({ z: 10, z2: { z21: 100 } })]], [['z', 10], ['z2.z21', 100]]],
21 | [[[[], deepPatch({ z: 10, z2: { z21: 100 } })]], [['z', 10], ['z2.z21', 100]]],
22 | [[deepPatch({ z: 10, z2: { z21: 100 } })], [['z', 10], ['z2.z21', 100]]],
23 | [deepPatch({ z: 10, z2: { z21: 100 } }), [['z', 10], ['z2.z21', 100]]],
24 |
25 | [{ 'a.a1': deepPatch({ z: 10, z2: { z21: 100 } }), 'a.a1.z': 9999 }, [['a.a1.z', 10], ['a.a1.z2.z21', 100], ['a.a1.z', 9999]]],
26 | ];
27 |
28 | const errorCases = [
29 | [null, []],
30 | [undefined, []],
31 | [1, []],
32 | ['1123', []],
33 | [11.5, []],
34 | [new Error(), []],
35 | ];
36 |
37 | const consoleError = console.error;
38 |
39 | describe('extToArray', () => {
40 |
41 | describe('returns error', () => {
42 | beforeAll(() => {
43 | jest.spyOn(console, 'error');
44 | console.error.mockImplementation((error) => {
45 | if (error.message.startsWith('Changes should be Object or Array')) {
46 | console.debug(`Expected test error: ${error.message}`);
47 | } else {
48 | consoleError(error);
49 | }
50 | });
51 | });
52 |
53 | afterAll(() => {
54 | console.error.mockReset();
55 | });
56 |
57 | test.each(errorCases)('extToArray(%j) === %j', (data, expected) => {
58 | expect(extToArray(data)).toEqual(expected);
59 | });
60 | });
61 |
62 | test.each(testCases)('extToArray(%j) === %j', (data, expected) => {
63 | expect(extToArray(data)).toEqual(expected);
64 | });
65 |
66 | test('should return the same array', () => {
67 | const data = [['a', 10], ['b'], ['c.11', 20]];
68 | expect(extToArray(data)).toBe(data);
69 | });
70 |
71 | test('should return array with the same Object', () => {
72 | const value = { z: 1, z2: 2 };
73 | const data = { 'a.a2': value };
74 | const result = extToArray(data);
75 | expect(result[0][1]).toBe(value);
76 | });
77 | });
--------------------------------------------------------------------------------
/src/tests/extToTree.test.js:
--------------------------------------------------------------------------------
1 | import { extToTree, XMutateLockedElementX } from '../index';
2 |
3 | const testCases = [
4 | [
5 | { 'a.a2': [123] },
6 | undefined,
7 | { a: { a2: new XMutateLockedElementX([123]) } }
8 | ],
9 | [
10 | { 'a.a2': [123] },
11 | { a: { a2: [] } },
12 | { a: { a2: new XMutateLockedElementX([123]) } }
13 | ],
14 | [
15 | { 'a.a2[]': 123, 'a.a2.[]': 321 },
16 | undefined,
17 | { a: { a2: { '[+101]': 123, '[+102]': 321 } } }
18 | ],
19 | [
20 | { 'a.a2[>2]': 123 },
21 | undefined,
22 | { a: { a2: { '[>2]': 123 } } }
23 | ],
24 | // [
25 | // { 'a.a2[>2...]': 123 },
26 | // undefined,
27 | // { a: { a2: { '[>2...]': 123 } } }
28 | // ],
29 | [
30 | { 'a.a2': { k: 1 }, 'a.a2.z': 5 },
31 | undefined,
32 | { a: { a2: new XMutateLockedElementX({ k: 1, z: 5 }) } }
33 | ],
34 | [
35 | { 'a.a2.k': 1, 'a.a2.z': 5 },
36 | undefined,
37 | { a: { a2: { k: 1, z: 5 } } }
38 | ],
39 | [
40 | { 'a.a2.k': 1, 'a.a2.z': 5 },
41 | { a: { a2: { k: 1, z: 5 } } },
42 | {},
43 | ],
44 | [
45 | { 'a.a2.k': 1, 'a.a2.z': 5 },
46 | { a: { a2: { k: 1 } } },
47 | { a: { a2: { z: 5 } } },
48 | ],
49 | ];
50 |
51 | describe('extToTree', () => {
52 | test.each(testCases)('%# extToArray(%j)', (data, obj, expected) => {
53 | const result = extToTree(data, obj);
54 | expect(result).toEqual(expected);
55 | });
56 | });
--------------------------------------------------------------------------------
/src/tests/getObjectPaths.test.js:
--------------------------------------------------------------------------------
1 | import { getObjectPaths } from '../index';
2 |
3 | const errorValue = new Error('Example');
4 | const recursivePath = {
5 | a: 10,
6 | b: {
7 | b1: 99,
8 | b2: null,
9 | }
10 | };
11 | recursivePath.b.b2 = recursivePath;
12 |
13 | const data = [
14 | [
15 | { a: { a1: { a11: 10 }, a2: 20 }, b: 99 },
16 | undefined,
17 | [['a.a1.a11', 10], ['a.a2', 20], ['b', 99]]
18 | ],
19 | [null, undefined, []],
20 | [undefined, undefined, []],
21 | ['11111', undefined, []],
22 | [10, undefined, []],
23 | [10.8, undefined, []],
24 | [new Error(), undefined, []],
25 | [{}, undefined, []],
26 | [[], undefined, []],
27 | [
28 | { a: { a1: errorValue } },
29 | undefined,
30 | [['a.a1', errorValue]]
31 | ],
32 | [
33 | { a: { a1: { 'a1.1': 10 }, a2: 20 }, 'b.bb': 99 },
34 | undefined,
35 | [[['a', 'a1', 'a1.1'], 10], ['a.a2', 20], [['b.bb'], 99]]
36 | ],
37 | [recursivePath, undefined, [['a', 10], ['b.b1', 99]]],
38 | [
39 | { a: { a1: { a11: 10 }, a2: 20 }, b: 99 },
40 | ['x'],
41 | [['x.a.a1.a11', 10], ['x.a.a2', 20], ['x.b', 99]]
42 | ],
43 | ];
44 |
45 | describe('getObjectPaths', () => {
46 | test.each(data)('getObjectPaths(%j, %j) === %j', (data, prefix, expected) => {
47 | expect(getObjectPaths(data, prefix)).toEqual(expected);
48 | });
49 | });
--------------------------------------------------------------------------------
/src/tests/getPairValue.test.js:
--------------------------------------------------------------------------------
1 | import { getPairValue } from '../index';
2 |
3 | const data = [
4 | [['a.b.c', 25], 25],
5 | [['a.b.c', 999, 12], 999],
6 | [['a.b.c'], undefined],
7 | [[], undefined],
8 | ['', undefined],
9 | [{}, undefined],
10 | [12, undefined],
11 | [null, undefined],
12 | ];
13 |
14 | describe.skip('getPairValue', () => {
15 | test.each(data)('getPairValue(%j) === %j', (data, expected) => {
16 | expect(getPairValue(data)).toBe(expected);
17 | });
18 | });
--------------------------------------------------------------------------------
/src/tests/getRealIndex.test.js:
--------------------------------------------------------------------------------
1 | import { getRealIndex } from '../index';
2 |
3 | const data = [
4 | [[10, 20, 30], '()', '()'],
5 | [[10, 20, 30], '', ''],
6 | [[10, 20, 30], '1312', '1312'],
7 | [[10, 20, 30], 'sdfs', 'sdfs'],
8 | [[10, 20, 30], '+[]', '+[]'],
9 |
10 | [[10, 20, 30], '[0]', 0],
11 | [[10, 20, 30], '[5]', 5],
12 | [[10, 20, 30], '[]', 3],
13 | [[10, 20, 30], '[+]', 3],
14 | [[10, 20, 30], '[+883]', 3],
15 | [[10, 20, 30], '[+sfsfs]', 3],
16 |
17 | [[10, 20, 30], '[=]', -1],
18 | [[10, 20, 0], '[=]', -1],
19 | [[10, 20, 30], '[=99]', -1],
20 | [[10, 20, 30], '[=id=1]', -1],
21 |
22 | [[10, 20, 30], '[=20]', 1],
23 | [[10, 20, 20], '[=20]', 1],
24 | [[10, 20, 30], '[==20]', 1],
25 | [[10, 20, 20], '[==20]', 1],
26 |
27 | [{}, '[=20]', -1],
28 | [null, '[=20]', -1],
29 | ['sasda', '[==20]', -1],
30 | [99, '[==20]', -1],
31 |
32 | [[10, false, true], '[=false]', 1],
33 | [['false', false, true], '[=false]', 0],
34 | [[10, false, true], '[=true]', 2],
35 | [[0.0, 0.00, 0], '[=0]', 0],
36 | [['0.0', '0.00', '0'], '[=0]', 2],
37 |
38 | [[{ id: 10 }, { id: 20 }, { id: 30 }], '[=id=20]', 1],
39 | [[{ id: 10 }, { id: 20 }, { id: 30 }], '[=zzz=20]', -1],
40 | [[{ id: 10 }, { id: 20 }, { id: 30 }], '[=id=99]', -1],
41 |
42 | [[{ data: { v: 10 } }, { data: { v: 20 } }, { data: { v: 20 } }], '[=data.v=20]', 1],
43 | [[{ data: { v: 10 } }, { data: { v: 20 } }, { data: { v: 20 } }], '[=data.v=60]', -1],
44 | [[{ data: { v: false } }, { data: { v: true } }, { data: { v: '' } }], '[=data.v=true]', 1],
45 | [[{ data: { v: '' } }, { data: { v: 0 } }, { data: { v: false } }], '[=data.v=false]', 2],
46 | ];
47 |
48 | describe('getRealIndex', () => {
49 | test.each(data)('getRealIndex(%j, %s) === %j', (data, key, expected) => {
50 | expect(getRealIndex(data, key)).toBe(expected);
51 | });
52 | });
--------------------------------------------------------------------------------
/src/tests/getValue.test.js:
--------------------------------------------------------------------------------
1 | import { getValue } from '../index';
2 |
3 |
4 | const data = [
5 | [null, 'a', undefined],
6 | [[10], '0', 10],
7 | [[{ a: 10 }], '0.a', 10],
8 | [[{ a: 10 }], '1.a', undefined],
9 | [{ a: 10 }, 'a', 10],
10 | [{ a: 10 }, 'a.aa', undefined],
11 | [{ a: 10 }, 'a2', undefined],
12 | [{ }, 'a', undefined],
13 | [{ }, 'a.aa', undefined],
14 | [{ a: { aa: { aaa: 10 } } }, 'a', { aa: { aaa: 10 } }],
15 | [{ a: { aa: { aaa: 10 } } }, 'a.aa', { aaa: 10 }],
16 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa', 10],
17 | [{ a: { aa: { aaa: 10 } } }, '[a]', undefined],
18 | [{ a: { aa: { aaa: 10 } } }, 'a.[aa]', undefined],
19 | [{ a: { aa: { aaa: 10 } } }, '[a].[aa].[aaa]', undefined],
20 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa2', undefined],
21 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2', undefined],
22 | [{ a: { aa: { aaa: 10 } } }, 'a2', undefined],
23 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa.aaaa', undefined],
24 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2.aaa', undefined],
25 | [{ a: { aa: { aaa: 10 } } }, 'a2.aa.aaa', undefined],
26 |
27 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[0]', 1],
28 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[2]', 3],
29 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[3]', undefined],
30 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[]', undefined],
31 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[+5454]', undefined],
32 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[].bbb', undefined],
33 |
34 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[=3]', 3],
35 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.[=99]', undefined],
36 |
37 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].id', 20],
38 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.1.id', 20],
39 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].data', undefined],
40 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[1].data.value', undefined],
41 | [{ a: [{ id: 10 }, { id: 20, data: {} }] }, 'a.[1].data', {}],
42 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[=id=20]', { id: 20 }],
43 | [{ a: [{ id: 10 }, { id: 20 }] }, 'a.[=id=20].id', 20],
44 | ];
45 |
46 | const data2 = [
47 | [{ a: { aa: { aaa: 10 } } }, 'a'.split('.'), { aa: { aaa: 10 } }],
48 | [{ a: { aa: { aaa: 10 } } }, 'a.aa'.split('.'), { aaa: 10 }],
49 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa'.split('.'), 10],
50 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa2'.split('.'), undefined],
51 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2'.split('.'), undefined],
52 | [{ a: { aa: { aaa: 10 } } }, 'a2'.split('.'), undefined],
53 | [{ a: { aa: { aaa: 10 } } }, 'a.aa.aaa.aaaa'.split('.'), undefined],
54 | [{ a: { aa: { aaa: 10 } } }, 'a.aa2.aaa'.split('.'), undefined],
55 | [{ a: { aa: { aaa: 10 } } }, 'a2.aa.aaa'.split('.'), undefined],
56 |
57 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.0'.split('.'), 1],
58 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.2'.split('.'), 3],
59 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.3'.split('.'), undefined],
60 | [{ a: { aa: { aaa: [1,2,3] } } }, 'a.aa.aaa.'.split('.'), undefined],
61 | ];
62 |
63 | describe('getValue', () => {
64 | test.each(data)('getValue(%j, %s) === %j', (obj, path, expected) => {
65 | const result = getValue(obj, path);
66 | expect(result).toEqual(expected);
67 | });
68 |
69 | test.each(data2)('getValue(%j, %j) === %j', (obj, path, expected) => {
70 | const result = getValue(obj, path);
71 | expect(result).toEqual(expected);
72 | });
73 | });
--------------------------------------------------------------------------------
/src/tests/index_data.test.js:
--------------------------------------------------------------------------------
1 | import mutate from '../index';
2 |
3 |
4 | const data1 = [
5 | [{ a: 10 }, [['a', 5]], { a: 5 }],
6 | [{ a: 10 }, [['b', 5]], { a: 10, b: 5 }],
7 | [{}, [['a', 10], ['b', 5]], { a: 10, b: 5 }],
8 | [{ a: 10 }, [['a']], { }],
9 | [{ a: 10 }, [null], { a: 10 }],
10 | [{ a: 10 }, [['a'], ['b']], { }],
11 | [{ a: 10 }, ['a', 'b'], { }],
12 | [{ a: 10 }, [['a'], ['b', 5]], { b: 5 }],
13 | [{ a: 10 }, [['a', [1,2,3]]], { a: [1,2,3] }],
14 | [{ a: 10 }, [['a', { aa: 1 }]], { a: { aa: 1 } }],
15 | [{ a: 10 }, [['a', 5], ['b', { bb: 2 }]], { a: 5, b: { bb: 2 } }],
16 | ];
17 |
18 | const data2 = [
19 | // extend object
20 | [{ a: { aa: 10 } }, [['a.aa', 5]], { a: { aa: 5 } }],
21 | [{ a: { aa: 10 } }, [['a.bb 10', 5]], { a: { aa: 10, 'bb 10': 5 } }],
22 | [{ a: { aa: 10 } }, [['a.aa']], { a: { } }],
23 | [{ a: { aa: { aaa: 10 } } }, [['a.aa'], ['a.aa.aaa']], { a: { } }],
24 | [{ a: { aa: 10 } }, [['a.aa.[]', 1]], { a: { aa: [1] } }],
25 | [{ a: { aa: 10 } }, [['a.aa'], ['a']], { }],
26 | [{ a: { aa: 10 } }, ['a.aa', 'a'], { }],
27 | [{ a: 10 }, [['a.aa', 5]], { a: { aa: 5 } }],
28 | [{ a: 10 }, [['a.aa.aaa', 5]], { a: { aa: { aaa: 5 } } }],
29 | [{ a: 10 }, [['a.aa.aaa', 5], ['a.aa.aaa.aaaa', 2]], { a: { aa: { aaa: { aaaa: 2 } } } }],
30 | [{ a: 10 }, [['a.aa', 5], ['a.aa2', 2]], { a: { aa: 5, aa2: 2 } }],
31 | [{ a: 10 }, [['a.aa', 5], ['b.bb', 2]], { a: { aa: 5 }, b: { bb: 2 } }],
32 | ];
33 |
34 | const arrayChanges = [
35 | // extend array
36 | [[], [['[]', 5]], [5]],
37 | [{ a: [] }, [['a.[]', 5]], { a: [5] }],
38 | [{ a: [] }, [['a.[0]', 5]], { a: [5] }],
39 | [{ a: [] }, [['a[0]', 5]], { a: [5] }],
40 | [{ a: [] }, [['a[][]', 5]], { a: [[5]] }],
41 | [{ a: [] }, [['a.[].[]', 5]], { a: [[5]] }],
42 | [{ a: [] }, [['a.[2]', 5]], { a: [undefined, undefined, 5] }],
43 | [{ a: [1] }, [['a.[]', 5]], { a: [1, 5] }],
44 | [{ a: [1] }, [['a.[]', 5],['a.[]', 7]], { a: [1, 5, 7] }],
45 | [{ a: [1] }, [['a.[0]', 5]], { a: [5] }],
46 | [{ a: [1] }, [['a.[0]']], { a: [] }],
47 |
48 | [{ a: [1, 2, 3, 4, 5] }, [['a.[2]']], { a: [1, 2, 4, 5] }],
49 | [{ a: [1, 2, 3, 4, 5] }, [['a.[-1]']], { a: [1, 2, 3, 4, 5] }],
50 | [{ a: [1, 2, 3, 4, 5] }, [['a.[-1]', 99]], { a: [1, 2, 3, 4, 5] }],
51 | [{ a: [1, 2, 3, 4, 5] }, [['a.[=2]']], { a: [1, 3, 4, 5] }],
52 | [{ a: [1, 2, 3, 4, 5] }, [['a.[=999]']], { a: [1, 2, 3, 4, 5] }],
53 | [{ a: [1, 2, 3, 4, 5] }, [['a.[=2]', 200], ['a.[=9999]', '999'], ['a.[=1]', 100]], { a: [100, 200, 3, 4, 5] }],
54 |
55 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[1]']], { a: [{ id: 1 }] }],
56 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[-1]']], { a: [{ id: 1 }, { id: 2 }] }],
57 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[-1]', 99]], { a: [{ id: 1 }, { id: 2 }] }],
58 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[=id=2]']], { a: [{ id: 1 }] }],
59 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[=id=999]']], { a: [{ id: 1 }, { id: 2 }] }],
60 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[=id=2].v', 1000], ['a.[=id=9999].v', '999']], { a: [{ id: 1 }, { id: 2, v: 1000 }] }],
61 | [{ a: [{ id: 1 }, { id: 2 }] }, [['a.[=id=1].d.v', 77]], { a: [{ id: 1, d: { v: 77 } }, { id: 2 }] }],
62 | ];
63 |
64 | const objectChanges = [
65 | // changes are object
66 | [{ a: [] }, { 'a.[]': 5 }, { a: [5] }],
67 | [{ a: [] }, { 'a.[0]': 5 }, { a: [5] }],
68 | [{ a: [] }, { 'a.[2]': 5 }, { a: [undefined, undefined, 5] }],
69 | [{ a: [1] }, { 'a.[]': 5 }, { a: [1, 5] }],
70 | [{ a: [1] }, { 'a.[0]': 5 }, { a: [5] }],
71 | [{ a: { aa: 10 } }, { 'a.aa': 5 }, { a: { aa: 5 } }],
72 | [{ a: { aa: 10 } }, { 'a.aa': undefined, 'a.aaa': 99 }, { a: { aaa: 99 } }],
73 | [{ }, { 'a.aa.aaa': undefined }, { }],
74 | [{ }, [['a.aa.aaa']], { }],
75 | [{ }, [['a.aa'], ['a.aa', 100]], { a: { aa: 100 } }],
76 | [{ }, [['a.aa', 100], ['a']], { }],
77 | [{ a: { 0: 'v0', 1: 'v1' } }, [['a.0']], { a: { 1: 'v1' } }],
78 | [{ a: [1,2,3] }, [['a.[]']], { a: [1,2,3] }],
79 | [{ a: [1,2,3] }, [['a.[0]']], { a: [2,3] }],
80 | [{ a: [1,2,3] }, [['a.[0]', undefined]], { a: [2,3] }],
81 | [{ a: [1,2,3] }, [['a.0']], { a: [undefined, 2,3] }],
82 | ];
83 |
84 | const changeObject = [
85 | // set object, extend object
86 | [{ }, [['a', { aa: 5 }]], { a: { aa: 5 } }],
87 | [{ a: 10 }, [['a', { aa: { aaa: 5 } }], ['b.bb.bbb']], { a: { aa: { aaa: 5 } } }],
88 | [{ a: 10 }, [['a', { aa: { aaa: 5 } }], ['a.aa.aaa2', 1]], { a: { aa: { aaa: 5, aaa2: 1 } } }],
89 | [{ a: 10 }, [['a', { aa: { aaa: 5, aaa2: 1 } }], ['a.aa.aaa2']], { a: { aa: { aaa: 5 } } }],
90 | [{ a: 10 }, [['a', { aa: 5 }], ['a', [1,2,3]]], { a: [1,2,3] }],
91 | [{ a: 10 }, [['a', { aa: 5 }], ['a.aa', 12]], { a: { aa: 12 } }],
92 | [{ b: 20 }, [['a', { aa: 5 }], ['a']], { b: 20 }],
93 | [{ b: 20 }, [['a', { aa: 5 }], ['a.aa']], { a: { }, b: 20 }],
94 | [{ }, [['a.a1', 200], ['a', { a2: 100 }]], { a: { a2: 100 } }],
95 | [{ }, [['a', [1, 2, 3, 4, 5]], ['a.[2]']], { a: [1, 2, 4, 5] }],
96 | ];
97 |
98 | const complexChanges = [
99 | // complex changes
100 | [{ a: 10, b: [], c: {} }, { a: 50, b: { b1: 10 }, c: [1,2,3] }, { a: 50, b: { b1: 10 }, c: [1,2,3] }],
101 | [
102 | { a: 10, b: [], c: {}, d: { d1: 12 }, e: [9,8,7] },
103 | { a: 50, b: { b1: 10 }, c: [1,2,3], 'c.[]': { cc: 22 }, 'b.b2': 17, 'd.d2': 15, 'e.[0]': 1, 'e.[]': 3 },
104 | { a: 50, b: { b1: 10, b2: 17 }, c: [1,2,3, { cc: 22 }], d: { d1: 12, d2: 15 }, e: [1,8,7,3] }
105 | ],
106 | [
107 | { a: { a1: { a1_1: 22 } }, b: [{ b1: 10 }], c: [{ c1: 1 }] },
108 | { 'a.a1.a1_1': 33, 'a.a1.a1_2': 9, 'a.a2': 14, 'b.[0].b1': 11, 'b.[]': 15, 'b.[0].b2': null, 'c[0].c1': undefined, 'c[0]': 7 },
109 | { a: { a1: { a1_1: 33, a1_2: 9 }, a2: 14 }, b: [{ b1: 11, b2: null }, 15], c: [7] }
110 | ],
111 | [
112 | { a: 10, b: 20 },
113 | { a: { a1: 1, a2: 2 }, 'a.a3.a3_1': 20, b: [1,2,3,{ b1: 1 }], 'b.[]': 11, 'b[3].b2.b2_1.b2_1_1': 'b2_1_1 value', 'c.[]': 14 },
114 | { a: { a1: 1, a2: 2, a3: { a3_1: 20 } }, b: [1,2,3,{ b1: 1, b2: { b2_1: { b2_1_1: 'b2_1_1 value' } } }, 11], c: [14] }
115 | ]
116 | ];
117 |
118 | describe('mutate', () => {
119 | describe('Dataset - data1: ', () => {
120 | test.each(data1)('mutate(%j + %j)', (obj, changes, expected) => {
121 | expect(mutate(obj, changes)).toEqual(expected);
122 | });
123 |
124 | test.each(data1)('mutate(%j)(%j)', (obj, changes, expected) => {
125 | expect(mutate(obj)(changes)).toEqual(expected);
126 | });
127 | });
128 |
129 | describe('Dataset - data2: ', () => {
130 | test.each(data2)('mutate(%j + %j)', (obj, changes, expected) => {
131 | expect(mutate(obj, changes)).toEqual(expected);
132 | });
133 |
134 | test.each(data2)('mutate(%j)(%j)', (obj, changes, expected) => {
135 | expect(mutate(obj)(changes)).toEqual(expected);
136 | });
137 | });
138 |
139 | describe('Dataset - arrayChanges: ', () => {
140 | test.each(arrayChanges)('mutate(%j + %j)', (obj, changes, expected) => {
141 | expect(mutate(obj, changes)).toEqual(expected);
142 | });
143 |
144 | test.each(arrayChanges)('mutate(%j)(%j)', (obj, changes, expected) => {
145 | expect(mutate(obj)(changes)).toEqual(expected);
146 | });
147 | });
148 |
149 | describe('Dataset - objectChanges: ', () => {
150 | test.each(objectChanges)('mutate(%j + %j)', (obj, changes, expected) => {
151 | expect(mutate(obj, changes)).toEqual(expected);
152 | });
153 |
154 | test.each(objectChanges)('mutate(%j)(%j)', (obj, changes, expected) => {
155 | expect(mutate(obj)(changes)).toEqual(expected);
156 | });
157 | });
158 |
159 | describe('Dataset - changeObject: ', () => {
160 | test.each(changeObject)('mutate(%j + %j)', (obj, changes, expected) => {
161 | expect(mutate(obj, changes)).toEqual(expected);
162 | });
163 | });
164 |
165 | describe('Dataset - complexChanges: ', () => {
166 | test.each(complexChanges)('mutate(%j + %j)', (obj, changes, expected) => {
167 | expect(mutate(obj, changes)).toEqual(expected);
168 | });
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/src/tests/isArrayElement.test.js:
--------------------------------------------------------------------------------
1 | import { isArrayElement } from '../index';
2 |
3 | const data = [
4 | ['[]', true],
5 | ['[+12312]', true],
6 | ['[99]', true],
7 | ['key', false],
8 | [99, false],
9 | [null, false],
10 | [undefined, false],
11 | [false, false],
12 | [new Date(), false],
13 | [{}, false],
14 | ];
15 |
16 | describe('isArrayElement', () => {
17 | test.each(data)('isArrayElement(%s) === %b', (value, expected) => {
18 | const result = isArrayElement(value);
19 | expect(result).toEqual(expected);
20 | });
21 | });
--------------------------------------------------------------------------------
/src/tests/mutate.test.js:
--------------------------------------------------------------------------------
1 | import mutate from '../index';
2 |
3 |
4 | describe('mutate', () => {
5 | test('should except by object', () => {
6 | const obj = 10;
7 | const changes = [];
8 | try {
9 | mutate(obj, changes);
10 | } catch (ex) {
11 | expect(ex.message).toBe('Type of variable should be Object or Array');
12 | }
13 | });
14 |
15 | test('should except by object as null', () => {
16 | const obj = null;
17 | const changes = [];
18 | try {
19 | mutate(obj, changes);
20 | } catch (ex) {
21 | expect(ex.message).toBe('Type of variable should be Object or Array');
22 | }
23 | });
24 |
25 | test('should except by path', () => {
26 | const obj = { a: 100 };
27 | const changes = [ ['', 1000] ];
28 | try {
29 | mutate(obj, changes);
30 | } catch (ex) {
31 | expect(ex.message).toBe('Path should not be empty');
32 | }
33 | });
34 |
35 | test('should except by changes', () => {
36 | const obj = { a: 100 };
37 | const changes = 10;
38 | try {
39 | mutate(obj, changes);
40 | } catch (ex) {
41 | expect(ex.message).toBe('Changes should be Object or Array');
42 | }
43 | });
44 |
45 | test('should update deeply', () => {
46 | const obj = { a: { aa: { aaa: 10 }, aa2: { aa2a: 5 } }, b: { bb: { bbb: 1 } }, c: { cc: { ccc: 1 } } };
47 | const changes = [['a.aa.aaa', 15], ['c.cc2', 7]];
48 | const result = mutate(obj, changes);
49 | expect(result).not.toBe(obj);
50 | expect(result.a).not.toBe(obj.a);
51 |
52 | expect(result.a.aa).not.toBe(obj.a.aa);
53 | expect(result.a.aa.aaa).not.toBe(obj.a.aa.aaa);
54 |
55 | expect(result.a.aa2).toBe(obj.a.aa2);
56 | expect(result.a.aa2.aa2a).toBe(obj.a.aa2.aa2a);
57 |
58 | expect(result.b).toBe(obj.b);
59 | expect(result.b.bb).toBe(obj.b.bb);
60 | expect(result.b.bb.bbb).toBe(obj.b.bb.bbb);
61 |
62 | expect(result.c).not.toBe(obj.c);
63 | expect(result.c.cc).toBe(obj.c.cc);
64 | expect(result.c.cc2).not.toBe(obj.c.cc2);
65 | expect(result.c.cc.ccc).toBe(obj.c.cc.ccc);
66 | });
67 |
68 | test('should set object value', () => {
69 | function MyParentClass() {}
70 | MyParentClass.prototype.myFunc = () => 10;
71 | function MyClass() {}
72 | MyClass.prototype = Object.create(MyParentClass.prototype);
73 |
74 | const obj = { };
75 | const itArray = [1,2,3];
76 | const itMyObject = new MyClass();
77 |
78 | const itObject = { b1: 1, b2: 2 };
79 | const changes = [
80 | ['a.a1', itArray],
81 | ['a.a2', itMyObject],
82 | ['b', itObject]
83 | ];
84 | const result = mutate(obj, changes);
85 |
86 | expect(result.a.a1).toBe(itArray);
87 | expect(result.a.a2).toBe(itMyObject);
88 | expect(result.a.a2.myFunc).not.toBe(undefined);
89 | expect(result.a.a2.myFunc()).toBe(10);
90 | expect(result.b).toBe(itObject);
91 | });
92 |
93 | test('should replace by object value', () => {
94 | const obj = { b: { b5: 5, b6: 6 } };
95 | const changes = {
96 | b: { b1: 1, b2: 2, b3: 3 }
97 | };
98 | const result = mutate(obj, changes);
99 |
100 | expect(result.b).toEqual(changes.b);
101 | expect(result.b).toBe(changes.b);
102 | });
103 |
104 | test('should replace by array value', () => {
105 | const obj = { b: [5,6] };
106 | const changes = {
107 | b: [1,2,3]
108 | };
109 | const result = mutate(obj, changes);
110 |
111 | expect(result.b).toEqual(changes.b);
112 | expect(result.b).toBe(changes.b);
113 | });
114 |
115 | test('should change object value', () => {
116 | const obj = { b: [] };
117 | const patchObject = { b1: 1, b2: 2, b3: 3 };
118 | const changes = [
119 | ['b', patchObject],
120 | ['b.b4', 4]
121 | ];
122 | const result = mutate(obj, changes);
123 |
124 | expect(result.b).toEqual(patchObject);
125 | expect(result.b).toBe(patchObject);
126 | expect(patchObject).toEqual({ b1: 1, b2: 2, b3: 3, b4: 4 });
127 | });
128 |
129 | test('should change array value', () => {
130 | const obj = { b: [5,6] };
131 | const patchArray = [1,2,3];
132 | const changes = [
133 | ['b', patchArray],
134 | ['b.[]', 4]
135 | ];
136 | const result = mutate(obj, changes);
137 |
138 | expect(result.b).toEqual(patchArray);
139 | expect(result.b).toBe(patchArray);
140 | expect(patchArray).toEqual([1,2,3,4]);
141 | });
142 |
143 | test('should mutate array', () => {
144 | const obj = [5,6,7,8,9,10];
145 | const patched = [5,6,'X',8,'Y',10,1,2,3];
146 | const changes = [
147 | ['[]', 1],
148 | ['[]', 2],
149 | ['[]', 3],
150 | ['[2]', 'X'],
151 | ['[4]', 'Y']
152 | ];
153 | const result = mutate(obj, changes);
154 | expect(result).toEqual(patched);
155 | });
156 |
157 | test('should insert item into array', () => {
158 | const obj = [5,6,7,8,9,10];
159 | const patched = [222,'XXX',5,333,6,7,8,9,10,1, 'XXX'];
160 | const changes = [
161 | ['[]', 1],
162 | ['[>0]', 222],
163 | ['[>1]', 'XXX'],
164 | ['[>3]', 333],
165 | ['[>20]', 'XXX'],
166 | ];
167 | const result = mutate(obj, changes);
168 | expect(result).toEqual(patched);
169 | });
170 |
171 | test('should mutate element of array', () => {
172 | const obj = [5,{ a: 1, c: 3 }];
173 | const patched = [5,{ a: 1, b: 7, d: ['x', 'Z', 'q'] },9];
174 | const changes = [
175 | ['[1].c'],
176 | ['[1].b', 7],
177 | ['[1].d[]', 'x'],
178 | ['[1].d[]', 'y'],
179 | ['[1].d[1]', 'Z'],
180 | ['[1].d[]', 'q'],
181 | ['[]', 9],
182 | ];
183 | const result = mutate(obj, changes);
184 | expect(result).toEqual(patched);
185 | });
186 |
187 | test('should add and mutate element of array', () => {
188 | const obj = [5,{ a: 1, c: 3 }];
189 | const patched = [5,{ a: 1, c: 3, d: { d1: 'y', d2: 'Z' } }];
190 | const changes = [
191 | ['[1].d', {}],
192 | ['[1].d.d1', 'y'],
193 | ['[1].d.d2', 'Z'],
194 | ];
195 | const result = mutate(obj, changes);
196 | expect(result).toEqual(patched);
197 | });
198 |
199 | test('should ignore previously changed prop', () => {
200 | const obj = [5,{ a: 1, c: 3 }];
201 | const patched = [5,{ a: 1, c: 3, d: { d2: 'Z' } }];
202 | const changes = [
203 | // it will be ignored
204 | ['[1].d.d1', 'y'],
205 | // because this resets `d` object
206 | ['[1].d', {}],
207 | ['[1].d.d2', 'Z'],
208 | ];
209 | const result = mutate(obj, changes);
210 | expect(result).toEqual(patched);
211 | });
212 |
213 | test('should mutate source data', () => {
214 | const obj = {};
215 | const source = { a: 1, b: 2 };
216 | const patched = { data: { ...source, key: 7 } };
217 | const changes = [
218 | ['data', source],
219 | // data.key is similar to `source.key=7`, before mutation
220 | ['data.key', 7],
221 | ];
222 | const result = mutate(obj, changes);
223 |
224 | expect(result).toEqual(patched);
225 | expect(source).toEqual({ a: 1, b: 2, key: 7 });
226 | });
227 |
228 | test('should returns equal results for Array and Object changes', () => {
229 | const obj = {
230 | a: 100,
231 | b: 200,
232 | c: {
233 | c1: 1,
234 | c2: 2
235 | },
236 | d: []
237 | };
238 |
239 | const arrayChanges = [
240 | ['a', 111],
241 | ['b.b1', 222],
242 | ['b.b2', 'text'],
243 | ['c.c1', 20],
244 | ['c.c2'],
245 | ['d.[]', 10],
246 | ['d.[]', 20],
247 | ['e', [1,2,3]]
248 | ];
249 |
250 | const objectChanges = {
251 | 'a': 111,
252 | 'b.b1': 222,
253 | 'b.b2': 'text',
254 | 'c.c1': 20,
255 | 'c.c2': undefined,
256 | 'd.[+123434]': 10,
257 | 'd.[+554542]': 20,
258 | 'e': [1,2,3]
259 | };
260 |
261 | const result1 = mutate(obj, arrayChanges);
262 | const result2 = mutate(obj, objectChanges);
263 |
264 | expect(result1).toEqual(result2);
265 | });
266 |
267 |
268 | describe('like function', () => {
269 | test('should return function', () => {
270 | const obj = { a: 100 };
271 | const result = mutate(obj);
272 |
273 | expect(result instanceof Function).toBe(true);
274 | expect(result.length).toBe(1);
275 | });
276 |
277 | test('should do group changes changes', () => {
278 | const obj = { a: 100 };
279 | const changes1 = { a: 200 };
280 | const changes2 = { ['b.[]']: 150, e: 1000 };
281 | const changes3 = { ['b.[0]']: 300, c: 99, e: undefined };
282 |
283 | const func = mutate(obj);
284 | func(changes1);
285 | func(changes2);
286 | const result = func(changes3);
287 |
288 | expect(result).toEqual({
289 | a: 200,
290 | b: [300],
291 | c: 99
292 | });
293 | });
294 | });
295 | });
296 |
--------------------------------------------------------------------------------
/src/tests/mutateDeep.test.js:
--------------------------------------------------------------------------------
1 | import mutate, { deepPatch } from '../index';
2 |
3 | const data = [
4 | [
5 | 'Obj',
6 | { a: { a1: { a11: 10 }, a2: 20 }, b: 99 },
7 | { a: { a2: 18, a3: 30, a4: { a41: 41 } }, b: 100 },
8 | { a: { a1: { a11: 10 }, a2: 18, a3: 30, a4: { a41: 41 } }, b: 100 }
9 | ],
10 |
11 | [
12 | 'Obj-deep',
13 | { a: { a1: { a11: 10, a12: { a121: 17 } }, a2: 20 }, b: 99 },
14 | { a: { a1: deepPatch({ a12: { a122: 99 } }) } },
15 | { a: { a1: { a11: 10, a12: { a121: 17, a122: 99 } }, a2: 20 }, b: 99 }
16 | ],
17 |
18 | [
19 | 'deep',
20 | { a: { a1: 1, a4: { a41: 1, a42: 5 } } },
21 | deepPatch({ a: { a4: { a41: 41 } } }),
22 | { a: { a1: 1, a4: { a41: 41, a42: 5 } } }
23 | ],
24 |
25 | [
26 | 'arrayOf-Obj',
27 | { a: { a1: 1 } },
28 | [{ a4: { a41: 41, a45: 1 } }, { a4: { a41: 12, a42: 10, a43: [1,2,3] } }],
29 | { a: { a1: 1 }, a4: { a41: 12, a45: 1, a42: 10, a43: [1,2,3] } }
30 | ],
31 |
32 | [
33 | 'arrayOf-deep',
34 | { a: { a1: 1, a4: { a41: 1, a42: 5 } } },
35 | [deepPatch({ a: { a4: { a41: 41 } } }), deepPatch({ a: { a4: { a42: 42 } } })],
36 | { a: { a1: 1, a4: { a41: 41, a42: 42 } } }
37 | ],
38 | ];
39 |
40 | describe('mutate with deep', () => {
41 | test.each(data)('mutate.deep(*, %s)', (title, obj, ext, expected) => {
42 | expect(mutate.deep(obj, ext)).toEqual(expected);
43 | });
44 | });
--------------------------------------------------------------------------------
/src/tests/mutate_function.test.js:
--------------------------------------------------------------------------------
1 | import mutate from '../index';
2 |
3 |
4 | describe('mutate patch-function', () => {
5 | test('should return function', () => {
6 | const obj = [1, 2, 3];
7 | const patch = mutate(obj);
8 |
9 | expect(typeof patch).toEqual('function');
10 | expect(patch.length).toEqual(1);
11 | });
12 |
13 | test('should return new patched array each time', () => {
14 | const obj = [1, 2, 3];
15 | const patch = mutate(obj);
16 | const result1 = patch({ '[]': 10 });
17 | const result2 = patch({ '[]': 20 });
18 | const result3 = patch();
19 |
20 | expect(obj).toEqual([1, 2, 3]);
21 | expect(result1).toEqual([1, 2, 3, 10]);
22 | expect(result2).toEqual([1, 2, 3, 10, 20]);
23 | expect(result3).toBe(result2);
24 | });
25 |
26 | test('should return new patched object each time', () => {
27 | const obj = { a: 1, b: 2, c: 3 };
28 | const patch = mutate(obj);
29 | const result1 = patch({ d: 4 });
30 | const result2 = patch({ e: 5 });
31 | const result3 = patch();
32 |
33 | expect(obj).toEqual({ a: 1, b: 2, c: 3 });
34 | expect(result1).toEqual({ a: 1, b: 2, c: 3, d: 4 });
35 | expect(result2).toEqual({ a: 1, b: 2, c: 3, d: 4, e: 5 });
36 | expect(result3).toBe(result2);
37 | });
38 | });
--------------------------------------------------------------------------------
/src/tests/object_immutable.test.js:
--------------------------------------------------------------------------------
1 | import mutate from '../index';
2 |
3 |
4 | describe('mutate', () => {
5 | describe( 'should return the same object', () => {
6 | test('when changes are empty array', () => {
7 | const obj = { a: 1 };
8 | const changes = [];
9 | const result = mutate(obj, changes);
10 | expect(result).toBe(obj);
11 | });
12 |
13 | test('when changes are empty object', () => {
14 | const obj = { a: 1 };
15 | const changes = {};
16 | const result = mutate(obj, changes);
17 | expect(result).toBe(obj);
18 | });
19 |
20 | test('when new value is equal of current value', () => {
21 | const obj = { a: 1 };
22 | const changes = { a: 1 };
23 | const result = mutate(obj, changes);
24 | expect(result).toBe(obj);
25 | });
26 |
27 | test('when changes contains instruction to remove undefined elements', () => {
28 | const obj = { a: 1 };
29 | const changes = { b: undefined };
30 | const result = mutate(obj, changes);
31 | expect(result).toBe(obj);
32 | });
33 |
34 | test('when new value is equal of current value', () => {
35 | const obj = { a: 1 };
36 | const changes = { a: 1, b: undefined };
37 | const result = mutate(obj, changes);
38 | expect(result).toBe(obj);
39 | });
40 |
41 | test('when new value is equal of current value in deep path', () => {
42 | const obj = { a: { aa: [1,2,3] } };
43 | const changes = { 'a.aa.[2]': 3 };
44 | const result = mutate(obj, changes);
45 | expect(result).toBe(obj);
46 | });
47 |
48 | test('when new value is equal of current value in deep path', () => {
49 | const obj = { a: { aa: { aaa: 35 } } };
50 | const changes = { 'a.aa.aaa': 35 };
51 | const result = mutate(obj, changes);
52 | expect(result).toBe(obj);
53 | });
54 | });
55 |
56 | describe( 'should return new object', () => {
57 | test('when changes contains new value as array', () => {
58 | const obj = { a: 1 };
59 | const changes = [['a', 15]];
60 | const result = mutate(obj, changes);
61 | expect(result).not.toBe(obj);
62 | expect(result).toEqual({ a: 15 });
63 | });
64 |
65 | test('when changes contains new value as object', () => {
66 | const obj = { a: 1 };
67 | const changes = { a: 30 };
68 | const result = mutate(obj, changes);
69 | expect(result).not.toBe(obj);
70 | expect(result).toEqual({ a: 30 });
71 | });
72 |
73 | test('when changes contains new value', () => {
74 | const obj = { a: 1 };
75 | const changes = { a: 1, b: 7 };
76 | const result = mutate(obj, changes);
77 | expect(result).not.toBe(obj);
78 | expect(result).toEqual({ a: 1, b: 7 });
79 | });
80 |
81 | test('when changes contains instruction to remove defined prop', () => {
82 | const obj = { a: 1 };
83 | const changes = { a: undefined };
84 | const result = mutate(obj, changes);
85 | expect(result).not.toBe(obj);
86 | expect(result).toEqual({ });
87 | });
88 |
89 | test('when changes contains instruction to remove defined prop', () => {
90 | const obj = { a: { aa: { aaa: 35 } } };
91 | const changes = { 'a.aa.aaa': undefined };
92 | const result = mutate(obj, changes);
93 | expect(result).not.toBe(obj);
94 | expect(result).toEqual({ a: { aa: { } } });
95 | });
96 |
97 | test('when new value is not equal of current value in deep path', () => {
98 | const obj = { a: { aa: [1,2,3] } };
99 | const changes = { 'a.aa.[2]': 7 };
100 | const result = mutate(obj, changes);
101 | expect(result).not.toBe(obj);
102 | expect(result).toEqual({ a: { aa: [1,2,7] } });
103 | });
104 |
105 | test('when new value is not equal of current value in deep path', () => {
106 | const obj = { a: { aa: { aaa: 35 } } };
107 | const changes = { 'a.aa.aaa': 99 };
108 | const result = mutate(obj, changes);
109 | expect(result).not.toBe(obj);
110 | expect(result).toEqual({ a: { aa: { aaa: 99 } } });
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/src/tests/path_array.test.js:
--------------------------------------------------------------------------------
1 | import mutate from '../index';
2 |
3 | const data = [
4 | [{ 'a.01': 10 }, [[['a.01'], 5]], { 'a.01': 5 }],
5 | [{ a: { 'a.1': { 'a.1.1': 99 } } }, [['a:a.1:a.1.1'.split(':'), 5]], { a: { 'a.1': { 'a.1.1': 5 } } }],
6 | [{ a: { 'a.1': { 'a.1.1': 99 } } }, [['a:a.1:a.1.2'.split(':'), 5]], { a: { 'a.1': { 'a.1.1': 99, 'a.1.2': 5 } } }],
7 | [{ a: { 'a.1': { 'a.1.1': 99 } } }, [['a-a.1-a.1.1'.split('-')]], { a: { 'a.1': { } } }],
8 | [{ a: { 'a.1': { 'a.1.1': 99 } } }, [['a-a.1-a.1.2'.split('-')]], { a: { 'a.1': { 'a.1.1': 99 } } }],
9 | ];
10 |
11 |
12 | describe('mutate', () => {
13 | describe('path as Array', () => {
14 | test.each(data)('mutate(%j + %j)', (obj, changes, expected) => {
15 | expect(mutate(obj, changes)).toEqual(expected);
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/tests/separatePath.test.js:
--------------------------------------------------------------------------------
1 | import { separatePath } from '../index';
2 |
3 | const data = [
4 | ['a[]', 'a.[]'],
5 | ['a.[]', 'a.[]'],
6 | ['a.b.c[].d[]', 'a.b.c.[].d.[]'],
7 | ];
8 |
9 | describe('separatePath', () => {
10 | test.each(data)('separatePath(%s) === %s', (data, expected) => {
11 | expect(separatePath(data)).toBe(expected);
12 | });
13 | });
--------------------------------------------------------------------------------
/src/tests/splitPath.test.js:
--------------------------------------------------------------------------------
1 | import { splitPath } from '../index';
2 |
3 | const data = [
4 | ['a.b.c', ['a', 'b', 'c']],
5 | ['a.', ['a', '']],
6 | ['a.[0]', ['a', '[0]']],
7 | ['', ['']],
8 | ];
9 |
10 | const data2 = [
11 | [['a', 'b', 'c']],
12 | [[]],
13 | [[1,2,3]],
14 | [['a.02', 'b.01']],
15 | ];
16 |
17 | describe('splitPath', () => {
18 | test.each(data)('splitPath(%s) = %j', (data, expected) => {
19 | expect(splitPath(data)).toEqual(expected);
20 | });
21 |
22 | test.each(data2)('splitPath(%s)', (data) => {
23 | expect(splitPath(data)).toBe(data);
24 | });
25 |
26 | test('should return exception for Object path', () => {
27 | try {
28 | splitPath({});
29 | } catch (ex) {
30 | expect(ex.message).toBe('Path should be String or Array of Strings');
31 | }
32 | });
33 | });
--------------------------------------------------------------------------------