├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── apply-changes.png ├── basic-structure-for-objects-arrays.png ├── basic-structure-for-values.png ├── basic-use.png ├── comparators.png ├── comparators.png~ ├── differify-array-output.png ├── differify-object-output.png ├── how-to-use-differify-array-example.png ├── how-to-use-differify-object-example.png └── logo.svg ├── badges ├── badge-branches.svg ├── badge-functions.svg ├── badge-lines.svg └── badge-statements.svg ├── bin └── defaultExport.js ├── package-lock.json ├── package.json ├── src ├── comparator-selector.ts ├── comparators.ts ├── config-builder.ts ├── differify.ts ├── enums │ ├── modes.ts │ └── property-status.ts ├── property-diff-model.ts ├── types │ ├── comparators.ts │ ├── config.ts │ └── diff.ts └── utils │ └── validations.ts ├── test-dir └── index.js ├── test ├── comparators.test.ts ├── differify.benchmark.js ├── differify.test.ts └── property-diff-model.test.ts ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["airbnb-base"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | "no-plusplus": "off", 18 | "no-prototype-builtins": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Differify CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 16.x, 18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm test 29 | - run: npm run build --if-present 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist 4 | coverage 5 | index.js 6 | index.d.ts 7 | test-dir -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - [2023-06-05 21:49:34 -0300] (8aac961) fixing vulnerabilities + updating dependencies 2 | - [2021-06-02 23:38:40 -0300] (2e29448) Merge pull request #40 from netilon/hotfix_issue36 3 | - [2021-06-02 23:01:37 -0300] (8ae3397) adding more test to cover issue36 4 | - [2021-06-02 22:46:23 -0300] (fb9acba) updating README 5 | - [2021-06-02 22:19:27 -0300] (d19a837) adding changelog 6 | - [2021-06-02 22:13:39 -0300] (0fc2279) now the input data for the 'compare' method is included in its response (inside the 'original'/'current' properties). Before this update, if you used a diff mode different than DIFF_MODES.DIFF, then you got a null value for the '_' property, but now what you will get is not the '_' property but the properties 'original' and 'current' filled with the input data (first and second parameter). It will be usefull to use this output with methods like applyLeftChanges, applyRightChanges and filterDiffByStatus. This only apply when you use DIFF_MODES.REFERENCE OR DIFF_MODES.STRING to make them compatible with the mentioned methods.If you use the DIFF_MODES.DIFF in the config, the output will be the same. 7 | - [2021-03-21 13:16:30 -0300] (c981db5) Merge pull request #31 from netilon/feature_issue30 8 | - [2021-03-21 13:10:19 -0300] (2749631) removing redundant information in the extended filtered output 9 | - [2021-03-21 12:43:30 -0300] (007b871) updating documentation 10 | - [2021-03-21 12:27:19 -0300] (5f1da8a) adding feature for filterDiffByStatus method to be able to get extended information for each element in the output (issue30) + adding tests to address this new feature + upgrading package version -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2020, Fabian Roberto Orue - netilon.com 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use of this software in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above 11 | copyright notice, this list of conditions and the 12 | following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above 15 | copyright notice, this list of conditions and the 16 | following disclaimer in the documentation and/or other 17 | materials provided with the distribution. 18 | 19 | * Neither the name of Fabian Roberto Orue nor the names of its 20 | contributors may be used to endorse or promote products 21 | derived from this software without specific prior 22 | written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 27 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 31 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Differify](assets/logo.svg) 2 | 3 | ## One of the Fastest **deep** object/array diff 4 | 5 | - Benchmarks with other popular packages on the same category: 6 | @netilon/differify x 1,045,377 ops/sec ±1.42% (93 runs sampled) 7 | 8 | deep-object-diff x 184,838 ops/sec ±2.55% (85 runs sampled) 9 | 10 | recursive-diff x 108,276 ops/sec ±1.93% (94 runs sampled) 11 | 12 | Fastest is @netilon/differify 13 | 14 | ## Whats new? 15 | 16 | - Completely rewritten 17 | - The new version 4.x is **x2 faster** than the older versions (version < 3.0.0) 18 | and is now one of the **Fastests deep object/array** comparators. 19 | - Typescript support added. 20 | - Support for **Node.js** and **Browsers** (it works on both) 21 | - Just **7.6K (gzipped 2K)** weight (import) 22 | - **No dependencies** 23 | - **New features** were added! Now you can easily do more things with differify! 24 | - new config option added. Now, you can decide whether you prefer to compare arrays, either in an `ordered` or in an `unordered` way. Remember that, by default, you have an ordered comparison. 25 | - you can apply changes (merge) from `left to right` (applyRightChanges) or `right to left` (applyLeftChanges) 26 | - you can just `keep the differences between two entities` It's very useful indeed! (see more in the [Documentation](#id3) about the diffOnly option of `apply[Right|Left]Changes` methods). 27 | - you can filter the diff result of `compare()` method by an specific status (`ADDED`, `MODIFIED`, `DELETED`, `EQUAL`). 28 | 29 | ## Synopsis 30 | 31 | Differify allows you to get the diff between two entities (objects diff, arrays diff, date diff, functions diff, number diff, etc) very easily, quickly and in a friendly way. 32 | 33 | GitHub Workflow StatusGitHub package.json version 34 | 35 | ![](badges/badge-functions.svg) 36 | ![](badges/badge-lines.svg) 37 | ![](badges/badge-statements.svg) 38 | 39 | ## Your contribution is appreciated (thanks!) 40 | 41 | [![alt text](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif 'thanks for contribute!')](https://paypal.me/netilon) 42 | 43 | --- 44 | 45 | **Index** 46 | 47 | 1. [Installation](#id1) 48 | 2. [How to use](#id2) 49 | 3. [Documentation](#id3) 50 | 4. [Configuration](#id4) 51 | 5. [Typescript](#id5) 52 | 6. [Examples](#id6) 53 | 54 | --- 55 | 56 | ## Installation 57 | 58 | npm install @netilon/differify 59 | 60 | ## How to use it 61 | 62 | Comparing things with differify is **very simple**! 63 | 64 | ### **> Compare objects** 65 | 66 | ![](assets/how-to-use-differify-object-example.png) 67 | 68 | ### **> Object diff output** 69 | 70 | ![](assets/differify-object-output.png) 71 | 72 | ### **> Easy access and use** 73 | 74 | ![](assets/basic-use.png) 75 | 76 | ### **> Compare arrays** 77 | 78 | ![](assets/how-to-use-differify-array-example.png) 79 | 80 | ### **> Array diff output** 81 | 82 | ![](assets/differify-array-output.png) 83 | 84 | ### **Simple Structure** 85 | 86 | As you can see, there are two different kinds of structures that you can get from `compare` method call. 87 | 88 | 1. For objects and arrays **only**, you will get this structure: 89 | 90 | ![](assets/basic-structure-for-objects-arrays.png) 91 | 92 | - the `_` property contains the detailed diff information (it's an underscore to improve the readability in complex nested objects property accesses) 93 | - the `status` property contains the global status of the comparison ('EQUAL', 'MODIFIED', 'DELETED', 'ADDED') 94 | - the `changes` property is the total changes found when the comparison was performed. 95 | 96 | 2. For anything that `Object.prototype.toString.call()` does NOT return `[object Array]` or `[object Object]` (functions, dates, numbers, etc), you will get this structure: 97 | 98 | ![](assets/basic-structure-for-values.png) 99 | 100 | - the `original` property has the **original** value (left parameter in `compare` method). 101 | - the `current` property has the **current** value (right parameter in `compare` method). 102 | - the `status` property contains the current status of the comparison ('EQUAL', 'MODIFIED', 'DELETED', 'ADDED') 103 | - the `changes` property will be 1 or 0 depending if there was a change or not. 104 | 105 | ### **> Apply changes** 106 | 107 | ![](assets/apply-changes.png) 108 | 109 | # Documentation 110 | 111 | ## Methods 112 | 113 | **Method:** 114 | 115 | _setConfig(*object*);_ 116 | 117 | **Description:** It sets the configuration options that will be applied when compare() method is called. 118 | 119 | **Params:** 120 | 121 | Configuration Object (see the Configuration section). 122 | 123 | **Return:** void 124 | 125 | --- 126 | 127 | **Method:** 128 | 129 | _getConfig();_ 130 | 131 | **Description:** It returns a copy of the current configuration object. 132 | 133 | **Return:** Object 134 | 135 | --- 136 | 137 | **Method:** 138 | 139 | _compare(*any*, *any*);_ 140 | 141 | **Description:** It returns the difference between two entities. 142 | 143 | **Params:** 144 | 145 | Both parameters indicate the entities to be compared. 146 | 147 | **Return:** Object. 148 | 149 | --- 150 | 151 | **Method:** 152 | 153 | _applyRightChanges(diffResult, diffOnly);_ 154 | 155 | **Description:** It will apply the changes (merge both entities) and will keep the modified values **from the right**. 156 | 157 | **Params:** 158 | 159 | _diffResult_: Object - It is the Object returned by the `compare()` method call. 160 | 161 | _diffOnly_: boolean - (default: false) It returns just the difference (only the !== `EQUAL` properties). 162 | 163 | **Return:** Object. 164 | 165 | --- 166 | 167 | **Method:** 168 | 169 | _applyLeftChanges(diffResult, diffOnly);_ 170 | 171 | **Description:** It will apply the changes (merge both entities) and will keep the modified values **from the left**. 172 | 173 | **Params:** 174 | 175 | _diffResult_: Object - It is the Object returned by the `compare()` method call. 176 | 177 | _diffOnly_: boolean - (default: false) It returns just the difference (only the !== `EQUAL` properties). 178 | 179 | **Return:** Object. 180 | 181 | --- 182 | 183 | **Method:** 184 | 185 | _filterDiffByStatus(diffResult, status, extendedInformation);_ 186 | 187 | **Description:** It will return the changes that match the specified property status (second parameter) using the **DIFF_MODES.DIFF** in the configuration. **IMPORTANT:** If you use another diff mode (different than DIFF_MODES.DIFF), then you will get the corresponding value from original/current property (based on the status) without any filtering (you will get the raw value). 188 | 189 | **Params:** 190 | 191 | _diffResult_: Object - It is the Object returned by the `compare()` method call. 192 | 193 | _status_: string - one of the following (`ADDED` || `MODIFIED` || `DELETED` || `EQUAL`). 194 | 195 | _extendedInformation_: boolean - if true, it will add more detail about the elements to the given output. Defaults to false. 196 | 197 | **Return:** any || null - it depends on the input type. If there is no status matching, then null will be returned. 198 | 199 | --- 200 | 201 | ## Configuration 202 | 203 | You can pass a config to the setConfig() method to change the behavior and adjust it to fit your needs. If you prefer, you can set it once and use it everywhere or you can change it when you need it. 204 | 205 | | key | value | default | description | 206 | | ---------------------- | ------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 207 | | _mode.array_ | string | DIFF | **DIFF**: it will iterate over each element in the array A, and will compare each element against the element in the same index in the array B.

**REFERENCE**: just compare the references of each array.

**STRING**: only will check the array length and will do a toString comparison if necessary. | 208 | | _mode.object_ | string | DIFF | **DIFF**: it will iterate over each property in the object A and will compare each value with the same property value in the object B.

**REFERENCE**: just compare the references of each object.

**STRING**: only do a toString comparison. | 209 | | _mode.function_ | string | REFERENCE | **REFERENCE**: just compare the references of each function.

**STRING**: only do a toString comparison (useful to compare the function bodies). | 210 | | _compareArraysInOrder_ | boolean | true | if it is `true`, it will compare each element in both arrays one by one in an ordered way, assuming that each element, in the same index in both arrays, should be the same element that can have changes or not ([see an example of this case](#id6)). If the option is set to `false`, then it will compare each element in both arrays and it will check if they are `EQUAL`, `ADDED` or `DELETED` without keeping in mind the appearence order (there won't be details about the `MODIFIED` status, it's only available if the option is set to true, since in that case, the order matters and we know that each element in the same index (but in different arrays), should be the same element that could have been changed or not) ([see an example of this case](#id7)). | 211 | 212 | **Configuration example:** 213 | 214 | const Differify = require('@netilon/differify'); 215 | 216 | differify = new Differify({ mode: { object: 'DIFF', array: 'DIFF' } }); 217 | 218 | const diff = differify.compare(a, b); 219 | 220 | if you dont specify any configuration, the default options are the following: 221 | 222 | { 223 | mode: { 224 | array: 'DIFF', 225 | object: 'DIFF', 226 | function: 'REFERENCE', 227 | } 228 | 229 | ## Typescript 230 | 231 | To use the differify library with Typescript, you have to configure your tsconfig.json file and enable the following properties: 232 | 233 | tsconfig.json 234 | 235 | { 236 | "compilerOptions": { 237 | "allowJs": true, 238 | "esModuleInterop": true, 239 | } 240 | } 241 | 242 | then in your .ts file, you can import Differify this way: 243 | 244 | import Differify, { DIFF_MODES } from '@netilon/differify'; 245 | 246 | // See the examples section. 247 | 248 | ## Examples 249 | 250 | You have to know that the configuration you provide will change the behavior of the comparators and it will result in different outputs. 251 | 252 | Just play around with it and use the configuration that fits your needs. 253 | 254 | The following image, **just represents the idea** of what each option does, but **is not** the real implementation: 255 | 256 | ![](assets/comparators.png) 257 | 258 | _Eg:_ 259 | 260 | ### with the option **_DIFF_**: 261 | 262 | const testA = [1,2]; 263 | const testB = [1,3]; 264 | 265 | you will get this output (note that there is a detail for each element in the array A and B): 266 | 267 | { 268 | "_": [{ 269 | "original": 1, 270 | "current": 1, 271 | "status": "EQUAL", 272 | "changes": 0 273 | }, { 274 | "original": 2, 275 | "current": 3, 276 | "status": "MODIFIED", 277 | "changes": 1 278 | }], 279 | "status": "MODIFIED", 280 | "changes": 1 281 | } 282 | 283 | ### with the option **_STRING_** or **_REFERENCE_**: 284 | 285 | const testA = [1,2]; 286 | const testB = [1,3]; 287 | 288 | you will get this output (just a string comparison): 289 | 290 | { 291 | "original": [1,2], 292 | "current": [1,3], 293 | "status": "MODIFIED", 294 | // always will be 1 or 0 because there is no 295 | // deep checking (use the DIFF option if you want more information) 296 | "changes": 1 297 | } 298 | 299 | ### Array comparison example, keeping the order (compareArraysInOrder: true) 300 | 301 | const differify = new Differify({ 302 | compareArraysInOrder: true, //default value 303 | mode: { object: 'DIFF', array: 'DIFF' }, 304 | }); 305 | 306 | const diff = differify.compare(['a', 'b'], ['a', 'z', 'b']); 307 | 308 | /* 309 | OUTPUT 310 | 311 | { 312 | "_": [{ 313 | "original": "a", 314 | "current": "a", 315 | "status": "EQUAL", 316 | "changes": 0 317 | }, { 318 | "original": "b", 319 | "current": "z", 320 | "status": "MODIFIED", 321 | "changes": 1 322 | }, { 323 | "original": null, 324 | "current": "b", 325 | "status": "ADDED", 326 | "changes": 1 327 | }], 328 | "status": "MODIFIED", 329 | "changes": 2 330 | } 331 | 332 | */ 333 | 334 | ### Array comparison example, without having the order in mind (compareArraysInOrder: false).. 335 | 336 | **NOTE**: In case you have Object elements inside the array and you are using compareArraysInOrder to false and array mode to true, then it will compare the objects as serialized strings to know if they are equal or not. 337 | 338 | const differify = new Differify({ 339 | compareArraysInOrder: false, 340 | mode: { object: 'DIFF', array: 'DIFF' }, 341 | }); 342 | 343 | const diff = differify.compare(['a', 'b'], ['a', 'z', 'b']); 344 | 345 | /* 346 | OUTPUT 347 | 348 | 349 | { 350 | "_": [{ 351 | "original": "a", 352 | "current": "a", 353 | "status": "EQUAL", 354 | "changes": 0 355 | }, { 356 | "original": "b", 357 | "current": "b", 358 | "status": "EQUAL", 359 | "changes": 0 360 | }, { 361 | "original": null, 362 | "current": "z", 363 | "status": "ADDED", 364 | "changes": 1 365 | }], 366 | "status": "MODIFIED", 367 | "changes": 1 368 | } 369 | 370 | */ 371 | 372 | ## Your contribution is appreciated (thanks!) 373 | 374 | [![alt text](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif 'thanks for contribute!')](https://paypal.me/netilon) 375 | -------------------------------------------------------------------------------- /assets/apply-changes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/apply-changes.png -------------------------------------------------------------------------------- /assets/basic-structure-for-objects-arrays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/basic-structure-for-objects-arrays.png -------------------------------------------------------------------------------- /assets/basic-structure-for-values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/basic-structure-for-values.png -------------------------------------------------------------------------------- /assets/basic-use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/basic-use.png -------------------------------------------------------------------------------- /assets/comparators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/comparators.png -------------------------------------------------------------------------------- /assets/comparators.png~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/comparators.png~ -------------------------------------------------------------------------------- /assets/differify-array-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/differify-array-output.png -------------------------------------------------------------------------------- /assets/differify-object-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/differify-object-output.png -------------------------------------------------------------------------------- /assets/how-to-use-differify-array-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/how-to-use-differify-array-example.png -------------------------------------------------------------------------------- /assets/how-to-use-differify-object-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netilon/differify/734f5b052e379f8967d4c5ceef0e28ca8051df00/assets/how-to-use-differify-object-example.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 25 | 26 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 62 | 65 | Differify 72 | 79 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /badges/badge-branches.svg: -------------------------------------------------------------------------------- 1 | Coverage:branches: 94.04%Coverage:branches94.04% -------------------------------------------------------------------------------- /badges/badge-functions.svg: -------------------------------------------------------------------------------- 1 | Coverage:functions: 100%Coverage:functions100% -------------------------------------------------------------------------------- /badges/badge-lines.svg: -------------------------------------------------------------------------------- 1 | Coverage:lines: 100%Coverage:lines100% -------------------------------------------------------------------------------- /badges/badge-statements.svg: -------------------------------------------------------------------------------- 1 | Coverage:statements: 100%Coverage:statements100% -------------------------------------------------------------------------------- /bin/defaultExport.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | (function () { 5 | try { 6 | const file = path.resolve(__dirname, '..', 'index.d.ts'); 7 | const content = fs.readFileSync(file, { encoding: 'utf8' }); 8 | const toAppend = 'export default Differify'; 9 | if (!content.includes(toAppend)) { 10 | let newContent = ''; 11 | newContent += content; 12 | newContent += '\n\n'; 13 | newContent += toAppend; 14 | fs.writeFileSync(file, newContent); 15 | console.log('default export added!'); 16 | } else { 17 | console.log('default export already exists!, aborting operation.'); 18 | } 19 | } catch (e) { 20 | console.log('index.d.ts: ', e.message); 21 | console.log('aborting operation...'); 22 | } 23 | })(); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netilon/differify", 3 | "version": "4.0.1", 4 | "description": "Differify allows you to get the diff between two entities (objects diff, arrays diff, date diff, functions diff, number diff, etc) very easily, quickly and in a friendly way.", 5 | "main": "./index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "index.js", 9 | "index.d.ts" 10 | ], 11 | "scripts": { 12 | "start": "NODE_ENV=production node index.js", 13 | "start:dev": "NODE_ENV=development nodemon index.js", 14 | "debug": "node --inspect-brk=9229 --nolazy ./test-dir/index.js", 15 | "build": "rm -rf ./index.js && rm -rf ./index.d.ts && npx webpack && node ./bin/defaultExport.js", 16 | "prepare": "npm run test -- --coverage && npm run build", 17 | "lint": "eslint ./src", 18 | "test": "jest", 19 | "changelog": "git log --oneline --pretty=format:'- [%ci] (%h) %s' -n 10 > CHANGELOG.md", 20 | "coverage": "jest --coverage && jest-coverage-badges --output './badges'", 21 | "benchmark": "node ./test/differify.benchmark.js" 22 | }, 23 | "pre-commit": [ 24 | "test" 25 | ], 26 | "keywords": [ 27 | "differify", 28 | "diff", 29 | "object diff", 30 | "array diff", 31 | "difference", 32 | "compare", 33 | "comparator", 34 | "comparison", 35 | "node", 36 | "react", 37 | "angular", 38 | "vue", 39 | "browser", 40 | "netilon" 41 | ], 42 | "author": "Fabian Roberto Orue (https://www.netilon.com)", 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/netilon/differify" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/netilon/differify/issues", 49 | "email": "fabianorue@gmail.com" 50 | }, 51 | "homepage": "http://www.netilon.com", 52 | "license": "BSD-3-Clause", 53 | "devDependencies": { 54 | "@babel/core": "^7.14.3", 55 | "@babel/preset-env": "^7.14.4", 56 | "babel-loader": "^8.2.2", 57 | "benchmark": "^2.1.4", 58 | "eslint": "^6.8.0", 59 | "eslint-config-airbnb-base": "^14.2.1", 60 | "eslint-plugin-import": "^2.23.4", 61 | "jest": "^26.6.3", 62 | "jest-coverage-badges": "^1.0.0", 63 | "nodemon": "^2.0.7", 64 | "pre-commit": "^1.2.2", 65 | "ts-jest": "^26.5.6", 66 | "ts-loader": "^8.3.0", 67 | "ts-node": "^9.1.1", 68 | "typescript": "^4.3.2", 69 | "typescript-declaration-webpack-plugin": "^0.2.2", 70 | "webpack": "^5.85.1", 71 | "webpack-cli": "^5.1.3", 72 | "webpack-comment-remover-loader": "0.0.3" 73 | }, 74 | "jest": { 75 | "coverageReporters": [ 76 | "json-summary", 77 | "text", 78 | "lcov" 79 | ], 80 | "preset": "ts-jest/presets/js-with-ts", 81 | "globals": { 82 | "ts-jest": { 83 | "diagnostics": { 84 | "ignoreCodes": [ 85 | 2345, 86 | 2339, 87 | 2322, 88 | 2322, 89 | 2740, 90 | 2349, 91 | 2554, 92 | 2304, 93 | 2582 94 | ] 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/comparator-selector.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | 6 | import DIFF_MODES from './enums/modes'; 7 | import { buildDiff, buildDeepDiff } from './property-diff-model'; 8 | import PROPERTY_STATUS from './enums/property-status'; 9 | import { 10 | valueRefEqualityComparator, 11 | arraySimpleComparator, 12 | dateComparator, 13 | toStringComparator, 14 | getConfiguredOrderedDeepArrayComparator, 15 | getConfiguredUnorderedDeepArrayComparator, 16 | getConfiguredDeepObjectComparator, 17 | JSONStringComparator, 18 | } from './comparators'; 19 | import { 20 | ComparatorMethods, 21 | comparatorTypes, 22 | comparator, 23 | } from './types/comparators'; 24 | import config from './types/config'; 25 | import { multiPropDiff } from './types/diff'; 26 | 27 | export default function comparatorSelector(): ComparatorMethods { 28 | //types 29 | const typeMap: comparatorTypes = { 30 | string: null, 31 | number: null, 32 | boolean: null, 33 | function: null, 34 | object: null, 35 | }; 36 | 37 | const deepTypeMap = {}; 38 | 39 | //comparator selectors 40 | function multipleComparatorSelector(a, b): multiPropDiff { 41 | if (a === b) { 42 | return buildDiff(a, b, PROPERTY_STATUS.EQUAL); 43 | } 44 | 45 | const aType = typeof a; 46 | const bType = typeof b; 47 | 48 | if (aType !== bType) { 49 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 50 | } 51 | const comparator = typeMap[aType]; 52 | return comparator ? comparator(a, b) : valueRefEqualityComparator(a, b); 53 | } 54 | 55 | function deepComparatorSelector(a, b): multiPropDiff { 56 | // checks array => date => object 57 | const aType = Object.prototype.toString.call(a); 58 | const bType = Object.prototype.toString.call(b); 59 | 60 | if (aType === bType) { 61 | const comparator = deepTypeMap[aType]; 62 | return comparator ? comparator(a, b) : valueRefEqualityComparator(a, b); 63 | } 64 | 65 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 66 | } 67 | 68 | function configure(config: config): void { 69 | const objectComp: { [key: string]: comparator } = {}; 70 | 71 | objectComp[DIFF_MODES.DIFF] = getConfiguredDeepObjectComparator( 72 | multipleComparatorSelector 73 | ); 74 | objectComp[DIFF_MODES.REFERENCE] = (a, b) => { 75 | const pDiff = valueRefEqualityComparator(a, b); 76 | return buildDiff(a, b, pDiff.status, pDiff.changes); 77 | }; 78 | objectComp[DIFF_MODES.STRING] = (a, b) => { 79 | const pDiff = JSONStringComparator(a, b); 80 | return buildDiff(a, b, pDiff.status, pDiff.changes); 81 | }; 82 | const arrayComp = {}; 83 | //TODO: si el modo es deepUnorderedArrayComparator entonces el comparar objetos 84 | //dentro del array, debe ser no deep STRING mode 85 | arrayComp[DIFF_MODES.DIFF] = config.compareArraysInOrder 86 | ? getConfiguredOrderedDeepArrayComparator(multipleComparatorSelector) 87 | : getConfiguredUnorderedDeepArrayComparator(multipleComparatorSelector); 88 | arrayComp[DIFF_MODES.REFERENCE] = (a, b) => { 89 | const pDiff = valueRefEqualityComparator(a, b); 90 | return buildDiff(a, b, pDiff.status, pDiff.changes); 91 | }; 92 | arrayComp[DIFF_MODES.STRING] = (a, b) => { 93 | const pDiff = arraySimpleComparator(a, b); 94 | return buildDiff(a, b, pDiff.status, pDiff.changes); 95 | }; 96 | const functionComp = {}; 97 | functionComp[DIFF_MODES.REFERENCE] = valueRefEqualityComparator; 98 | functionComp[DIFF_MODES.STRING] = toStringComparator; 99 | 100 | typeMap.string = valueRefEqualityComparator; 101 | typeMap.number = valueRefEqualityComparator; 102 | typeMap.boolean = valueRefEqualityComparator; 103 | typeMap.function = functionComp[config.mode.function]; 104 | typeMap.object = deepComparatorSelector; 105 | 106 | deepTypeMap['[object Array]'] = arrayComp[config.mode.array]; 107 | deepTypeMap['[object Date]'] = dateComparator; 108 | deepTypeMap['[object Object]'] = objectComp[config.mode.object]; 109 | } 110 | 111 | function getComparatorByType(type: string): comparator { 112 | return typeMap[type]; 113 | } 114 | 115 | return { 116 | multipleComparatorSelector, 117 | deepComparatorSelector, 118 | getComparatorByType, 119 | configure, 120 | }; 121 | } 122 | -------------------------------------------------------------------------------- /src/comparators.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | 6 | import PROPERTY_STATUS from './enums/property-status'; 7 | import { buildDiff, buildDeepDiff } from './property-diff-model'; 8 | import { multiPropDiff, deepPropDiff } from './types/diff'; 9 | import { comparator } from './types/comparators'; 10 | import { has } from './utils/validations'; 11 | 12 | export function valueRefEqualityComparator(a, b): multiPropDiff { 13 | if (a === b) { 14 | return buildDiff(a, b, PROPERTY_STATUS.EQUAL); 15 | } 16 | 17 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 18 | } 19 | 20 | export function dateComparator(aDate, bDate): multiPropDiff { 21 | if (aDate.getTime() === bDate.getTime()) { 22 | return buildDiff(aDate, bDate, PROPERTY_STATUS.EQUAL); 23 | } 24 | 25 | return buildDiff(aDate, bDate, PROPERTY_STATUS.MODIFIED, 1); 26 | } 27 | 28 | export function arraySimpleComparator(aArr, bArr): multiPropDiff { 29 | if (aArr.length === bArr.length) { 30 | if (JSON.stringify(aArr) === JSON.stringify(bArr)) { 31 | return buildDiff(aArr, bArr, PROPERTY_STATUS.EQUAL); 32 | } 33 | } 34 | return buildDiff(aArr, bArr, PROPERTY_STATUS.MODIFIED, 1); 35 | } 36 | 37 | export function JSONStringComparator(a, b): multiPropDiff { 38 | if (JSON.stringify(a) === JSON.stringify(b)) { 39 | return buildDiff(a, b, PROPERTY_STATUS.EQUAL); 40 | } 41 | 42 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 43 | } 44 | 45 | export function toStringComparator(a, b): multiPropDiff { 46 | if (a.toString() === b.toString()) { 47 | return buildDiff(a, b, PROPERTY_STATUS.EQUAL); 48 | } 49 | 50 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 51 | } 52 | 53 | /** 54 | * Compare each element keeping the order of each one. 55 | * @param {*} aArr 56 | * @param {*} bArr 57 | */ 58 | 59 | export function getConfiguredOrderedDeepArrayComparator( 60 | multipleComparator: comparator 61 | ): comparator { 62 | function orderedDeepArrayComparator(aArr, bArr) { 63 | let maxArr; 64 | let minArr; 65 | let listALargerThanB = 0; // 0 equal \ -1 a major | 1 b major 66 | if (aArr.length > bArr.length || aArr.length === bArr.length) { 67 | maxArr = aArr; 68 | minArr = bArr; 69 | listALargerThanB = -1; 70 | } else { 71 | maxArr = bArr; 72 | minArr = aArr; 73 | listALargerThanB = 1; 74 | } 75 | 76 | const ret = []; 77 | let changes = 0; 78 | let i; 79 | for (i = 0; i < minArr.length; ++i) { 80 | ret.push(multipleComparator(aArr[i], bArr[i])); 81 | changes += ret[i].changes || 0; 82 | } 83 | if (listALargerThanB === -1) { 84 | for (i; i < maxArr.length; ++i) { 85 | ret.push(buildDiff(aArr[i], null, PROPERTY_STATUS.DELETED, 1)); 86 | ++changes; 87 | } 88 | } else if (listALargerThanB === 1) { 89 | for (i; i < maxArr.length; ++i) { 90 | ret.push(buildDiff(null, bArr[i], PROPERTY_STATUS.ADDED, 1)); 91 | ++changes; 92 | } 93 | } 94 | 95 | return buildDeepDiff( 96 | ret, 97 | changes > 0 ? PROPERTY_STATUS.MODIFIED : PROPERTY_STATUS.EQUAL, 98 | changes 99 | ); 100 | } 101 | 102 | return orderedDeepArrayComparator; 103 | } 104 | 105 | /** 106 | * Compare the array in an unordered way, without having in mind the 107 | * order of each element. It will look for equality if not, the element 108 | * is treated as a difference. 109 | * @param {*} multipleComparator 110 | */ 111 | 112 | export function getConfiguredUnorderedDeepArrayComparator( 113 | multipleComparator: comparator 114 | ): comparator { 115 | function deepUnorderedArrayComparator(aArr, bArr) { 116 | let maxArr; 117 | if (aArr.length >= bArr.length) { 118 | maxArr = aArr; 119 | } else { 120 | maxArr = bArr; 121 | } 122 | 123 | let changes = 0; 124 | let i; 125 | const ret = []; 126 | let key; 127 | let comparatorRes; 128 | let currElement; 129 | let currMapElement; 130 | let keyList; 131 | 132 | const comparisonPairsMap = Object.create(null); 133 | for (i = 0; i < maxArr.length; ++i) { 134 | if (i < aArr.length) { 135 | currElement = aArr[i]; 136 | key = JSON.stringify(currElement); 137 | keyList = comparisonPairsMap[key]; 138 | if (keyList !== undefined && keyList.length > 0) { 139 | currMapElement = keyList[keyList.length - 1]; 140 | if (currMapElement.b !== null) { 141 | comparatorRes = multipleComparator(currElement, currMapElement.b); 142 | 143 | ret.push(comparatorRes); 144 | keyList.pop(); 145 | if (keyList.length === 0) { 146 | delete comparisonPairsMap[key]; 147 | } 148 | } else { 149 | keyList.unshift({ 150 | a: currElement, 151 | b: null, 152 | }); 153 | } 154 | } else { 155 | comparisonPairsMap[key] = [ 156 | { 157 | a: currElement, 158 | b: null, 159 | }, 160 | ]; 161 | } 162 | } 163 | if (i < bArr.length) { 164 | currElement = bArr[i]; 165 | key = JSON.stringify(currElement); 166 | keyList = comparisonPairsMap[key]; 167 | if (keyList !== undefined && keyList.length > 0) { 168 | currMapElement = keyList[keyList.length - 1]; 169 | if (currMapElement.a !== null) { 170 | comparatorRes = multipleComparator(currMapElement.a, currElement); 171 | 172 | ret.push(comparatorRes); 173 | keyList.pop(); 174 | if (keyList.length === 0) { 175 | delete comparisonPairsMap[key]; 176 | } 177 | } else { 178 | keyList.unshift({ 179 | a: null, 180 | b: currElement, 181 | }); 182 | } 183 | } else { 184 | comparisonPairsMap[key] = [ 185 | { 186 | a: null, 187 | b: currElement, 188 | }, 189 | ]; 190 | } 191 | } 192 | } 193 | 194 | //matchAll 195 | let uncomparedPair = Object.create(null); 196 | uncomparedPair.a = []; 197 | uncomparedPair.b = []; 198 | 199 | for (let key in comparisonPairsMap) { 200 | keyList = comparisonPairsMap[key]; 201 | for (let i = 0; i < keyList.length; ++i) { 202 | currMapElement = keyList[i]; 203 | if (currMapElement.a) { 204 | if (uncomparedPair.b.length > 0) { 205 | comparatorRes = multipleComparator( 206 | currMapElement.a, 207 | uncomparedPair.b.pop() 208 | ); 209 | 210 | changes += comparatorRes.changes; 211 | ret.push(comparatorRes); 212 | } else { 213 | uncomparedPair.a.unshift(currMapElement.a); 214 | } 215 | } else if (currMapElement.b) { 216 | if (uncomparedPair.a.length > 0) { 217 | comparatorRes = multipleComparator( 218 | uncomparedPair.a.pop(), 219 | currMapElement.b 220 | ); 221 | 222 | changes += comparatorRes.changes; 223 | ret.push(comparatorRes); 224 | } else { 225 | uncomparedPair.b.unshift(currMapElement.b); 226 | } 227 | } 228 | } 229 | } 230 | 231 | for (let i = uncomparedPair.a.length - 1; i > -1; --i) { 232 | ret.push( 233 | buildDiff(uncomparedPair.a[i], null, PROPERTY_STATUS.DELETED, 1) 234 | ); 235 | ++changes; 236 | } 237 | 238 | for (let i = uncomparedPair.b.length - 1; i > -1; --i) { 239 | ret.push(buildDiff(null, uncomparedPair.b[i], PROPERTY_STATUS.ADDED, 1)); 240 | ++changes; 241 | } 242 | 243 | return buildDeepDiff( 244 | ret, 245 | changes > 0 ? PROPERTY_STATUS.MODIFIED : PROPERTY_STATUS.EQUAL, 246 | changes 247 | ); 248 | } 249 | return deepUnorderedArrayComparator; 250 | } 251 | 252 | export function getConfiguredDeepObjectComparator( 253 | multipleComparator: comparator 254 | ): comparator { 255 | function deepObjectComparator(a, b) { 256 | const ret = {}; 257 | let aLength = 0; 258 | let bLength = 0; 259 | let changes = 0; 260 | for (const propA in a) { 261 | if (has(a, propA)) { 262 | ++aLength; 263 | if (has(b, propA)) { 264 | ret[propA] = multipleComparator(a[propA], b[propA]); 265 | } else { 266 | ret[propA] = buildDiff(a[propA], null, PROPERTY_STATUS.DELETED, 1); 267 | } 268 | changes += ret[propA].changes; 269 | } 270 | } 271 | 272 | for (const propB in b) { 273 | if (has(b, propB)) { 274 | ++bLength; 275 | if (!has(a, propB)) { 276 | //TODO: avoid multiple indirections. 277 | ret[propB] = buildDiff(null, b[propB], PROPERTY_STATUS.ADDED, 1); 278 | changes += ret[propB].changes; 279 | } 280 | } 281 | } 282 | 283 | return aLength === 0 && bLength === 0 284 | ? buildDeepDiff(null, PROPERTY_STATUS.EQUAL, changes) 285 | : buildDeepDiff( 286 | ret, 287 | changes > 0 ? PROPERTY_STATUS.MODIFIED : PROPERTY_STATUS.EQUAL, 288 | changes 289 | ); 290 | } 291 | return deepObjectComparator; 292 | } 293 | -------------------------------------------------------------------------------- /src/config-builder.ts: -------------------------------------------------------------------------------- 1 | import DIFF_MODES from './enums/modes'; 2 | import { isObject, isValidString } from './utils/validations'; 3 | import config from './types/config'; 4 | export default function Configuration(config?: config) { 5 | this.compareArraysInOrder = true; 6 | 7 | this.mode = { 8 | array: DIFF_MODES.DIFF, 9 | object: DIFF_MODES.DIFF, 10 | function: DIFF_MODES.REFERENCE, 11 | }; 12 | 13 | if (isObject(config)) { 14 | if (typeof config.compareArraysInOrder === 'boolean') { 15 | this.compareArraysInOrder = config.compareArraysInOrder; 16 | } 17 | 18 | if (isObject(config.mode)) { 19 | const allowedComparissions = Object.values(DIFF_MODES); 20 | 21 | if (isValidString(config.mode.array)) { 22 | const comparison = config.mode.array.toUpperCase(); 23 | if ( 24 | allowedComparissions.find((prop) => prop === comparison) !== undefined 25 | ) { 26 | this.mode.array = comparison; 27 | } 28 | } 29 | 30 | if (isValidString(config.mode.object)) { 31 | const comparison = config.mode.object.toUpperCase(); 32 | if ( 33 | allowedComparissions.find((prop) => prop === comparison) !== undefined 34 | ) { 35 | this.mode.object = comparison; 36 | } 37 | } 38 | if (isValidString(config.mode.function)) { 39 | const comparison = config.mode.function.toUpperCase(); 40 | if ( 41 | comparison === DIFF_MODES.REFERENCE || 42 | comparison === DIFF_MODES.STRING 43 | ) { 44 | this.mode.function = comparison; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/differify.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | import DIFF_MODES from './enums/modes'; 6 | import { isArray } from './utils/validations'; 7 | import { buildDiff } from './property-diff-model'; 8 | import Configuration from './config-builder'; 9 | import PROPERTY_STATUS from './enums/property-status'; 10 | import { valueRefEqualityComparator } from './comparators'; 11 | import comparatorSelector from './comparator-selector'; 12 | import { propertySelector, ComparatorMethods } from './types/comparators'; 13 | import config from './types/config'; 14 | import { multiPropDiff, deepPropDiff, propDiff } from './types/diff'; 15 | 16 | const INVALID_VAL = Symbol('invalid'); 17 | 18 | function diff( 19 | comparatorSelector: ComparatorMethods, 20 | a: any, 21 | b: any 22 | ): multiPropDiff { 23 | // here, we avoid comparing by reference because of the nested objects can be changed 24 | const aType = typeof a; 25 | const bType = typeof b; 26 | 27 | if (aType !== bType) { 28 | return buildDiff(a, b, PROPERTY_STATUS.MODIFIED, 1); 29 | } 30 | const comparator = comparatorSelector.getComparatorByType(aType); 31 | return comparator ? comparator(a, b) : valueRefEqualityComparator(a, b); 32 | } 33 | 34 | /** 35 | * It returns a normalized output based on the type of the 36 | * input when the output is invalid. 37 | * @param inputData 38 | * @param outputData 39 | * @returns 40 | */ 41 | function normalizeInvalidOutputFormat(inputData, outputData) { 42 | return outputData === INVALID_VAL 43 | ? Array.isArray(inputData) 44 | ? [] 45 | : {} 46 | : outputData; 47 | } 48 | 49 | const applyChanges = (next, selector: propertySelector) => { 50 | if (isArray(next)) { 51 | const list = []; 52 | let curr; 53 | for (let i = 0; i < next.length; i++) { 54 | curr = selector(next[i]); 55 | if (curr !== INVALID_VAL) { 56 | list.push(curr); 57 | } 58 | } 59 | 60 | return list.length === 0 ? INVALID_VAL : list; 61 | } 62 | 63 | if (typeof next === 'object') { 64 | const o = {}; 65 | let curr; 66 | let atLeastOneProp = false; 67 | /* eslint-disable no-debugger,guard-for-in */ 68 | for (const i in next) { 69 | if (Object.prototype.hasOwnProperty.call(next, i)) { 70 | curr = selector(next[i]); 71 | if (curr !== INVALID_VAL) { 72 | o[i] = curr; 73 | atLeastOneProp = true; 74 | } 75 | } 76 | } 77 | /* eslint-enable no-alert,guard-for-in */ 78 | return atLeastOneProp ? o : INVALID_VAL; 79 | } 80 | 81 | return selector(next); 82 | }; 83 | 84 | const rightChangeSelector = (curr: multiPropDiff) => { 85 | if (curr._) { 86 | return applyChanges(curr._, rightChangeSelector); 87 | } 88 | return curr.status === PROPERTY_STATUS.DELETED ? curr.original : curr.current; 89 | }; 90 | 91 | const leftChangeSelector = (curr: multiPropDiff) => { 92 | if (curr._) { 93 | return applyChanges(curr._, leftChangeSelector); 94 | } 95 | return curr.status === PROPERTY_STATUS.ADDED ? curr.current : curr.original; 96 | }; 97 | 98 | const diffChangeSelectorCreator = (selector: propertySelector) => { 99 | const diffChangeSelector = (curr) => { 100 | if (curr._ && curr.changes > 0) { 101 | return applyChanges(curr._, diffChangeSelector); 102 | } 103 | return curr.status === PROPERTY_STATUS.EQUAL ? INVALID_VAL : selector(curr); 104 | }; 105 | return diffChangeSelector; 106 | }; 107 | 108 | const statusSelectorCreator = (status: string) => { 109 | const property = status === PROPERTY_STATUS.DELETED ? 'original' : 'current'; 110 | const check = status === PROPERTY_STATUS.EQUAL; 111 | const statusChangeSelector = (curr) => { 112 | if (curr._ && (check || curr.changes > 0)) { 113 | return applyChanges(curr._, statusChangeSelector); 114 | } 115 | return curr.status === status ? curr[property] : INVALID_VAL; 116 | }; 117 | return statusChangeSelector; 118 | }; 119 | 120 | const statusSelectorCreatorExtendedInformation = (status: string) => { 121 | const check = status === PROPERTY_STATUS.EQUAL; 122 | const statusChangeSelector = (curr) => { 123 | if (curr._ && (check || curr.changes > 0)) { 124 | return applyChanges(curr._, statusChangeSelector); 125 | } 126 | return curr.status === status 127 | ? { current: curr.current, original: curr.original } 128 | : INVALID_VAL; 129 | }; 130 | return statusChangeSelector; 131 | }; 132 | 133 | const getValidStatus = (status: string): string | null => { 134 | if (typeof status === 'string') { 135 | const s = status.trim().toUpperCase(); 136 | return Object.keys(PROPERTY_STATUS).find((prop) => s === prop) !== undefined 137 | ? s 138 | : null; 139 | } 140 | return null; 141 | }; 142 | 143 | const isValidPropertyDescriptor = (prop) => 144 | prop && 'original' in prop && 'current' in prop && 'status' in prop; 145 | 146 | class Differify { 147 | static DIFF_MODES = DIFF_MODES; 148 | static PROPERTY_STATUS = PROPERTY_STATUS; 149 | static multiPropDiff: multiPropDiff; 150 | static deepPropDiff: deepPropDiff; 151 | static propDiff: propDiff; 152 | private compSelector = comparatorSelector(); 153 | 154 | private config: config = null; 155 | constructor(config?: config) { 156 | this.config = new Configuration(config); 157 | this.compSelector.configure(this.config); 158 | } 159 | /** 160 | * It sets the configuration options that will be applied when compare() method is called. 161 | * @param _config 162 | */ 163 | setConfig = (_config: config) => { 164 | this.config = new Configuration(_config); 165 | this.compSelector.configure(this.config); 166 | }; 167 | 168 | /** 169 | * It returns a copy of the current configuration object. 170 | * @returns {config} 171 | */ 172 | getConfig = (): config => { 173 | return { 174 | compareArraysInOrder: this.config.compareArraysInOrder, 175 | mode: { ...this.config.mode }, 176 | }; 177 | }; 178 | 179 | /** 180 | * It returns the difference between two entities. 181 | * @param a 182 | * @param b 183 | * @returns {multiPropDiff} 184 | */ 185 | compare = (a: any, b: any): multiPropDiff => { 186 | return diff(this.compSelector, a, b); 187 | }; 188 | 189 | /** 190 | * It will apply the changes (merge both entities) and will keep the modified values 191 | * @param {multiPropDiff} diffResult | it is the Object returned by the compare() method call. 192 | * @param {boolean} diffOnly | It returns just the difference (only the !== EQUAL properties) [default: false]. 193 | * @returns {Object|Array} 194 | */ 195 | applyLeftChanges = (diffResult: multiPropDiff, diffOnly: boolean = false) => { 196 | if (diffResult && diffResult._) { 197 | return normalizeInvalidOutputFormat( 198 | diffResult._, 199 | applyChanges( 200 | diffResult._, 201 | diffOnly 202 | ? diffChangeSelectorCreator(leftChangeSelector) 203 | : leftChangeSelector 204 | ) 205 | ); 206 | } 207 | 208 | if (isValidPropertyDescriptor(diffResult)) { 209 | return diffResult.original; 210 | } 211 | 212 | return null; 213 | }; 214 | 215 | /** 216 | * It will apply the changes (merge both entities) and will keep the modified values 217 | * @param {multiPropDiff} diffResult | it is the Object returned by the compare() method call. 218 | * @param {boolean} diffOnly | It returns just the difference (only the !== EQUAL properties) 219 | * @returns {Object} 220 | */ 221 | applyRightChanges = ( 222 | diffResult: multiPropDiff, 223 | diffOnly: boolean = false 224 | ) => { 225 | if (diffResult && diffResult._) { 226 | return normalizeInvalidOutputFormat( 227 | diffResult._, 228 | applyChanges( 229 | diffResult._, 230 | diffOnly 231 | ? diffChangeSelectorCreator(rightChangeSelector) 232 | : rightChangeSelector 233 | ) 234 | ); 235 | } 236 | 237 | if (isValidPropertyDescriptor(diffResult)) { 238 | return diffResult.current; 239 | } 240 | 241 | return null; 242 | }; 243 | 244 | /** 245 | * It will return the changes that match with the specified status (second parameter). 246 | * @param {multiPropDiff} diffResult | It is the Object returned by the compare() method call. 247 | * @param {boolean} status | one of the following (ADDED || MODIFIED || DELETED || EQUAL). 248 | * @returns {Object|Array} | depending on if the input is an Object or an Array. 249 | */ 250 | filterDiffByStatus = ( 251 | diffResult: multiPropDiff, 252 | status: string = PROPERTY_STATUS.MODIFIED, 253 | extendedInformation: boolean = false 254 | ) => { 255 | const propStatus = getValidStatus(status); 256 | if (propStatus && diffResult) { 257 | if (diffResult._) { 258 | return normalizeInvalidOutputFormat( 259 | diffResult._, 260 | applyChanges( 261 | diffResult._, 262 | extendedInformation 263 | ? statusSelectorCreatorExtendedInformation(status) 264 | : statusSelectorCreator(status) 265 | ) 266 | ); 267 | } 268 | if ( 269 | isValidPropertyDescriptor(diffResult) && 270 | diffResult.status === propStatus 271 | ) { 272 | const selector = extendedInformation 273 | ? statusSelectorCreatorExtendedInformation(status) 274 | : statusSelectorCreator(status); 275 | return selector(diffResult); 276 | } 277 | } 278 | return null; 279 | }; 280 | } 281 | 282 | export default Differify; 283 | -------------------------------------------------------------------------------- /src/enums/modes.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | enum DIFF_MODES { 6 | REFERENCE = 'REFERENCE', 7 | DIFF = 'DIFF', 8 | STRING = 'STRING', 9 | } 10 | 11 | export default DIFF_MODES; 12 | -------------------------------------------------------------------------------- /src/enums/property-status.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | enum PROPERTY_STATUS { 6 | ADDED = 'ADDED', 7 | DELETED = 'DELETED', 8 | MODIFIED = 'MODIFIED', 9 | EQUAL = 'EQUAL', 10 | }; 11 | 12 | export default PROPERTY_STATUS; -------------------------------------------------------------------------------- /src/property-diff-model.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | 6 | import { deepPropDiff, propDiff } from './types/diff'; 7 | import PROPERTY_STATUS from './enums/property-status'; 8 | 9 | export function buildDiff( 10 | original: any, 11 | current: any, 12 | status: PROPERTY_STATUS, 13 | changes: number = 0 14 | ): propDiff { 15 | return { 16 | original, 17 | current, 18 | status, 19 | changes, 20 | }; 21 | } 22 | 23 | export function buildDeepDiff( 24 | data, 25 | status: PROPERTY_STATUS, 26 | changes: number = 0 27 | ): deepPropDiff { 28 | return { 29 | _: data, 30 | status, 31 | changes, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/comparators.ts: -------------------------------------------------------------------------------- 1 | import { multiPropDiff } from './diff'; 2 | import config from './config'; 3 | 4 | export type propertySelector = (prop : multiPropDiff) => any; 5 | export type multipleComparatorSelector = (a: any, b: any) => multiPropDiff; 6 | export type deepComparatorSelector = (a: any, b: any) => multiPropDiff; 7 | export type configure = (config: config) => void; 8 | 9 | export type comparator = (a: any, b: any) => multiPropDiff; 10 | 11 | export type comparatorTypes = { 12 | string: comparator | null; 13 | number: comparator | null; 14 | boolean: comparator | null; 15 | function: comparator | null; 16 | object: comparator | null; 17 | }; 18 | 19 | 20 | export type comparatorTypeMap = (type: string) => comparator; 21 | 22 | export type ComparatorMethods = { 23 | multipleComparatorSelector: multipleComparatorSelector; 24 | deepComparatorSelector: deepComparatorSelector; 25 | getComparatorByType: comparatorTypeMap; 26 | configure: configure; 27 | }; 28 | 29 | export type ComparatorSelectors = () => ComparatorMethods; 30 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | import DIFF_MODES from '../enums/modes' 2 | 3 | type modeOptions = { 4 | array?: DIFF_MODES; 5 | object?: DIFF_MODES; 6 | function?: DIFF_MODES; 7 | }; 8 | 9 | type config = { 10 | mode ?: modeOptions 11 | compareArraysInOrder?: boolean; 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/types/diff.ts: -------------------------------------------------------------------------------- 1 | import PROPERTY_STATUS from '../enums/property-status'; 2 | 3 | export type propDiff = { 4 | original: any; 5 | current: any; 6 | status: PROPERTY_STATUS; 7 | changes: number; 8 | _?: undefined; 9 | }; 10 | 11 | export type deepPropDiff = { 12 | _: { [key: string]: propDiff | deepPropDiff } | Array | null; 13 | status: PROPERTY_STATUS; 14 | changes: number; 15 | original?: undefined; 16 | current?: undefined; 17 | }; 18 | 19 | export type multiPropDiff = deepPropDiff | propDiff; 20 | -------------------------------------------------------------------------------- /src/utils/validations.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2020 Fabian Roberto Orue 3 | * BSD Licensed 4 | */ 5 | export function isArray(value) : boolean { 6 | return value && Array.isArray(value); 7 | } 8 | 9 | export function isObject(value) : boolean { 10 | return value && !Array.isArray(value) && typeof value === 'object'; 11 | } 12 | 13 | export function isValidString(val) : boolean{ 14 | return val && typeof val === 'string' && val.length > 0; 15 | } 16 | 17 | export function has(obj, prop) : boolean { 18 | return obj.hasOwnProperty 19 | ? obj.hasOwnProperty(prop) 20 | : obj[prop] !== undefined; 21 | } -------------------------------------------------------------------------------- /test-dir/index.js: -------------------------------------------------------------------------------- 1 | const Differify = require('../index'); 2 | 3 | const differify = new Differify({ 4 | compareArraysInOrder: false, 5 | mode: { object: 'DIFF', array: 'DIFF' }, 6 | }); 7 | 8 | const A = [ 9 | { 10 | id: 155, 11 | phrase: "I was deleted", 12 | }, 13 | { 14 | id: 156, 15 | phrase: "Can you help me with", 16 | }, 17 | { 18 | id: 123, 19 | phrase: "Was edite", 20 | }, 21 | { 22 | id: 157, 23 | phrase: "Help me with", 24 | }, 25 | ] 26 | 27 | const B = [ 28 | { 29 | id: 156, 30 | phrase: "Can you help me with", 31 | }, 32 | { 33 | id: 123, 34 | phrase: "Was edited", 35 | }, 36 | { 37 | id: 88, 38 | phrase: "Was added in between", 39 | }, 40 | { 41 | id: 157, 42 | phrase: "Help me with", 43 | }, 44 | ] 45 | 46 | 47 | 48 | const diff = differify.compare(A, B); 49 | console.log(diff, JSON.stringify(differify.filterDiffByStatus(diff, 'ADDED'))); 50 | // console.log(differify.applyRightChanges(diff)); 51 | -------------------------------------------------------------------------------- /test/comparators.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JSONStringComparator, 3 | arraySimpleComparator, 4 | dateComparator, 5 | getConfiguredDeepObjectComparator, 6 | getConfiguredOrderedDeepArrayComparator, 7 | getConfiguredUnorderedDeepArrayComparator, 8 | toStringComparator, 9 | valueRefEqualityComparator, 10 | } from '../src/comparators'; 11 | import comparatorSelector from '../src/comparator-selector'; 12 | 13 | let A = {}; 14 | let B = {}; 15 | 16 | const compSelector = comparatorSelector(); 17 | 18 | beforeEach(() => { 19 | A = { 20 | name: 'Fabian', 21 | age: 18, 22 | nested: { 23 | id: 1, 24 | roles: ['admin', 'user'], 25 | }, 26 | hobbies: [ 27 | { points: 10, desc: 'football soccer' }, 28 | { points: 9, desc: 'programming' }, 29 | ], 30 | }; 31 | 32 | B = { 33 | name: 'Judith', 34 | age: 18, 35 | nested: { 36 | id: 2, 37 | roles: ['user'], 38 | }, 39 | hobbies: [ 40 | { points: 10, desc: 'dance' }, 41 | { points: 9, desc: 'programming' }, 42 | ], 43 | }; 44 | }); 45 | 46 | describe('Testing each comparator separately', () => { 47 | test('JSONStringComparator: should return JSON String comparison', () => { 48 | let res = JSONStringComparator(A, B); 49 | 50 | expect(res).not.toBe(null); 51 | expect(res.status).toEqual('MODIFIED'); 52 | expect(res.changes).toEqual(1); 53 | expect(res.original).toEqual(A); 54 | expect(res.current).toEqual(B); 55 | 56 | res = JSONStringComparator(A, A); 57 | 58 | expect(res).not.toBe(null); 59 | expect(res.status).toEqual('EQUAL'); 60 | expect(res.changes).toEqual(0); 61 | expect(res.original).toEqual(A); 62 | expect(res.current).toEqual(A); 63 | }); 64 | test('toStringComparator: should return String comparison', () => { 65 | // returns equals because it checks the prototype.toString() of each object 66 | // and both are objects [object Object] 67 | let res = toStringComparator(A, B); 68 | 69 | expect(res).not.toBe(null); 70 | expect(res.status).toEqual('EQUAL'); 71 | expect(res.changes).toEqual(0); 72 | expect(res.original).toEqual(A); 73 | expect(res.current).toEqual(B); 74 | 75 | res = toStringComparator(A, A); 76 | 77 | expect(res).not.toBe(null); 78 | expect(res.status).toEqual('EQUAL'); 79 | expect(res.changes).toEqual(0); 80 | expect(res.original).toEqual(A); 81 | expect(res.current).toEqual(A); 82 | 83 | const C = function C() {}; 84 | res = toStringComparator(A, C); 85 | 86 | expect(res).not.toBe(null); 87 | expect(res.status).toEqual('MODIFIED'); 88 | expect(res.changes).toEqual(1); 89 | expect(res.original).toEqual(A); 90 | expect(res.current).toEqual(C); 91 | }); 92 | 93 | test('arraySimpleComparator: should return JSON String comparison between arrays', () => { 94 | let res = arraySimpleComparator([A], [B]); 95 | 96 | expect(res).not.toBe(null); 97 | expect(res.status).toEqual('MODIFIED'); 98 | expect(res.changes).toEqual(1); 99 | expect(res.original).toEqual([A]); 100 | expect(res.current).toEqual([B]); 101 | 102 | res = arraySimpleComparator([A], [A]); 103 | 104 | expect(res).not.toBe(null); 105 | expect(res.status).toEqual('EQUAL'); 106 | expect(res.changes).toEqual(0); 107 | expect(res.original).toEqual([A]); 108 | expect(res.current).toEqual([A]); 109 | }); 110 | 111 | test('dateComparator: should return JSON String comparison between arrays', () => { 112 | const dateA = new Date(); 113 | const dateB = new Date(); 114 | dateB.setDate(dateA.getDate() + 1); 115 | 116 | let res = dateComparator(dateA, dateB); 117 | 118 | expect(res).not.toBe(null); 119 | expect(res.status).toEqual('MODIFIED'); 120 | expect(res.changes).toEqual(1); 121 | expect(res.original).toEqual(dateA); 122 | expect(res.current).toEqual(dateB); 123 | 124 | dateB.setDate(dateA.getDate()); 125 | 126 | res = dateComparator(dateA, dateB); 127 | 128 | expect(res).not.toBe(null); 129 | expect(res.status).toEqual('EQUAL'); 130 | expect(res.changes).toEqual(0); 131 | expect(res.original).toEqual(dateA); 132 | expect(res.current).toEqual(dateB); 133 | }); 134 | 135 | test('valueRefEqualityComparator: should return reference or value comparison', () => { 136 | let res = valueRefEqualityComparator(A, B); 137 | 138 | expect(res).not.toBe(null); 139 | expect(res.status).toEqual('MODIFIED'); 140 | expect(res.changes).toEqual(1); 141 | expect(res.original).toEqual(A); 142 | expect(res.current).toEqual(B); 143 | 144 | res = valueRefEqualityComparator(A, A); 145 | 146 | expect(res).not.toBe(null); 147 | expect(res.status).toEqual('EQUAL'); 148 | expect(res.changes).toEqual(0); 149 | expect(res.original).toEqual(A); 150 | expect(res.current).toEqual(A); 151 | 152 | res = valueRefEqualityComparator(B, B); 153 | 154 | expect(res).not.toBe(null); 155 | expect(res.status).toEqual('EQUAL'); 156 | expect(res.changes).toEqual(0); 157 | expect(res.original).toEqual(B); 158 | expect(res.current).toEqual(B); 159 | 160 | res = valueRefEqualityComparator(1, 2); 161 | 162 | expect(res).not.toBe(null); 163 | expect(res.status).toEqual('MODIFIED'); 164 | expect(res.changes).toEqual(1); 165 | expect(res.original).toEqual(1); 166 | expect(res.current).toEqual(2); 167 | 168 | res = valueRefEqualityComparator(1, 1); 169 | 170 | expect(res).not.toBe(null); 171 | expect(res.status).toEqual('EQUAL'); 172 | expect(res.changes).toEqual(0); 173 | expect(res.original).toEqual(1); 174 | expect(res.current).toEqual(1); 175 | 176 | res = valueRefEqualityComparator([], []); 177 | 178 | expect(res).not.toBe(null); 179 | expect(res.status).toEqual('MODIFIED'); 180 | expect(res.changes).toEqual(1); 181 | expect(res.original).toEqual([]); 182 | expect(res.current).toEqual([]); 183 | 184 | var sameArr = []; 185 | res = valueRefEqualityComparator(sameArr, sameArr); 186 | 187 | expect(res).not.toBe(null); 188 | expect(res.status).toEqual('EQUAL'); 189 | expect(res.changes).toEqual(0); 190 | expect(res.original).toEqual(sameArr); 191 | expect(res.current).toEqual(sameArr); 192 | }); 193 | 194 | test('deepObjectComparator: should return a deep object comparison', () => { 195 | compSelector.configure({ 196 | compareArraysInOrder: true, //default value 197 | mode: { object: 'DIFF', array: 'DIFF' }, 198 | }); 199 | 200 | const deepObjectComparator = getConfiguredDeepObjectComparator( 201 | compSelector.multipleComparatorSelector 202 | ); 203 | 204 | const res = deepObjectComparator(A, B); 205 | 206 | expect(res).not.toBe(null); 207 | expect(res.status).toEqual('MODIFIED'); 208 | expect(res.changes).toEqual(5); 209 | expect(res._).not.toBe(null); 210 | expect(res._.age).not.toBe(null); 211 | expect(res._.age.original).toBe(A.age); 212 | expect(res._.age.current).toBe(B.age); 213 | expect(res._.name).not.toBe(null); 214 | expect(res._.name.original).toBe(A.name); 215 | expect(res._.name.current).toBe(B.name); 216 | expect(res._.nested).not.toBe(null); 217 | expect(res._.nested._).not.toBe(null); 218 | expect(res._.nested._.id.original).toBe(A.nested.id); 219 | expect(res._.nested._.id.current).toBe(B.nested.id); 220 | //Object 221 | expect(res._.nested._.roles._).not.toBe(null); 222 | expect( 223 | Object.prototype.toString.call(res._.nested._.roles._) === 224 | '[object Array]' 225 | ).toBe(true); 226 | expect(res._.nested._.roles._.length).toBe(2); 227 | expect(res._.nested._.roles._[0].status).toBe('MODIFIED'); 228 | expect(res._.nested._.roles._[0].changes).toBe(1); 229 | expect(res._.nested._.roles._[0].original).toBe(A.nested.roles[0]); 230 | expect(res._.nested._.roles._[0].current).toBe(B.nested.roles[0]); 231 | 232 | expect(res._.nested._.roles._[1].status).toBe('DELETED'); 233 | expect(res._.nested._.roles._[1].changes).toBe(1); 234 | expect(res._.nested._.roles._[1].original).toBe(A.nested.roles[1]); 235 | expect(res._.nested._.roles._[1].current).toBe(null); 236 | 237 | //Array 238 | expect(res._.hobbies._).not.toBe(null); 239 | expect( 240 | Object.prototype.toString.call(res._.hobbies._) === '[object Array]' 241 | ).toBe(true); 242 | expect(res._.hobbies._.length).toBe(2); 243 | expect(res._.hobbies._[0].status).toBe('MODIFIED'); 244 | expect(res._.hobbies._[0].changes).toBe(1); 245 | expect(res._.hobbies._[0]._.points.original).toBe(A.hobbies[0].points); 246 | expect(res._.hobbies._[0]._.points.current).toBe(B.hobbies[0].points); 247 | expect(res._.hobbies._[0]._.points.status).toBe('EQUAL'); 248 | expect(res._.hobbies._[0]._.points.changes).toBe(0); 249 | expect(res._.hobbies._[0]._.desc.original).toBe(A.hobbies[0].desc); 250 | expect(res._.hobbies._[0]._.desc.current).toBe(B.hobbies[0].desc); 251 | expect(res._.hobbies._[0]._.desc.status).toBe('MODIFIED'); 252 | expect(res._.hobbies._[0]._.desc.changes).toBe(1); 253 | expect(res._.hobbies._[1].status).toBe('EQUAL'); 254 | expect(res._.hobbies._[1].changes).toBe(0); 255 | expect(res._.hobbies._[1]._.points.original).toBe(A.hobbies[1].points); 256 | expect(res._.hobbies._[1]._.points.current).toBe(B.hobbies[1].points); 257 | expect(res._.hobbies._[1]._.points.status).toBe('EQUAL'); 258 | expect(res._.hobbies._[1]._.points.changes).toBe(0); 259 | expect(res._.hobbies._[1]._.desc.original).toBe(A.hobbies[1].desc); 260 | expect(res._.hobbies._[1]._.desc.current).toBe(B.hobbies[1].desc); 261 | expect(res._.hobbies._[1]._.desc.status).toBe('EQUAL'); 262 | expect(res._.hobbies._[1]._.desc.changes).toBe(0); 263 | }); 264 | 265 | test('getConfiguredOrderedDeepArrayComparator: should return a deep array ORDERED comparison', () => { 266 | compSelector.configure({ 267 | compareArraysInOrder: true, //default value 268 | mode: { object: 'DIFF', array: 'DIFF' }, 269 | }); 270 | 271 | const orderedDeepArrayComparator = getConfiguredOrderedDeepArrayComparator( 272 | compSelector.multipleComparatorSelector 273 | ); 274 | 275 | const res = orderedDeepArrayComparator(A.hobbies, B.hobbies); 276 | 277 | expect(res._).not.toBe(null); 278 | expect(Object.prototype.toString.call(res._) === '[object Array]').toBe( 279 | true 280 | ); 281 | expect(res._.length).toBe(2); 282 | expect(res._[0].status).toBe('MODIFIED'); 283 | expect(res._[0].changes).toBe(1); 284 | expect(res._[0]._.points.original).toBe(A.hobbies[0].points); 285 | expect(res._[0]._.points.current).toBe(B.hobbies[0].points); 286 | expect(res._[0]._.points.status).toBe('EQUAL'); 287 | expect(res._[0]._.points.changes).toBe(0); 288 | expect(res._[0]._.desc.original).toBe(A.hobbies[0].desc); 289 | expect(res._[0]._.desc.current).toBe(B.hobbies[0].desc); 290 | expect(res._[0]._.desc.status).toBe('MODIFIED'); 291 | expect(res._[0]._.desc.changes).toBe(1); 292 | expect(res._[1].status).toBe('EQUAL'); 293 | expect(res._[1].changes).toBe(0); 294 | expect(res._[1]._.points.original).toBe(A.hobbies[1].points); 295 | expect(res._[1]._.points.current).toBe(B.hobbies[1].points); 296 | expect(res._[1]._.points.status).toBe('EQUAL'); 297 | expect(res._[1]._.points.changes).toBe(0); 298 | expect(res._[1]._.desc.original).toBe(A.hobbies[1].desc); 299 | expect(res._[1]._.desc.current).toBe(B.hobbies[1].desc); 300 | expect(res._[1]._.desc.status).toBe('EQUAL'); 301 | expect(res._[1]._.desc.changes).toBe(0); 302 | }); 303 | test('getConfiguredUnorderedDeepArrayComparator: should return a deep array UNORDERED comparison', () => { 304 | // for unordered array comparison, is not possible to go deeper 305 | // when array elements are objects, because there is no way 306 | // to know wich object in the A array is related to another object 307 | // in the B array. TODO: make a relateElementsBy? to match elements? 308 | compSelector.configure({ 309 | compareArraysInOrder: false, //default value 310 | mode: { object: 'DIFF', array: 'DIFF' }, 311 | }); 312 | 313 | const unorderedDeepArrayComparator = getConfiguredUnorderedDeepArrayComparator( 314 | compSelector.multipleComparatorSelector 315 | ); 316 | 317 | let res = unorderedDeepArrayComparator(A.hobbies, B.hobbies); 318 | 319 | expect(res._).not.toBe(null); 320 | expect(Object.prototype.toString.call(res._) === '[object Array]').toBe( 321 | true 322 | ); 323 | 324 | expect(res._.length).toBe(2); 325 | expect(res.status).toBe('MODIFIED'); 326 | 327 | expect(res._[0].changes).toBe(0); 328 | 329 | expect(res._[0]._.points.original).toBe(A.hobbies[1].points); 330 | expect(res._[0]._.points.current).toBe(B.hobbies[1].points); 331 | expect(res._[0]._.points.changes).toBe(0); 332 | expect(res._[0]._.points.status).toBe('EQUAL'); 333 | expect(res._[0]._.desc.original).toBe(A.hobbies[1].desc); 334 | expect(res._[0]._.desc.current).toBe(B.hobbies[1].desc); 335 | expect(res._[0]._.desc.changes).toBe(0); 336 | expect(res._[0]._.desc.status).toBe('EQUAL'); 337 | 338 | expect(res._[1]._.points.original).toBe(A.hobbies[0].points); 339 | expect(res._[1]._.points.current).toBe(B.hobbies[0].points); 340 | expect(res._[1]._.points.changes).toBe(0); 341 | expect(res._[1]._.points.status).toBe('EQUAL'); 342 | expect(res._[1]._.desc.original).toBe(A.hobbies[0].desc); 343 | expect(res._[1]._.desc.current).toBe(B.hobbies[0].desc); 344 | expect(res._[1]._.desc.changes).toBe(1); 345 | expect(res._[1]._.desc.status).toBe('MODIFIED'); 346 | }); 347 | }); 348 | -------------------------------------------------------------------------------- /test/differify.benchmark.js: -------------------------------------------------------------------------------- 1 | 2 | const Bencharmk = require('benchmark'); 3 | const Differify = require('../index'); 4 | 5 | const A = [ 6 | { 7 | id: 1, 8 | name: 'Leanne Graham', 9 | username: 'Bret', 10 | email: 'Sincere@april.biz', 11 | address: { 12 | street: 'Kulas Light', 13 | suite: 'Apt. 556', 14 | city: 'Gwenborough', 15 | zipcode: '92998-3874', 16 | geo: { 17 | lat: '-37.3159', 18 | lng: '81.1496', 19 | }, 20 | }, 21 | phone: '1-770-736-8031 x56442', 22 | website: 'hildegard.org', 23 | company: { 24 | name: 'Romaguera-Crona', 25 | catchPhrase: 'Multi-layered client-server neural-net', 26 | bs: 'harness real-time e-markets', 27 | }, 28 | }, 29 | { 30 | id: 2, 31 | name: 'Ervin Howell', 32 | username: 'Antonette', 33 | email: 'Shanna@melissa.tv', 34 | address: { 35 | street: 'Victor Plains', 36 | suite: 'Suite 879', 37 | city: 'Wisokyburgh', 38 | zipcode: '90566-7771', 39 | geo: { 40 | lat: '-43.9509', 41 | lng: '-34.4618', 42 | }, 43 | }, 44 | phone: '010-692-6593 x09125', 45 | website: 'anastasia.net', 46 | company: { 47 | name: 'Deckow-Crist', 48 | catchPhrase: 'Proactive didactic contingency', 49 | bs: 'synergize scalable supply-chains', 50 | }, 51 | }, 52 | { 53 | id: 3, 54 | name: 'Clementine Bauch', 55 | username: 'Samantha', 56 | email: 'Nathan@yesenia.net', 57 | address: { 58 | street: 'Douglas Extension', 59 | suite: 'Suite 847', 60 | city: 'McKenziehaven', 61 | zipcode: '59590-4157', 62 | geo: { 63 | lat: '-68.6102', 64 | lng: '-47.0653', 65 | }, 66 | }, 67 | phone: '1-463-123-4447', 68 | website: 'ramiro.info', 69 | company: { 70 | name: 'Romaguera-Jacobson', 71 | catchPhrase: 'Face to face bifurcated interface', 72 | bs: 'e-enable strategic applications', 73 | }, 74 | }, 75 | { 76 | id: 4, 77 | name: 'Patricia Lebsack', 78 | username: 'Karianne', 79 | email: 'Julianne.OConner@kory.org', 80 | address: { 81 | street: 'Hoeger Mall', 82 | suite: 'Apt. 692', 83 | city: 'South Elvis', 84 | zipcode: '53919-4257', 85 | geo: { 86 | lat: '29.4572', 87 | lng: '-164.2990', 88 | }, 89 | }, 90 | phone: '493-170-9623 x156', 91 | website: 'kale.biz', 92 | company: { 93 | name: 'Robel-Corkery', 94 | catchPhrase: 'Multi-tiered zero tolerance productivity', 95 | bs: 'transition cutting-edge web services', 96 | }, 97 | }, 98 | ]; 99 | 100 | const B = [ 101 | { 102 | id: 5, 103 | name: 'Chelsey Dietrich', 104 | username: 'Kamren', 105 | email: 'Lucio_Hettinger@annie.ca', 106 | address: { 107 | street: 'Skiles Walks', 108 | suite: 'Suite 351', 109 | city: 'Roscoeview', 110 | zipcode: '33263', 111 | geo: { 112 | lat: '-31.8129', 113 | lng: '62.5342', 114 | }, 115 | }, 116 | phone: '(254)954-1289', 117 | website: 'demarco.info', 118 | company: { 119 | name: 'Keebler LLC', 120 | catchPhrase: 'User-centric fault-tolerant solution', 121 | bs: 'revolutionize end-to-end systems', 122 | }, 123 | }, 124 | { 125 | id: 6, 126 | name: 'Mrs. Dennis Schulist', 127 | username: 'Leopoldo_Corkery', 128 | email: 'Karley_Dach@jasper.info', 129 | address: { 130 | street: 'Norberto Crossing', 131 | suite: 'Apt. 950', 132 | city: 'South Christy', 133 | zipcode: '23505-1337', 134 | geo: { 135 | lat: '-71.4197', 136 | lng: '71.7478', 137 | }, 138 | }, 139 | phone: '1-477-935-8478 x6430', 140 | website: 'ola.org', 141 | company: { 142 | name: 'Considine-Lockman', 143 | catchPhrase: 'Synchronised bottom-line interface', 144 | bs: 'e-enable innovative applications', 145 | }, 146 | }, 147 | { 148 | id: 7, 149 | name: 'Kurtis Weissnat', 150 | username: 'Elwyn.Skiles', 151 | email: 'Telly.Hoeger@billy.biz', 152 | address: { 153 | street: 'Rex Trail', 154 | suite: 'Suite 280', 155 | city: 'Howemouth', 156 | zipcode: '58804-1099', 157 | geo: { 158 | lat: '24.8918', 159 | lng: '21.8984', 160 | }, 161 | }, 162 | phone: '210.067.6132', 163 | website: 'elvis.io', 164 | company: { 165 | name: 'Johns Group', 166 | catchPhrase: 'Configurable multimedia task-force', 167 | bs: 'generate enterprise e-tailers', 168 | }, 169 | }, 170 | { 171 | id: 8, 172 | name: 'Nicholas Runolfsdottir V', 173 | username: 'Maxime_Nienow', 174 | email: 'Sherwood@rosamond.me', 175 | address: { 176 | street: 'Ellsworth Summit', 177 | suite: 'Suite 729', 178 | city: 'Aliyaview', 179 | zipcode: '45169', 180 | geo: { 181 | lat: '-14.3990', 182 | lng: '-120.7677', 183 | }, 184 | }, 185 | phone: '586.493.6943 x140', 186 | website: 'jacynthe.com', 187 | company: { 188 | name: 'Abernathy Group', 189 | catchPhrase: 'Implemented secondary concept', 190 | bs: 'e-enable extensible e-tailers', 191 | }, 192 | }, 193 | { 194 | id: 9, 195 | name: 'Glenna Reichert', 196 | username: 'Delphine', 197 | email: 'Chaim_McDermott@dana.io', 198 | address: { 199 | street: 'Dayna Park', 200 | suite: 'Suite 449', 201 | city: 'Bartholomebury', 202 | zipcode: '76495-3109', 203 | geo: { 204 | lat: '24.6463', 205 | lng: '-168.8889', 206 | }, 207 | }, 208 | phone: '(775)976-6794 x41206', 209 | website: 'conrad.com', 210 | company: { 211 | name: 'Yost and Sons', 212 | catchPhrase: 'Switchable contextually-based project', 213 | bs: 'aggregate real-time technologies', 214 | }, 215 | }, 216 | { 217 | id: 10, 218 | name: 'Clementina DuBuque', 219 | username: 'Moriah.Stanton', 220 | email: 'Rey.Padberg@karina.biz', 221 | address: { 222 | street: 'Kattie Turnpike', 223 | suite: 'Suite 198', 224 | city: 'Lebsackbury', 225 | zipcode: '31428-2261', 226 | geo: { 227 | lat: '-38.2386', 228 | lng: '57.2232', 229 | }, 230 | }, 231 | phone: '024-648-3804', 232 | website: 'ambrose.net', 233 | company: { 234 | name: 'Hoeger LLC', 235 | catchPhrase: 'Centralized empowering task-force', 236 | bs: 'target end-to-end models', 237 | }, 238 | }, 239 | ]; 240 | 241 | const AO = { 242 | id: 1, 243 | name: 'Leanne Graham', 244 | username: 'Bret', 245 | email: 'Sincere@april.biz', 246 | address: { 247 | street: 'Kulas Light', 248 | suite: 'Apt. 556', 249 | city: 'Gwenborough', 250 | zipcode: '92998-3874', 251 | geo: { 252 | lat: '-37.3159', 253 | lng: '81.1496', 254 | }, 255 | }, 256 | phone: '1-770-736-8031 x56442', 257 | website: 'hildegard.org', 258 | company: { 259 | name: 'Romaguera-Crona', 260 | catchPhrase: 'Multi-layered client-server neural-net', 261 | bs: 'harness real-time e-markets', 262 | }, 263 | }; 264 | 265 | const BO = { 266 | id: 5, 267 | name: 'Chelsey Dietrich', 268 | username: 'Kamren', 269 | email: 'Lucio_Hettinger@annie.ca', 270 | address: { 271 | street: 'Skiles Walks', 272 | suite: 'Suite 351', 273 | city: 'Roscoeview', 274 | zipcode: '33263', 275 | geo: { 276 | lat: '-31.8129', 277 | lng: '62.5342', 278 | }, 279 | }, 280 | phone: '(254)954-1289', 281 | website: 'demarco.info', 282 | company: { 283 | name: 'Keebler LLC', 284 | catchPhrase: 'User-centric fault-tolerant solution', 285 | bs: 'revolutionize end-to-end systems', 286 | }, 287 | }; 288 | 289 | const differify = new Differify(); 290 | differify.setConfig({ mode: { object: 'DIFF', array: 'DIFF' } }); 291 | 292 | const differifyUnordered = new Differify(); 293 | differify.setConfig({ scan: { 294 | keepArrayOrder: false 295 | }, mode: { object: 'DIFF', array: 'DIFF' } }); 296 | 297 | const a = { 298 | name: 'Person1', 299 | extras: { 300 | something: '1', 301 | somethingElse: '2', 302 | }, 303 | member: true, 304 | doc: 10, 305 | }; 306 | const b = { 307 | name: 'Person1', 308 | extras: { 309 | something: '1', 310 | somethingElse: '2', 311 | }, 312 | member: false, 313 | badges: 7, 314 | }; 315 | 316 | 317 | const suite = new Bencharmk.Suite(); 318 | const diffTest = differify.compare(AO, BO); 319 | 320 | 321 | 322 | suite 323 | .add('Differify Complex object diff', () => { 324 | const diff = differify.compare(AO, BO); 325 | }) 326 | .add('Large array of objects', () => { 327 | const diff = differify.compare(A, B); 328 | }) 329 | .add('Large array unordered of objects', () => { 330 | const diff = differifyUnordered.compare(A, B); 331 | }) 332 | .add('Filter by status', () => { 333 | const diff = differify.filterDiffByStatus(diffTest, 'MODIFIED'); 334 | }) 335 | .add('Apply changes left', () => { 336 | const diff = differify.applyLeftChanges(diffTest); 337 | }) 338 | .add('Apply changes right', () => { 339 | const diff = differify.applyRightChanges(diffTest); 340 | }) 341 | // add listeners 342 | .on('cycle', function (event) { 343 | console.log(String(event.target)); 344 | }) 345 | .on('complete', function () { 346 | console.log('Fastest is ' + this.filter('fastest').map('name')); 347 | }) 348 | .run(); 349 | -------------------------------------------------------------------------------- /test/differify.test.ts: -------------------------------------------------------------------------------- 1 | import Differify from '../src/differify'; 2 | import PROPERTY_STATUS from '../src/enums/property-status'; 3 | import DIFF_MODES from '../src/enums/modes'; 4 | const differify = new Differify(); 5 | 6 | describe('Testing differify lib: ', () => { 7 | const getAObject = () => ({ 8 | name: 'Judith', 9 | age: 33, 10 | friends: ['Cecilia', 'Stephanie'], 11 | extras: { 12 | hobbies: ['Gym', 'Dance'], 13 | }, 14 | date: new Date(), 15 | }); 16 | 17 | const getBObject = () => ({ 18 | name: 'Fabian', 19 | age: 36, 20 | friends: ['Finn', 'Jake'], 21 | extras: { 22 | hobbies: ['Football Soccer', 'Programming'], 23 | }, 24 | date: new Date('12/15/1983 12:00:00'), 25 | }); 26 | 27 | test('testing bad config arguments', () => { 28 | differify.setConfig({ 29 | compareArraysInOrder: null, 30 | mode: { array: (null as any), object: (true as any) }, 31 | }); 32 | let config = differify.getConfig(); 33 | expect(typeof config.compareArraysInOrder).toEqual('boolean'); 34 | expect(config.compareArraysInOrder).toBeTruthy(); 35 | expect(config.mode.array).toEqual(DIFF_MODES.DIFF); 36 | expect(config.mode.object).toEqual(DIFF_MODES.DIFF); 37 | expect(config.mode.function).toEqual('REFERENCE'); 38 | }); 39 | 40 | test('testing case insensitive config arguments', () => { 41 | differify.setConfig({ 42 | mode: { array: DIFF_MODES.DIFF, object: DIFF_MODES.STRING, function: DIFF_MODES.STRING }, 43 | }); 44 | const config = differify.getConfig(); 45 | expect(config.mode.array).toEqual(DIFF_MODES.DIFF); 46 | expect(config.mode.object).toEqual('STRING'); 47 | expect(config.mode.function).toEqual('STRING'); 48 | }); 49 | 50 | test('testing empty config', () => { 51 | differify.setConfig(); 52 | let config = differify.getConfig(); 53 | expect(typeof config.compareArraysInOrder).toEqual('boolean'); 54 | expect(config.compareArraysInOrder).toBeTruthy(); 55 | expect(config.mode.array).toEqual(DIFF_MODES.DIFF); 56 | expect(config.mode.object).toEqual(DIFF_MODES.DIFF); 57 | expect(config.mode.function).toEqual('REFERENCE'); 58 | }); 59 | 60 | test('testing multiple instance config', () => { 61 | const diff = new Differify({ 62 | mode: { 63 | object: DIFF_MODES.DIFF, 64 | array: DIFF_MODES.DIFF, 65 | }, 66 | }); 67 | 68 | const diff2 = new Differify({ 69 | compareArraysInOrder: false, 70 | mode: { 71 | object: DIFF_MODES.DIFF, 72 | array: DIFF_MODES.STRING, 73 | }, 74 | }); 75 | 76 | expect(JSON.stringify(diff.getConfig())).toBe( 77 | JSON.stringify({ 78 | compareArraysInOrder: true, 79 | mode: { array: DIFF_MODES.DIFF, object: DIFF_MODES.DIFF, function: 'REFERENCE' }, 80 | }) 81 | ); 82 | expect(JSON.stringify(diff2.getConfig())).toBe( 83 | JSON.stringify({ 84 | compareArraysInOrder: false, 85 | mode: { array: DIFF_MODES.STRING, object: DIFF_MODES.DIFF, function: 'REFERENCE' }, 86 | }) 87 | ); 88 | }); 89 | 90 | test('testing incomplete config', () => { 91 | differify.setConfig({ 92 | mode: {}, 93 | }); 94 | let config = differify.getConfig(); 95 | expect(typeof config.compareArraysInOrder).toEqual('boolean'); 96 | expect(config.compareArraysInOrder).toBeTruthy(); 97 | expect(config.mode.array).toEqual(DIFF_MODES.DIFF); 98 | expect(config.mode.object).toEqual(DIFF_MODES.DIFF); 99 | expect(config.mode.function).toEqual('REFERENCE'); 100 | }); 101 | 102 | test('testing good config', () => { 103 | differify.setConfig({ 104 | compareArraysInOrder: false, 105 | mode: { 106 | array: DIFF_MODES.DIFF, 107 | object: DIFF_MODES.DIFF, 108 | function: DIFF_MODES.STRING, 109 | }, 110 | }); 111 | let config = differify.getConfig(); 112 | expect(typeof config.compareArraysInOrder).toEqual('boolean'); 113 | expect(config.compareArraysInOrder).toBeFalsy(); 114 | expect(config.mode.array).toEqual(DIFF_MODES.DIFF); 115 | expect(config.mode.object).toEqual(DIFF_MODES.DIFF); 116 | expect(config.mode.function).toEqual('STRING'); 117 | }); 118 | 119 | test('if no property match, should return null', () => { 120 | differify.setConfig({ 121 | mode: { 122 | array: DIFF_MODES.DIFF, 123 | object: DIFF_MODES.DIFF, 124 | function: DIFF_MODES.STRING, 125 | }, 126 | }); 127 | const diff = differify.compare(Object.create(null), getAObject()); 128 | 129 | expect(diff._.name.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 130 | expect(diff._.name.original).toBe(null); 131 | expect(diff._.name.current).toBe('Judith'); 132 | expect(diff._.date.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 133 | expect(diff._.date.original).toBe(null); 134 | expect(Object.prototype.toString.call(diff._.date.current)).toBe( 135 | '[object Date]' 136 | ); 137 | expect(diff._.age.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 138 | expect(diff._.age.original).toBe(null); 139 | expect(diff._.age.current).toBe(33); 140 | expect(diff._.friends.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 141 | expect(diff._.friends.original).toBe(null); 142 | expect(Object.prototype.toString.call(diff._.friends.current)).toBe( 143 | '[object Array]' 144 | ); 145 | }); 146 | 147 | test('empty objects, should return EQUAL', () => { 148 | differify.setConfig({ 149 | mode: { 150 | array: DIFF_MODES.DIFF, 151 | object: DIFF_MODES.DIFF, 152 | function: DIFF_MODES.STRING, 153 | }, 154 | }); 155 | const diff = differify.compare({}, {}); 156 | 157 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 158 | expect(diff.changes === 0).toBeTruthy(); 159 | expect(diff._ === null).toBeTruthy(); 160 | }); 161 | 162 | test('empty array, should return an empty array', () => { 163 | differify.setConfig({ 164 | mode: { 165 | array: DIFF_MODES.DIFF, 166 | object: DIFF_MODES.DIFF, 167 | function: DIFF_MODES.STRING, 168 | }, 169 | }); 170 | const diff = differify.compare([], []); 171 | 172 | expect( 173 | Object.prototype.toString.call(diff._) === '[object Array]' 174 | ).toBeTruthy(); 175 | expect(diff._.length).toBe(0); 176 | expect(diff.status).toBe(PROPERTY_STATUS.EQUAL); 177 | expect(diff.changes).toBe(0); 178 | }); 179 | 180 | test('diff with no prototyped object', () => { 181 | differify.setConfig({ 182 | mode: { 183 | array: DIFF_MODES.DIFF, 184 | object: DIFF_MODES.DIFF, 185 | function: DIFF_MODES.STRING, 186 | }, 187 | }); 188 | 189 | let diff = differify.compare(Object.create(null), getAObject()); 190 | 191 | expect(diff._.name.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 192 | expect(diff._.name.original).toBe(null); 193 | expect(diff._.name.current).toBe('Judith'); 194 | expect(diff._.date.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 195 | expect(diff._.date.original).toBe(null); 196 | expect(Object.prototype.toString.call(diff._.date.current)).toBe( 197 | '[object Date]' 198 | ); 199 | expect(diff._.age.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 200 | expect(diff._.age.original).toBe(null); 201 | expect(diff._.age.current).toBe(33); 202 | expect(diff._.friends.status === PROPERTY_STATUS.ADDED).toBeTruthy(); 203 | expect(diff._.friends.original).toBe(null); 204 | expect(Object.prototype.toString.call(diff._.friends.current)).toBe( 205 | '[object Array]' 206 | ); 207 | 208 | diff = differify.compare(getAObject(), Object.create(null)); 209 | 210 | expect(diff._.name.status === 'DELETED').toBeTruthy(); 211 | expect(diff._.name.current).toBe(null); 212 | expect(diff._.name.original).toBe('Judith'); 213 | expect(diff._.date.status === 'DELETED').toBeTruthy(); 214 | expect(diff._.date.current).toBe(null); 215 | expect(Object.prototype.toString.call(diff._.date.original)).toBe( 216 | '[object Date]' 217 | ); 218 | expect(diff._.age.status === 'DELETED').toBeTruthy(); 219 | expect(diff._.age.current).toBe(null); 220 | expect(diff._.age.original).toBe(33); 221 | expect(diff._.friends.status === 'DELETED').toBeTruthy(); 222 | expect(diff._.friends.current).toBe(null); 223 | expect(Object.prototype.toString.call(diff._.friends.original)).toBe( 224 | '[object Array]' 225 | ); 226 | }); 227 | 228 | test('checking Date diff', () => { 229 | differify.setConfig({ 230 | mode: { 231 | array: DIFF_MODES.DIFF, 232 | object: DIFF_MODES.DIFF, 233 | function: DIFF_MODES.STRING, 234 | }, 235 | }); 236 | let a = new Date(); 237 | let b = new Date(1983, 11, 15); 238 | 239 | let diff = differify.compare(a, b); 240 | 241 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 242 | 243 | const newDate = new Date(); 244 | a = newDate; 245 | b = newDate; 246 | diff = differify.compare(a, b); 247 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 248 | }); 249 | 250 | test('checking Native values diff', () => { 251 | differify.setConfig({ 252 | mode: { 253 | array: DIFF_MODES.DIFF, 254 | object: DIFF_MODES.DIFF, 255 | function: DIFF_MODES.STRING, 256 | }, 257 | }); 258 | 259 | // NATIVE DIFF 260 | expect( 261 | differify.compare(1, 2).status === PROPERTY_STATUS.MODIFIED 262 | ).toBeTruthy(); 263 | expect( 264 | differify.compare(true, false).status === PROPERTY_STATUS.MODIFIED 265 | ).toBeTruthy(); 266 | expect( 267 | differify.compare(null, null).status === PROPERTY_STATUS.EQUAL 268 | ).toBeTruthy(); 269 | expect( 270 | differify.compare(undefined, undefined).status === PROPERTY_STATUS.EQUAL 271 | ).toBeTruthy(); 272 | expect( 273 | differify.compare('a', 'b').status === PROPERTY_STATUS.MODIFIED 274 | ).toBeTruthy(); 275 | 276 | const newDate = new Date(); 277 | const a = newDate; 278 | const b = newDate; 279 | const diff = differify.compare(a, b); 280 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 281 | }); 282 | 283 | test('should return the diff between two entities with different typeof result', () => { 284 | differify.setConfig({ 285 | mode: { 286 | array: DIFF_MODES.DIFF, 287 | object: DIFF_MODES.DIFF, 288 | }, 289 | }); 290 | 291 | const a = [1, 2, 3]; 292 | const b = 1; 293 | let diff = differify.compare(a, b); 294 | 295 | expect(diff._).toBe(undefined); 296 | expect(diff.status).toBe(PROPERTY_STATUS.MODIFIED); 297 | expect(diff.changes).toBe(1); 298 | expect(diff.original).toBe(a); 299 | expect(diff.current).toBe(b); 300 | }); 301 | test('should return null when the passed status is not a valid one or is not present', () => { 302 | differify.setConfig({ 303 | mode: { 304 | array: DIFF_MODES.DIFF, 305 | object: DIFF_MODES.DIFF, 306 | }, 307 | }); 308 | 309 | const a = [1, 2, 3]; 310 | const b = 1; 311 | const diff = differify.compare(a, b); 312 | let res = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED); 313 | expect(typeof res).toBe('number'); 314 | expect(res).toBe(b); 315 | 316 | res = differify.filterDiffByStatus(diff, null); 317 | expect(res).toBe(null); 318 | 319 | res = differify.filterDiffByStatus(diff, PROPERTY_STATUS.ADDED); 320 | expect(res).toBe(null); 321 | res = differify.filterDiffByStatus(diff, PROPERTY_STATUS.EQUAL); 322 | expect(res).toBe(null); 323 | res = differify.filterDiffByStatus(diff, PROPERTY_STATUS.DELETED); 324 | expect(res).toBe(null); 325 | }); 326 | 327 | test('applyChanges: should return undefined when the data passed in is not a valid property descriptor', () => { 328 | let res = differify.applyLeftChanges({ _: 1 }, true); 329 | expect(res).toBe(undefined); 330 | 331 | res = differify.applyRightChanges({ _: 1 }, true); 332 | expect(res).toBe(undefined); 333 | 334 | res = differify.applyLeftChanges( 335 | { 336 | _: { 337 | name: { 338 | original: 'Fabian', 339 | current: 'Fabian', 340 | status: PROPERTY_STATUS.EQUAL, 341 | changes: 0, 342 | }, 343 | }, 344 | status: PROPERTY_STATUS.EQUAL, 345 | changes: 0, 346 | }, 347 | true 348 | ); 349 | expect(res).toStrictEqual({}); 350 | 351 | res = differify.applyLeftChanges( 352 | { 353 | current: 'Judith', 354 | original: 'Fabian', 355 | status: PROPERTY_STATUS.EQUAL, 356 | changes: 0, 357 | }, 358 | true 359 | ); 360 | expect(res).toBe('Fabian'); 361 | 362 | res = differify.applyLeftChanges( 363 | { 364 | _: { 365 | name: { 366 | current: 'Fabian', 367 | original: 'Judith', 368 | status: PROPERTY_STATUS.MODIFIED, 369 | changes: 1, 370 | }, 371 | }, 372 | }, 373 | true 374 | ); 375 | expect(res).toStrictEqual({ 376 | name: 'Judith', 377 | }); 378 | 379 | res = differify.applyRightChanges( 380 | { 381 | _: { 382 | name: { 383 | current: 'Fabian', 384 | original: 'Judith', 385 | status: PROPERTY_STATUS.MODIFIED, 386 | changes: 1, 387 | }, 388 | }, 389 | }, 390 | true 391 | ); 392 | 393 | expect(res).toStrictEqual({ 394 | name: 'Fabian', 395 | }); 396 | 397 | res = differify.applyRightChanges( 398 | { 399 | current: 'Judith', 400 | original: 'Fabian', 401 | status: PROPERTY_STATUS.EQUAL, 402 | changes: 0, 403 | }, 404 | true 405 | ); 406 | expect(res).toBe('Judith'); 407 | 408 | res = differify.applyRightChanges({}, true); 409 | expect(res).toBe(null); 410 | }); 411 | 412 | test('compare different types input but same prototype', () => { 413 | differify.setConfig({ 414 | mode: { 415 | array: DIFF_MODES.DIFF, 416 | object: DIFF_MODES.DIFF, 417 | }, 418 | }); 419 | 420 | const a = [1, 2, 3]; 421 | const b = { a: 'foo', b: 'bar' }; 422 | let diff = differify.compare(a, b); 423 | 424 | expect(diff.original).toBe(a); 425 | expect(diff.current).toBe(b); 426 | expect(diff.status).toBe(PROPERTY_STATUS.MODIFIED); 427 | expect(diff.changes).toBe(1); 428 | 429 | diff = differify.compare({ a: [1, 2, 3] }, { a: 'foo', b: 'bar' }); 430 | expect(diff.changes).toBe(2); 431 | expect(diff._.a.changes).toBe(1); 432 | expect(diff._.a.status).toBe(PROPERTY_STATUS.MODIFIED); 433 | expect(JSON.stringify(diff._.a.original)).toBe(JSON.stringify([1, 2, 3])); 434 | expect(diff._.a.current).toBe('foo'); 435 | expect(diff._.b.changes).toBe(1); 436 | expect(diff._.b.status).toBe(PROPERTY_STATUS.ADDED); 437 | expect(diff._.b.original).toBe(null); 438 | expect(diff._.b.current).toBe('bar'); 439 | }); 440 | 441 | test('Array comparission with ALL possible configurations', () => { 442 | // DIFF DIFF 443 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 444 | const a = ['Hello', 'how', 'are', 'you']; 445 | const b = ['fine', 'and', 'you']; 446 | 447 | expect(Object.prototype.toString.call(differify.compare([], [])._)).toBe( 448 | '[object Array]' 449 | ); 450 | expect(differify.compare([], [])._.length).toBe(0); 451 | 452 | let diff = differify.compare(a, b); 453 | 454 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 455 | expect(diff.changes === 4).toBeTruthy(); 456 | expect(diff._[0].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 457 | expect(diff._[1].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 458 | expect(diff._[2].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 459 | expect(diff._[3].status === 'DELETED').toBeTruthy(); 460 | 461 | diff = differify.compare(b, a); 462 | 463 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 464 | expect(diff.changes === 4).toBeTruthy(); 465 | expect(diff._[0].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 466 | expect(diff._[1].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 467 | expect(diff._[2].status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 468 | expect(diff._[3].status === PROPERTY_STATUS.ADDED).toBeTruthy(); 469 | 470 | // DIFF EQ 471 | diff = differify.compare(a, a); 472 | 473 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 474 | expect(diff.changes === 0).toBeTruthy(); 475 | expect(diff._[0].status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 476 | expect(diff._[1].status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 477 | expect(diff._[2].status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 478 | expect(diff._[3].status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 479 | 480 | // REFERENCE DIFF 481 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.REFERENCE } }); 482 | diff = differify.compare(a, b); 483 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 484 | expect(diff.changes === 1).toBeTruthy(); 485 | expect( 486 | Object.prototype.toString.call(diff) === '[object Object]' 487 | ).toBeTruthy(); 488 | 489 | expect(diff.original).toEqual(a); 490 | expect(diff.current).toEqual(b); 491 | 492 | // REFERENCE EQ 493 | diff = differify.compare(a, a); 494 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 495 | expect(diff.changes === 0).toBeTruthy(); 496 | 497 | // STRING DIFF 498 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.STRING } }); 499 | diff = differify.compare(a, b); 500 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 501 | expect(diff.changes === 1).toBeTruthy(); 502 | expect( 503 | Object.prototype.toString.call(diff) === '[object Object]' 504 | ).toBeTruthy(); 505 | 506 | expect(diff.original).toEqual(a); 507 | expect(diff.current).toEqual(b); 508 | 509 | diff = differify.compare([], []); 510 | 511 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 512 | expect(diff.changes === 0).toBeTruthy(); 513 | expect( 514 | Object.prototype.toString.call(diff) === '[object Object]' 515 | ).toBeTruthy(); 516 | expect(diff.original).toEqual([]); 517 | expect( 518 | Array.isArray(diff.original) && diff.original.length === 0 519 | ).toBeTruthy(); 520 | expect(diff.current).toEqual([]); 521 | expect( 522 | Array.isArray(diff.current) && diff.current.length === 0 523 | ).toBeTruthy(); 524 | 525 | expect(diff._ === undefined).toBeTruthy(); 526 | 527 | // STRING EQ 528 | diff = differify.compare(a, a); 529 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 530 | expect(diff.changes === 0).toBeTruthy(); 531 | expect( 532 | Object.prototype.toString.call(diff) === '[object Object]' 533 | ).toBeTruthy(); 534 | 535 | expect(diff.original).toEqual(a); 536 | expect(diff.current).toEqual(a); 537 | 538 | diff = differify.compare([], []); 539 | 540 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 541 | expect(diff.changes === 0).toBeTruthy(); 542 | expect( 543 | Object.prototype.toString.call(diff) === '[object Object]' 544 | ).toBeTruthy(); 545 | 546 | expect(diff.original).toEqual([]); 547 | expect( 548 | Array.isArray(diff.original) && diff.original.length === 0 549 | ).toBeTruthy(); 550 | expect(diff.current).toEqual([]); 551 | expect( 552 | Array.isArray(diff.current) && diff.current.length === 0 553 | ).toBeTruthy(); 554 | 555 | expect(diff._ === undefined).toBeTruthy(); 556 | }); 557 | 558 | test('Object comparission with ALL possible configurations', () => { 559 | // DIFF DIFF 560 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 561 | let a = getAObject(); 562 | let b = getBObject(); 563 | 564 | expect(differify.compare({}, {})._).toBe(null); 565 | expect(differify.compare({}, {}).status).toBe(PROPERTY_STATUS.EQUAL); 566 | expect(differify.compare({}, {}).changes).toBe(0); 567 | 568 | let diff = differify.compare(a, b); 569 | expect(diff.status).toBe(PROPERTY_STATUS.MODIFIED); 570 | expect(diff.changes).toBe(7); 571 | expect(diff._.name.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 572 | expect(diff._.age.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 573 | diff._.extras._.hobbies._.forEach((i) => 574 | expect(i.status === PROPERTY_STATUS.MODIFIED).toBeTruthy() 575 | ); 576 | expect(diff._.date.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 577 | 578 | // DIFF EQ 579 | a = getAObject(); 580 | b = getAObject(); 581 | b.date = a.date; 582 | diff = differify.compare(a, b); 583 | expect(diff.status).toBe(PROPERTY_STATUS.EQUAL); 584 | expect(diff.changes).toBe(0); 585 | expect(diff._.name.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 586 | expect(diff._.age.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 587 | diff._.extras._.hobbies._.forEach((i) => 588 | expect(i.status === PROPERTY_STATUS.EQUAL).toBeTruthy() 589 | ); 590 | expect(diff._.date.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 591 | 592 | // REFERENCE DIFF 593 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.REFERENCE } }); 594 | diff = differify.compare(getAObject(), getBObject()); 595 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 596 | expect(diff.changes === 1).toBeTruthy(); 597 | expect( 598 | differify.compare({}, {}).status === PROPERTY_STATUS.MODIFIED 599 | ).toBeTruthy(); 600 | 601 | // REFERENCE EQ 602 | a = getAObject(); 603 | b = a; 604 | diff = differify.compare(a, b); 605 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 606 | expect(diff.changes === 0).toBeTruthy(); 607 | 608 | // STRING DIFF 609 | differify.setConfig({ mode: { object: DIFF_MODES.STRING, array: DIFF_MODES.STRING } }); 610 | diff = differify.compare(getAObject(), getBObject()); 611 | expect(diff.status === PROPERTY_STATUS.MODIFIED).toBeTruthy(); 612 | expect(diff.changes === 1).toBeTruthy(); 613 | expect( 614 | differify.compare({}, {}).status === PROPERTY_STATUS.EQUAL 615 | ).toBeTruthy(); 616 | 617 | // STRING EQ 618 | a = getAObject(); 619 | b = a; 620 | diff = differify.compare(a, b); 621 | expect(diff.changes === 0).toBeTruthy(); 622 | expect(diff.status === PROPERTY_STATUS.EQUAL).toBeTruthy(); 623 | }); 624 | 625 | test('test output for ALL object modes', () => { 626 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 627 | const a = getAObject(); 628 | a.date = 1589657835225; 629 | const b = getBObject(); 630 | b.date = 1589657835225; 631 | 632 | expect(JSON.stringify(differify.compare(a, b))).toBe( 633 | '{"_":{"name":{"original":"Judith","current":"Fabian","status":"MODIFIED","changes":1},"age":{"original":33,"current":36,"status":"MODIFIED","changes":1},"friends":{"_":[{"original":"Cecilia","current":"Finn","status":"MODIFIED","changes":1},{"original":"Stephanie","current":"Jake","status":"MODIFIED","changes":1}],"status":"MODIFIED","changes":2},"extras":{"_":{"hobbies":{"_":[{"original":"Gym","current":"Football Soccer","status":"MODIFIED","changes":1},{"original":"Dance","current":"Programming","status":"MODIFIED","changes":1}],"status":"MODIFIED","changes":2}},"status":"MODIFIED","changes":2},"date":{"original":1589657835225,"current":1589657835225,"status":"EQUAL","changes":0}},"status":"MODIFIED","changes":6}' 634 | ); 635 | 636 | differify.setConfig({ mode: { object: 'REFERENCE' } }); 637 | 638 | let diff = differify.compare(a, b); 639 | 640 | expect(diff._ === undefined).toBeTruthy(); 641 | expect(diff.original).toEqual(a); 642 | expect(diff.current).toEqual(b); 643 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 644 | expect(diff.changes).toEqual(1); 645 | 646 | differify.setConfig({ mode: { object: DIFF_MODES.STRING } }); 647 | 648 | diff = differify.compare(a, b); 649 | 650 | expect(diff._ === undefined).toBeTruthy(); 651 | expect(diff.original).toEqual(a); 652 | expect(diff.current).toEqual(b); 653 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 654 | expect(diff.changes).toEqual(1); 655 | }); 656 | 657 | test('test output for ALL array modes', () => { 658 | const a = [1, 2, 3, 4, 5]; 659 | const b = [1, 2, 4, 6, 8, 10]; 660 | 661 | differify.setConfig({ mode: { array: DIFF_MODES.DIFF } }); 662 | expect(JSON.stringify(differify.compare(a, b))).toBe( 663 | '{"_":[{"original":1,"current":1,"status":"EQUAL","changes":0},{"original":2,"current":2,"status":"EQUAL","changes":0},{"original":3,"current":4,"status":"MODIFIED","changes":1},{"original":4,"current":6,"status":"MODIFIED","changes":1},{"original":5,"current":8,"status":"MODIFIED","changes":1},{"original":null,"current":10,"status":"ADDED","changes":1}],"status":"MODIFIED","changes":4}' 664 | ); 665 | 666 | differify.setConfig({ mode: { array: DIFF_MODES.REFERENCE, object: 'REFERENCE' } }); 667 | 668 | let diff = differify.compare(a, b); 669 | 670 | expect(diff._ === undefined).toBeTruthy(); 671 | expect(diff.original).toEqual(a); 672 | expect(diff.current).toEqual(b); 673 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 674 | expect(diff.changes).toEqual(1); 675 | 676 | differify.setConfig({ mode: { array: DIFF_MODES.STRING, object: 'REFERENCE' } }); 677 | 678 | diff = differify.compare(a, b); 679 | 680 | expect(diff._ === undefined).toBeTruthy(); 681 | expect(diff.original).toEqual(a); 682 | expect(diff.current).toEqual(b); 683 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 684 | expect(diff.changes).toEqual(1); 685 | }); 686 | 687 | test('should merge right changes properly', () => { 688 | const A = { 689 | id: 1, 690 | roles: ['developer'], 691 | name: 'Person1', 692 | hobbies: { 693 | a: 'futbol', 694 | b: [{ name: 'willy' }], 695 | }, 696 | birthdate: 440305200000, 697 | }; 698 | 699 | const B = { 700 | id: 2, 701 | roles: ['developer', 'admin'], 702 | name: 'Person2', 703 | hobbies: { 704 | a: 'dance', 705 | b: [{ name: 'willys' }], 706 | }, 707 | birthdate: 533444400000, 708 | }; 709 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 710 | 711 | let diff = differify.compare(A, B); 712 | 713 | let merged = differify.applyRightChanges(diff); 714 | 715 | expect(merged.id).toBe(2); 716 | expect(merged.name).toBe('Person2'); 717 | expect(merged.birthdate).toBe(533444400000); 718 | expect(merged.hobbies.a).toBe('dance'); 719 | expect(Object.prototype.toString.call(merged.hobbies.b)).toBe( 720 | '[object Array]' 721 | ); 722 | expect(merged.hobbies.b.length).toBe(1); 723 | expect(merged.hobbies.b[0].name).toBe('willys'); 724 | 725 | diff = differify.compare({ a: 'a', b: 'b', c: 'c' }, { a: 'b', b: 'a' }); 726 | merged = differify.applyRightChanges(diff); 727 | 728 | expect(merged.a).toBe('b'); 729 | expect(merged.b).toBe('a'); 730 | expect(merged.c).toBe('c'); 731 | 732 | diff = differify.compare([1, 2, 3, 9], [4, 5, 6]); 733 | merged = differify.applyRightChanges(diff); 734 | 735 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 736 | expect(merged.length).toBe(4); 737 | expect(merged[0]).toBe(4); 738 | expect(merged[1]).toBe(5); 739 | expect(merged[2]).toBe(6); 740 | expect(merged[3]).toBe(9); 741 | 742 | diff = differify.compare([1, 2], [4, 5, 6]); 743 | merged = differify.applyRightChanges(diff); 744 | 745 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 746 | expect(merged.length).toBe(3); 747 | expect(merged[0]).toBe(4); 748 | expect(merged[1]).toBe(5); 749 | expect(merged[2]).toBe(6); 750 | 751 | differify.setConfig({ 752 | ...differify.getConfig(), 753 | compareArraysInOrder: false, 754 | }); 755 | diff = differify.compare([1, 2], [1, 4, 5, 6]); 756 | merged = differify.applyRightChanges(diff); 757 | 758 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 759 | expect(merged.length).toBe(4); 760 | expect(merged[0]).toBe(1); 761 | expect(merged[1]).toBe(4); 762 | expect(merged[2]).toBe(5); 763 | expect(merged[3]).toBe(6); 764 | 765 | // STRING MODE 766 | 767 | differify.setConfig({ mode: { object: DIFF_MODES.STRING, array: DIFF_MODES.STRING } }); 768 | 769 | diff = differify.compare(A, B); 770 | 771 | merged = differify.applyRightChanges(diff); 772 | 773 | expect(merged.id).toBe(2); 774 | expect(merged.name).toBe('Person2'); 775 | expect(merged.birthdate).toBe(533444400000); 776 | expect(merged.hobbies.a).toBe('dance'); 777 | expect(Object.prototype.toString.call(merged.hobbies.b)).toBe( 778 | '[object Array]' 779 | ); 780 | expect(merged.hobbies.b.length).toBe(1); 781 | expect(merged.hobbies.b[0].name).toBe('willys'); 782 | 783 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.REFERENCE } }); 784 | 785 | diff = differify.compare(A, B); 786 | 787 | merged = differify.applyRightChanges(diff); 788 | 789 | expect(merged.id).toBe(2); 790 | expect(merged.name).toBe('Person2'); 791 | expect(merged.birthdate).toBe(533444400000); 792 | expect(merged.hobbies.a).toBe('dance'); 793 | expect(Object.prototype.toString.call(merged.hobbies.b)).toBe( 794 | '[object Array]' 795 | ); 796 | expect(merged.hobbies.b.length).toBe(1); 797 | expect(merged.hobbies.b[0].name).toBe('willys'); 798 | }); 799 | 800 | test('should merge left changes properly', () => { 801 | const B = { 802 | id: 2, 803 | roles: ['developer', 'admin'], 804 | name: 'Person2', 805 | hobbies: { 806 | a: 'dance', 807 | b: [{ name: 'willys' }], 808 | }, 809 | birthdate: 533444400000, 810 | }; 811 | 812 | const A = { 813 | id: 1, 814 | roles: ['developer'], 815 | name: 'Person1', 816 | hobbies: { 817 | a: 'futbol', 818 | b: [{ name: 'willy' }], 819 | }, 820 | birthdate: 440305200000, 821 | }; 822 | 823 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 824 | let diff = differify.compare(A, B); 825 | 826 | let merged = differify.applyLeftChanges(diff); 827 | 828 | expect(merged.id).toBe(1); 829 | expect(merged.name).toBe('Person1'); 830 | expect(merged.birthdate).toBe(440305200000); 831 | expect(merged.hobbies.a).toBe('futbol'); 832 | expect(Object.prototype.toString.call(merged.hobbies.b)).toBe( 833 | '[object Array]' 834 | ); 835 | expect(merged.hobbies.b.length).toBe(1); 836 | expect(merged.hobbies.b[0].name).toBe('willy'); 837 | 838 | diff = differify.compare({ a: 'a', b: 'b', c: 'c' }, { a: 'b', b: 'a' }); 839 | merged = differify.applyLeftChanges(diff); 840 | 841 | expect(merged.a).toBe('a'); 842 | expect(merged.b).toBe('b'); 843 | expect(merged.c).toBe('c'); 844 | 845 | diff = differify.compare([1, 2, 3], [4, 5, 6, 7]); 846 | merged = differify.applyLeftChanges(diff); 847 | 848 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 849 | expect(merged.length).toBe(4); 850 | expect(merged[0]).toBe(1); 851 | expect(merged[1]).toBe(2); 852 | expect(merged[2]).toBe(3); 853 | expect(merged[3]).toBe(7); 854 | 855 | diff = differify.compare([1, 2, 3], [4, 5]); 856 | merged = differify.applyLeftChanges(diff); 857 | 858 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 859 | expect(merged.length).toBe(3); 860 | expect(merged[0]).toBe(1); 861 | expect(merged[1]).toBe(2); 862 | expect(merged[2]).toBe(3); 863 | 864 | differify.setConfig({ mode: { object: DIFF_MODES.STRING, array: DIFF_MODES.STRING } }); 865 | 866 | diff = differify.compare([1, 2, 3], [4, 5]); 867 | merged = differify.applyLeftChanges(diff); 868 | 869 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 870 | expect(merged.length).toBe(3); 871 | expect(merged[0]).toBe(1); 872 | expect(merged[1]).toBe(2); 873 | expect(merged[2]).toBe(3); 874 | 875 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.REFERENCE } }); 876 | 877 | diff = differify.compare([1, 2, 3], [4, 5]); 878 | merged = differify.applyLeftChanges(diff); 879 | 880 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 881 | expect(merged.length).toBe(3); 882 | expect(merged[0]).toBe(1); 883 | expect(merged[1]).toBe(2); 884 | expect(merged[2]).toBe(3); 885 | }); 886 | 887 | test('should merge the difference ONLY', () => { 888 | const B = { 889 | id: 1, 890 | roles: ['developer', 'admin'], 891 | name: 'Person2', 892 | birthdate: 533444400000, 893 | color: 'red', 894 | }; 895 | 896 | const A = { 897 | id: 1, 898 | roles: ['developer'], 899 | name: 'Person1', 900 | birthdate: 440305200000, 901 | }; 902 | 903 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 904 | let diff = differify.compare(A, B); 905 | 906 | let merged = differify.applyLeftChanges(diff, true); 907 | 908 | expect(merged.id).toBe(undefined); 909 | expect(merged.name).toBe('Person1'); 910 | expect(merged.birthdate).toBe(440305200000); 911 | expect(merged.birthdate).toBe(440305200000); 912 | expect(merged.color).toBe('red'); 913 | expect(Object.prototype.toString.call(merged.roles)).toBe('[object Array]'); 914 | expect(merged.roles.length).toBe(1); 915 | expect(merged.roles[0]).toBe('admin'); 916 | 917 | diff = differify.compare({ a: 'a', b: 'b', c: 'c' }, { a: 'a', b: 'b' }); 918 | merged = differify.applyLeftChanges(diff, true); 919 | 920 | expect(merged.a).toBe(undefined); 921 | expect(merged.b).toBe(undefined); 922 | expect(merged.c).toBe('c'); 923 | 924 | diff = differify.compare([1, 2, 3], [1, 2, 3, 4]); 925 | merged = differify.applyLeftChanges(diff, true); 926 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 927 | expect(merged.length).toBe(1); 928 | expect(merged[0]).toBe(4); 929 | 930 | diff = differify.compare([1, 2, 3], [1, 4, 3, 2]); 931 | merged = differify.applyLeftChanges(diff, true); 932 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 933 | expect(merged.length).toBe(2); 934 | expect(merged[0]).toBe(2); 935 | expect(merged[1]).toBe(2); 936 | 937 | diff = differify.compare({ a: 'a', b: 'b', c: 'c' }, { a: 'b', b: 'a' }); 938 | merged = differify.applyLeftChanges(diff, true); 939 | 940 | expect(merged.a).toBe('a'); 941 | expect(merged.b).toBe('b'); 942 | expect(merged.c).toBe('c'); 943 | 944 | diff = differify.compare([1, 2, 3], [4, 5, 6, 7]); 945 | merged = differify.applyLeftChanges(diff, true); 946 | 947 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 948 | expect(merged.length).toBe(4); 949 | expect(merged[0]).toBe(1); 950 | expect(merged[1]).toBe(2); 951 | expect(merged[2]).toBe(3); 952 | expect(merged[3]).toBe(7); 953 | 954 | diff = differify.compare([1, 2, 3], [4, 5]); 955 | merged = differify.applyLeftChanges(diff, true); 956 | 957 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 958 | expect(merged.length).toBe(3); 959 | expect(merged[0]).toBe(1); 960 | expect(merged[1]).toBe(2); 961 | expect(merged[2]).toBe(3); 962 | 963 | diff = differify.compare([1, 2], [1, 2]); 964 | merged = differify.applyLeftChanges(diff, true); 965 | 966 | expect(merged).not.toBe(undefined); 967 | expect(merged.length).toBe(0); 968 | }); 969 | 970 | test('should return falsy values when right changes are applied', () => { 971 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 972 | 973 | const oldObj = { hasDocument: true }; 974 | const newObj = { hasDocument: false }; 975 | 976 | let diff = differify.compare(oldObj, newObj); 977 | expect(diff._).not.toBe(undefined); 978 | expect(diff.changes).toBe(1); 979 | expect(diff._.hasDocument.original).toBe(true); 980 | expect(diff._.hasDocument.current).toBe(false); 981 | let changes = differify.applyRightChanges(diff, true); 982 | expect(changes).not.toBe(undefined); 983 | expect(changes.hasDocument).toBe(false); 984 | 985 | newObj.hasDocument = null; 986 | diff = differify.compare(oldObj, newObj); 987 | expect(diff._).not.toBe(undefined); 988 | expect(diff.changes).toBe(1); 989 | expect(diff._.hasDocument.original).toBe(true); 990 | expect(diff._.hasDocument.current).toBe(null); 991 | changes = differify.applyRightChanges(diff, true); 992 | expect(changes).not.toBe(undefined); 993 | expect(changes.hasDocument).toBe(null); 994 | 995 | newObj.hasDocument = undefined; 996 | diff = differify.compare(oldObj, newObj); 997 | expect(diff._).not.toBe(undefined); 998 | expect(diff.changes).toBe(1); 999 | expect(diff._.hasDocument.original).toBe(true); 1000 | expect(diff._.hasDocument.current).toBe(undefined); 1001 | changes = differify.applyRightChanges(diff, true); 1002 | expect(changes).not.toBe(undefined); 1003 | expect(changes.hasDocument).toBe(undefined); 1004 | }); 1005 | 1006 | test('should return falsy values when left changes are applied', () => { 1007 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1008 | 1009 | const oldObj = { hasDocument: false }; 1010 | const newObj = { hasDocument: true }; 1011 | 1012 | let diff = differify.compare(oldObj, newObj); 1013 | expect(diff._).not.toBe(undefined); 1014 | expect(diff.changes).toBe(1); 1015 | expect(diff._.hasDocument.current).toBe(true); 1016 | expect(diff._.hasDocument.original).toBe(false); 1017 | let changes = differify.applyLeftChanges(diff, true); 1018 | expect(changes).not.toBe(undefined); 1019 | expect(changes.hasDocument).toBe(false); 1020 | 1021 | oldObj.hasDocument = null; 1022 | diff = differify.compare(oldObj, newObj); 1023 | expect(diff._).not.toBe(undefined); 1024 | expect(diff.changes).toBe(1); 1025 | expect(diff._.hasDocument.current).toBe(true); 1026 | expect(diff._.hasDocument.original).toBe(null); 1027 | changes = differify.applyLeftChanges(diff, true); 1028 | expect(changes).not.toBe(undefined); 1029 | expect(changes.hasDocument).toBe(null); 1030 | 1031 | oldObj.hasDocument = undefined; 1032 | diff = differify.compare(oldObj, newObj); 1033 | expect(diff._).not.toBe(undefined); 1034 | expect(diff.changes).toBe(1); 1035 | expect(diff._.hasDocument.current).toBe(true); 1036 | expect(diff._.hasDocument.original).toBe(undefined); 1037 | changes = differify.applyLeftChanges(diff, true); 1038 | expect(changes).not.toBe(undefined); 1039 | expect(changes.hasDocument).toBe(undefined); 1040 | }); 1041 | 1042 | test('should return a non null result when the config parameters are DIFF for objects and arrays', () => { 1043 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1044 | let diff = differify.compare( 1045 | { a: 'a', b: 'b', c: 'c' }, 1046 | { a: 'b', b: 'a' } 1047 | ); 1048 | let merged = differify.applyLeftChanges(diff); 1049 | expect(Object.prototype.toString.call(merged)).toBe('[object Object]'); 1050 | expect(merged).not.toBe(null); 1051 | 1052 | diff = differify.compare([1, 2, 3], [2, 5, 6]); 1053 | merged = differify.applyLeftChanges(diff); 1054 | expect(Object.prototype.toString.call(merged)).toBe('[object Array]'); 1055 | expect(merged).not.toBe(null); 1056 | }); 1057 | 1058 | test('applyLeftChanges: config combinatios', () => { 1059 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.DIFF } }); 1060 | const a = { a: 'a', b: 'b', c: 'c' }; 1061 | const b = { a: 'b', b: 'a' }; 1062 | let diff = differify.compare(a, b); 1063 | let merged = differify.applyLeftChanges(diff); 1064 | 1065 | expect(merged).toEqual(a); 1066 | 1067 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.STRING } }); 1068 | diff = differify.compare(a, b); 1069 | merged = differify.applyLeftChanges(diff); 1070 | expect(merged).toEqual({ a: 'a', b: 'b', c: 'c' }); 1071 | 1072 | differify.setConfig({ mode: { object: DIFF_MODES.STRING, array: DIFF_MODES.STRING } }); 1073 | diff = differify.compare(a, b); 1074 | merged = differify.applyLeftChanges(diff); 1075 | expect(merged).toEqual({ a: 'a', b: 'b', c: 'c' }); 1076 | 1077 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.REFERENCE } }); 1078 | diff = differify.compare(a, b); 1079 | merged = differify.applyLeftChanges(diff); 1080 | expect(merged).toEqual({ a: 'a', b: 'b', c: 'c' }); 1081 | }); 1082 | 1083 | test('should return null if wrong diff data is provided', () => { 1084 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1085 | let diff = []; 1086 | let merged = differify.applyLeftChanges(diff); 1087 | expect(merged).toBe(null); 1088 | 1089 | diff = {}; 1090 | merged = differify.applyLeftChanges(diff); 1091 | expect(merged).toBe(null); 1092 | 1093 | diff = null; 1094 | merged = differify.applyLeftChanges(diff); 1095 | expect(merged).toBe(null); 1096 | 1097 | diff = null; 1098 | merged = differify.filterDiffByStatus(diff); 1099 | expect(merged).toBe(null); 1100 | }); 1101 | 1102 | test('if no changes between two entities, the left or right apply method, should return the object when called', () => { 1103 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1104 | const A = { 1105 | name: 'Person1', 1106 | extras: { 1107 | something: '1', 1108 | somethingElse: '2', 1109 | }, 1110 | }; 1111 | const B = { 1112 | name: 'Person1', 1113 | extras: { 1114 | something: '1', 1115 | somethingElse: '2', 1116 | }, 1117 | }; 1118 | 1119 | const diff = differify.compare(A, B); 1120 | const merged = differify.applyLeftChanges(diff); 1121 | expect(merged).not.toBe(null); 1122 | expect(merged.name).toBe('Person1'); 1123 | expect(Object.prototype.toString.call(merged.extras)).toBe( 1124 | '[object Object]' 1125 | ); 1126 | expect(merged.extras.something).toBe('1'); 1127 | expect(merged.extras.somethingElse).toBe('2'); 1128 | }); 1129 | 1130 | test('should return the props filtered by status', () => { 1131 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1132 | const A = { 1133 | name: 'Person1', 1134 | extras: { 1135 | something: '1', 1136 | somethingElse: '2', 1137 | }, 1138 | member: true, 1139 | doc: 10, 1140 | friends: ['A', 'B', 'C'], 1141 | }; 1142 | const B = { 1143 | name: 'Person1', 1144 | extras: { 1145 | something: '1', 1146 | somethingElse: '2', 1147 | }, 1148 | member: false, 1149 | badges: 7, 1150 | friends: ['A', 'D', 'C', 'F'], 1151 | }; 1152 | 1153 | let diff = differify.compare(A, B); 1154 | let merged = differify.filterDiffByStatus(diff, 'DELETED'); 1155 | expect(merged).not.toBe(null); 1156 | expect(merged.name).toBe(undefined); 1157 | expect(merged.extras).toBe(undefined); 1158 | expect(merged.doc).toBe(10); 1159 | expect(merged.member).toBe(undefined); 1160 | expect(merged.friends).toBe(undefined); 1161 | 1162 | //Extended information 1163 | merged = differify.filterDiffByStatus(diff, 'DELETED', true); 1164 | expect(merged).not.toBe(null); 1165 | expect(merged.name).toBe(undefined); 1166 | expect(merged.extras).toBe(undefined); 1167 | expect(merged.doc.original).toBe(10); 1168 | expect(merged.doc.current).toBe(null); 1169 | expect(merged.member).toBe(undefined); 1170 | expect(merged.friends).toBe(undefined); 1171 | 1172 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.ADDED); 1173 | expect(merged).not.toBe(null); 1174 | expect(merged.name).toBe(undefined); 1175 | expect(merged.extras).toBe(undefined); 1176 | expect(merged.doc).toBe(undefined); 1177 | expect(merged.badges).toBe(7); 1178 | expect(merged.member).toBe(undefined); 1179 | expect(merged.friends.length).toBe(1); 1180 | expect(merged.friends[0]).toBe('F'); 1181 | 1182 | //Extended information 1183 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.ADDED, true); 1184 | expect(merged).not.toBe(null); 1185 | expect(merged.name).toBe(undefined); 1186 | expect(merged.extras).toBe(undefined); 1187 | expect(merged.doc).toBe(undefined); 1188 | expect(merged.badges.current).toBe(7); 1189 | expect(merged.badges.original).toBe(null); 1190 | expect(merged.member).toBe(undefined); 1191 | expect(merged.friends.length).toBe(1); 1192 | expect(merged.friends[0].original).toBe(null); 1193 | expect(merged.friends[0].current).toBe('F'); 1194 | 1195 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED); 1196 | expect(merged).not.toBe(null); 1197 | expect(merged.name).toBe(undefined); 1198 | expect(merged.extras).toBe(undefined); 1199 | expect(merged.doc).toBe(undefined); 1200 | expect(merged.badges).toBe(undefined); 1201 | expect(merged.member).toBeFalsy(); 1202 | expect(merged.friends.length).toBe(1); 1203 | expect(merged.friends[0]).toBe('D'); 1204 | 1205 | //Extended information 1206 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED, true); 1207 | expect(merged).not.toBe(null); 1208 | expect(merged.name).toBe(undefined); 1209 | expect(merged.extras).toBe(undefined); 1210 | expect(merged.doc).toBe(undefined); 1211 | expect(merged.badges).toBe(undefined); 1212 | expect(merged.member.original).toBeTruthy(); 1213 | expect(merged.member.current).toBeFalsy(); 1214 | expect(merged.friends.length).toBe(1); 1215 | expect(merged.friends[0].current).toBe('D'); 1216 | expect(merged.friends[0].original).toBe('B'); 1217 | 1218 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.EQUAL); 1219 | expect(merged).not.toBe(null); 1220 | expect(merged.name).toBe('Person1'); 1221 | expect(Object.prototype.toString.call(merged.extras)).toBe( 1222 | '[object Object]' 1223 | ); 1224 | expect(merged.extras.something).toBe('1'); 1225 | expect(merged.extras.somethingElse).toBe('2'); 1226 | expect(merged.doc).toBe(undefined); 1227 | expect(merged.badges).toBe(undefined); 1228 | expect(merged.member).toBe(undefined); 1229 | expect(merged.friends.length).toBe(2); 1230 | expect(merged.friends[0]).toBe('A'); 1231 | expect(merged.friends[1]).toBe('C'); 1232 | 1233 | //Extended information 1234 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.EQUAL, true); 1235 | expect(merged).not.toBe(null); 1236 | expect(merged.name.original).toBe('Person1'); 1237 | expect(merged.name.current).toBe('Person1'); 1238 | expect(Object.prototype.toString.call(merged.extras)).toBe( 1239 | '[object Object]' 1240 | ); 1241 | expect(merged.extras.something.original).toBe('1'); 1242 | expect(merged.extras.something.current).toBe('1'); 1243 | expect(merged.extras.somethingElse.original).toBe('2'); 1244 | expect(merged.extras.somethingElse.current).toBe('2'); 1245 | expect(merged.doc).toBe(undefined); 1246 | expect(merged.badges).toBe(undefined); 1247 | expect(merged.member).toBe(undefined); 1248 | expect(merged.friends.length).toBe(2); 1249 | expect(merged.friends[0].current).toBe('A'); 1250 | expect(merged.friends[0].original).toBe('A'); 1251 | expect(merged.friends[1].current).toBe('C'); 1252 | expect(merged.friends[1].original).toBe('C'); 1253 | 1254 | differify.setConfig({ mode: { object: 'REFERENCE', array: DIFF_MODES.REFERENCE } }); 1255 | diff = differify.compare(A, B); 1256 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED); 1257 | expect(merged).toEqual({ 1258 | name: 'Person1', 1259 | extras: { something: '1', somethingElse: '2' }, 1260 | member: false, 1261 | badges: 7, 1262 | friends: ['A', 'D', 'C', 'F'], 1263 | }); 1264 | 1265 | differify.setConfig({ mode: { object: DIFF_MODES.STRING, array: DIFF_MODES.STRING } }); 1266 | diff = differify.compare(A, B); 1267 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED); 1268 | expect(merged).toEqual(B); 1269 | }); 1270 | 1271 | test('if the input is an Array, must return an array with the elements filtered by status', () => { 1272 | differify.setConfig({ mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF } }); 1273 | const A = ['A', 'B', 'C']; 1274 | const B = ['A', 'D', 'C', 'F']; 1275 | 1276 | const diff = differify.compare(A, B); 1277 | let merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.ADDED); 1278 | expect(merged).not.toBe(null); 1279 | expect(merged._).toBe(undefined); 1280 | expect(Array.isArray(merged)).toBeTruthy(); 1281 | expect(merged.length).toBe(1); 1282 | expect(merged[0]).toBe('F'); 1283 | 1284 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.MODIFIED); 1285 | expect(merged).not.toBe(null); 1286 | expect(merged._).toBe(undefined); 1287 | expect(Array.isArray(merged)).toBeTruthy(); 1288 | expect(merged.length).toBe(1); 1289 | expect(merged[0]).toBe('D'); 1290 | 1291 | merged = differify.filterDiffByStatus(diff, PROPERTY_STATUS.EQUAL); 1292 | expect(merged).not.toBe(null); 1293 | expect(merged._).toBe(undefined); 1294 | expect(Array.isArray(merged)).toBeTruthy(); 1295 | expect(merged.length).toBe(2); 1296 | expect(merged[0]).toBe('A'); 1297 | expect(merged[1]).toBe('C'); 1298 | 1299 | merged = differify.filterDiffByStatus( 1300 | differify.compare([1, 2], [1]), 1301 | 'DELETED' 1302 | ); 1303 | expect(merged).not.toBe(null); 1304 | expect(merged._).toBe(undefined); 1305 | expect(Array.isArray(merged)).toBeTruthy(); 1306 | expect(merged.length).toBe(1); 1307 | expect(merged[0]).toBe(2); 1308 | }); 1309 | 1310 | test('Testing Unordered array comparison', () => { 1311 | differify.setConfig({ 1312 | compareArraysInOrder: false, 1313 | mode: { object: DIFF_MODES.DIFF, array: DIFF_MODES.DIFF }, 1314 | }); 1315 | const A = ['A', 'B', 'C']; 1316 | const B = ['A', 'D', 'C', 'F']; 1317 | 1318 | let diff = differify.compare(A, B); 1319 | 1320 | expect(diff.changes).toEqual(2); 1321 | expect(diff._.length).toEqual(4); 1322 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.EQUAL); 1323 | expect(diff._[0].original).toEqual('A'); 1324 | expect(diff._[0].current).toEqual('A'); 1325 | expect(diff._[1].status).toEqual(PROPERTY_STATUS.EQUAL); 1326 | expect(diff._[1].original).toEqual('C'); 1327 | expect(diff._[1].current).toEqual('C'); 1328 | expect(diff._[2].status).toEqual(PROPERTY_STATUS.MODIFIED); 1329 | expect(diff._[2].original).toEqual('B'); 1330 | expect(diff._[2].current).toEqual('D'); 1331 | expect(diff._[3].status).toEqual(PROPERTY_STATUS.ADDED); 1332 | expect(diff._[3].original).toEqual(null); 1333 | expect(diff._[3].current).toEqual('F'); 1334 | 1335 | diff = differify.compare(B, A); 1336 | 1337 | expect(diff.changes).toEqual(2); 1338 | expect(diff._.length).toEqual(4); 1339 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.EQUAL); 1340 | expect(diff._[0].original).toEqual('A'); 1341 | expect(diff._[0].current).toEqual('A'); 1342 | expect(diff._[1].status).toEqual(PROPERTY_STATUS.EQUAL); 1343 | expect(diff._[1].original).toEqual('C'); 1344 | expect(diff._[1].current).toEqual('C'); 1345 | expect(diff._[2].status).toEqual(PROPERTY_STATUS.MODIFIED); 1346 | expect(diff._[2].original).toEqual('D'); 1347 | expect(diff._[2].current).toEqual('B'); 1348 | expect(diff._[3].status).toEqual('DELETED'); 1349 | expect(diff._[3].original).toEqual('F'); 1350 | expect(diff._[3].current).toEqual(null); 1351 | 1352 | diff = differify.compare([1, 2, 3], [4, 5, 6, 1]); 1353 | 1354 | expect(diff.changes).toEqual(3); 1355 | expect(diff._.length).toEqual(4); 1356 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.EQUAL); 1357 | expect(diff._[0].original).toEqual(1); 1358 | expect(diff._[0].current).toEqual(1); 1359 | expect(diff._[1].status).toEqual(PROPERTY_STATUS.MODIFIED); 1360 | expect(diff._[1].original).toEqual(2); 1361 | expect(diff._[1].current).toEqual(4); 1362 | expect(diff._[2].status).toEqual(PROPERTY_STATUS.MODIFIED); 1363 | expect(diff._[2].original).toEqual(3); 1364 | expect(diff._[2].current).toEqual(5); 1365 | expect(diff._[3].status).toEqual(PROPERTY_STATUS.ADDED); 1366 | expect(diff._[3].original).toEqual(null); 1367 | expect(diff._[3].current).toEqual(6); 1368 | 1369 | diff = differify.compare( 1370 | [ 1371 | { name: 'Fabian', age: 18 }, 1372 | { name: 'Judith', age: 18 }, 1373 | ], 1374 | [ 1375 | { name: 'Andres', age: 18 }, 1376 | { name: 'Fabian', age: 18 }, 1377 | ] 1378 | ); 1379 | 1380 | expect(diff.changes).toEqual(1); 1381 | expect(diff._.length).toEqual(2); 1382 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 1383 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.EQUAL); 1384 | expect(diff._[0]._.name.original).toEqual('Fabian'); 1385 | expect(diff._[0]._.name.current).toEqual('Fabian'); 1386 | expect(diff._[0]._.age.original).toEqual(18); 1387 | expect(diff._[0]._.age.current).toEqual(18); 1388 | 1389 | expect(diff._[1].status).toEqual(PROPERTY_STATUS.MODIFIED); 1390 | expect(diff._[1]._.name.original).toEqual('Judith'); 1391 | expect(diff._[1]._.name.current).toEqual('Andres'); 1392 | expect(diff._[1]._.age.original).toEqual(18); 1393 | expect(diff._[1]._.age.current).toEqual(18); 1394 | 1395 | diff = differify.compare( 1396 | [ 1397 | { name: 'Fabian', age: 19 }, 1398 | { name: 'Judith', age: 18 }, 1399 | ], 1400 | [ 1401 | { name: 'Andres', age: 18 }, 1402 | { name: 'Fabian', age: 18 }, 1403 | ] 1404 | ); 1405 | 1406 | expect(diff.changes).toEqual(3); 1407 | expect(diff._.length).toEqual(2); 1408 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 1409 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.MODIFIED); 1410 | expect(diff._[0]._.name.original).toEqual('Fabian'); 1411 | expect(diff._[0]._.name.current).toEqual('Andres'); 1412 | expect(diff._[0]._.name.status).toEqual(PROPERTY_STATUS.MODIFIED); 1413 | expect(diff._[0]._.age.original).toEqual(19); 1414 | expect(diff._[0]._.age.current).toEqual(18); 1415 | expect(diff._[0]._.age.status).toEqual(PROPERTY_STATUS.MODIFIED); 1416 | 1417 | expect(diff._[1]._.name.original).toEqual('Judith'); 1418 | expect(diff._[1]._.name.current).toEqual('Fabian'); 1419 | expect(diff._[1]._.name.status).toEqual(PROPERTY_STATUS.MODIFIED); 1420 | expect(diff._[1]._.age.original).toEqual(18); 1421 | expect(diff._[1]._.age.current).toEqual(18); 1422 | expect(diff._[1]._.age.status).toEqual(PROPERTY_STATUS.EQUAL); 1423 | 1424 | diff = differify.compare( 1425 | [ 1426 | { 1427 | id: 155, 1428 | phrase: 'I was deleted', 1429 | }, 1430 | { 1431 | id: 156, 1432 | phrase: 'Can you help me with', 1433 | }, 1434 | { 1435 | id: 123, 1436 | phrase: 'Was edite', 1437 | }, 1438 | { 1439 | id: 157, 1440 | phrase: 'Help me with', 1441 | }, 1442 | ], 1443 | [ 1444 | { 1445 | id: 156, 1446 | phrase: 'Can you help me with', 1447 | }, 1448 | { 1449 | id: 123, 1450 | phrase: 'Was edited', 1451 | }, 1452 | { 1453 | id: 88, 1454 | phrase: 'Was added in between', 1455 | }, 1456 | { 1457 | id: 157, 1458 | phrase: 'Help me with', 1459 | }, 1460 | ] 1461 | ); 1462 | 1463 | expect(diff.changes).toEqual(4); 1464 | expect(diff._.length).toEqual(4); 1465 | expect(diff.status).toEqual(PROPERTY_STATUS.MODIFIED); 1466 | 1467 | expect(diff._[0].status).toEqual(PROPERTY_STATUS.EQUAL); 1468 | expect(diff._[0]._.id.original).toEqual(156); 1469 | expect(diff._[0]._.id.current).toEqual(156); 1470 | expect(diff._[0]._.id.status).toEqual(PROPERTY_STATUS.EQUAL); 1471 | expect(diff._[0]._.phrase.original).toEqual('Can you help me with'); 1472 | expect(diff._[0]._.phrase.current).toEqual('Can you help me with'); 1473 | expect(diff._[0]._.phrase.status).toEqual(PROPERTY_STATUS.EQUAL); 1474 | 1475 | expect(diff._[1].status).toEqual(PROPERTY_STATUS.EQUAL); 1476 | expect(diff._[1]._.id.original).toEqual(157); 1477 | expect(diff._[1]._.id.current).toEqual(157); 1478 | expect(diff._[1]._.id.status).toEqual(PROPERTY_STATUS.EQUAL); 1479 | expect(diff._[1]._.phrase.original).toEqual('Help me with'); 1480 | expect(diff._[1]._.phrase.current).toEqual('Help me with'); 1481 | expect(diff._[1]._.phrase.status).toEqual(PROPERTY_STATUS.EQUAL); 1482 | 1483 | expect(diff._[2].status).toEqual(PROPERTY_STATUS.MODIFIED); 1484 | expect(diff._[2]._.id.original).toEqual(155); 1485 | expect(diff._[2]._.id.current).toEqual(123); 1486 | expect(diff._[2]._.id.status).toEqual(PROPERTY_STATUS.MODIFIED); 1487 | expect(diff._[2]._.phrase.original).toEqual('I was deleted'); 1488 | expect(diff._[2]._.phrase.current).toEqual('Was edited'); 1489 | expect(diff._[2]._.phrase.status).toEqual(PROPERTY_STATUS.MODIFIED); 1490 | 1491 | expect(diff._[3].status).toEqual(PROPERTY_STATUS.MODIFIED); 1492 | expect(diff._[3]._.id.original).toEqual(123); 1493 | expect(diff._[3]._.id.current).toEqual(88); 1494 | expect(diff._[3]._.id.status).toEqual(PROPERTY_STATUS.MODIFIED); 1495 | expect(diff._[3]._.phrase.original).toEqual('Was edite'); 1496 | expect(diff._[3]._.phrase.current).toEqual('Was added in between'); 1497 | expect(diff._[3]._.phrase.status).toEqual(PROPERTY_STATUS.MODIFIED); 1498 | 1499 | const before = { 1500 | members: [ 1501 | { 1502 | memberIds: ['a', 'b'], 1503 | originalMemberId: '1', 1504 | type: 'COUNTRYCLUB', 1505 | }, 1506 | { 1507 | memberIds: ['a', 'b'], 1508 | originalMemberId: '1', 1509 | type: 'COUNTRYCLUB', 1510 | }, 1511 | { 1512 | memberIds: ['a', 'b', 'f'], 1513 | originalMemberId: '1', 1514 | type: 'COUNTRYCLUB', 1515 | }, 1516 | ], 1517 | }; 1518 | 1519 | const after = { 1520 | members: [ 1521 | { 1522 | memberIds: ['a', 'z', 'b'], 1523 | originalMemberId: '1', 1524 | type: 'COUNTRYCLUB', 1525 | }, 1526 | { 1527 | memberIds: ['a', 'z', 'b'], 1528 | originalMemberId: '1', 1529 | type: 'COUNTRYCLUB', 1530 | }, 1531 | { 1532 | memberIds: ['a', 'b'], 1533 | originalMemberId: '1', 1534 | type: 'COUNTRYCLUB', 1535 | }, 1536 | { 1537 | memberIds: ['a', 'z', 'b'], 1538 | originalMemberId: '1', 1539 | type: 'COUNTRYCLUB', 1540 | }, 1541 | ], 1542 | }; 1543 | 1544 | diff = differify.compare(before, after); 1545 | expect(diff.status).toBe(PROPERTY_STATUS.MODIFIED); 1546 | expect(diff.changes).toBe(3); 1547 | 1548 | expect(JSON.stringify(diff)).toBe( 1549 | '{"_":{"members":{"_":[{"_":{"memberIds":{"_":[{"original":"a","current":"a","status":"EQUAL","changes":0},{"original":"b","current":"b","status":"EQUAL","changes":0}],"status":"EQUAL","changes":0},"originalMemberId":{"original":"1","current":"1","status":"EQUAL","changes":0},"type":{"original":"COUNTRYCLUB","current":"COUNTRYCLUB","status":"EQUAL","changes":0}},"status":"EQUAL","changes":0},{"_":{"memberIds":{"_":[{"original":"a","current":"a","status":"EQUAL","changes":0},{"original":"b","current":"b","status":"EQUAL","changes":0},{"original":null,"current":"z","status":"ADDED","changes":1}],"status":"MODIFIED","changes":1},"originalMemberId":{"original":"1","current":"1","status":"EQUAL","changes":0},"type":{"original":"COUNTRYCLUB","current":"COUNTRYCLUB","status":"EQUAL","changes":0}},"status":"MODIFIED","changes":1},{"_":{"memberIds":{"_":[{"original":"a","current":"a","status":"EQUAL","changes":0},{"original":"b","current":"b","status":"EQUAL","changes":0},{"original":"f","current":"z","status":"MODIFIED","changes":1}],"status":"MODIFIED","changes":1},"originalMemberId":{"original":"1","current":"1","status":"EQUAL","changes":0},"type":{"original":"COUNTRYCLUB","current":"COUNTRYCLUB","status":"EQUAL","changes":0}},"status":"MODIFIED","changes":1},{"original":null,"current":{"memberIds":["a","z","b"],"originalMemberId":"1","type":"COUNTRYCLUB"},"status":"ADDED","changes":1}],"status":"MODIFIED","changes":3}},"status":"MODIFIED","changes":3}' 1550 | ); 1551 | 1552 | diff = differify.compare( 1553 | { 1554 | members: [ 1555 | { 1556 | memberIds: ['a', 'b'], 1557 | originalMemberId: '1', 1558 | type: 'COUNTRYCLUB', 1559 | }, 1560 | ], 1561 | }, 1562 | { 1563 | members: [ 1564 | { 1565 | memberIds: ['a', 'z', 'b'], 1566 | originalMemberId: '1', 1567 | type: 'COUNTRYCLUB', 1568 | }, 1569 | ], 1570 | } 1571 | ); 1572 | 1573 | expect(JSON.stringify(diff)).toBe( 1574 | '{"_":{"members":{"_":[{"_":{"memberIds":{"_":[{"original":"a","current":"a","status":"EQUAL","changes":0},{"original":"b","current":"b","status":"EQUAL","changes":0},{"original":null,"current":"z","status":"ADDED","changes":1}],"status":"MODIFIED","changes":1},"originalMemberId":{"original":"1","current":"1","status":"EQUAL","changes":0},"type":{"original":"COUNTRYCLUB","current":"COUNTRYCLUB","status":"EQUAL","changes":0}},"status":"MODIFIED","changes":1}],"status":"MODIFIED","changes":1}},"status":"MODIFIED","changes":1}' 1575 | ); 1576 | }); 1577 | }); 1578 | -------------------------------------------------------------------------------- /test/property-diff-model.test.ts: -------------------------------------------------------------------------------- 1 | import { buildDeepDiff, buildDiff } from '../src/property-diff-model'; 2 | 3 | describe('Testing property models', () => { 4 | test('buildDiff: should return simple diff model', () => { 5 | let res = buildDiff(1, 2, 'MODIFIED', 1); 6 | expect(res.status).toBe('MODIFIED'); 7 | expect(res.changes).toBe(1); 8 | expect(res.original).toBe(1); 9 | expect(res.current).toBe(2); 10 | res = buildDiff(1, 1, 'EQUAL'); 11 | expect(res.status).toBe('EQUAL'); 12 | expect(res.changes).toBe(0); 13 | expect(res.original).toBe(1); 14 | expect(res.current).toBe(1); 15 | }); 16 | 17 | test('buildDeepDiff: should return nested diff model', () => { 18 | let res = buildDeepDiff( 19 | { 20 | name: { 21 | original: 'Fabian', 22 | current: 'Judith', 23 | status: 'MODIFIED', 24 | changes: 1, 25 | }, 26 | }, 27 | 'MODIFIED', 28 | 1 29 | ); 30 | expect(res.status).toBe('MODIFIED'); 31 | expect(res.changes).toBe(1); 32 | expect(res._).not.toBe(undefined); 33 | expect(res._).not.toBe(null); 34 | expect(res._.name.original).toBe('Fabian'); 35 | expect(res._.name.current).toBe('Judith'); 36 | res = buildDeepDiff({ 37 | name: { 38 | original: 'Fabian', 39 | current: 'Fabian', 40 | status: 'EQUAL', 41 | changes: 0, 42 | }, 43 | }, 44 | 'EQUAL'); 45 | expect(res.status).toBe('EQUAL'); 46 | expect(res.changes).toBe(0); 47 | expect(res._).not.toBe(undefined); 48 | expect(res._).not.toBe(null); 49 | expect(res._.name.original).toBe('Fabian'); 50 | expect(res._.name.current).toBe('Fabian'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "es6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationDir": "./", /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | "sourceMap": true /* Generates corresponding '.map' file. */, 17 | // "outFile": "./index-ts.js", /* Concatenate and emit output to single file. */ 18 | // "outDir": "./tsbuild", /* Redirect output structure to the directory. */ 19 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | 28 | /* Strict Type-Checking Options */ 29 | "strict": false /* Enable all strict type-checking options. */, 30 | // "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | // "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true /* Skip type checking of declaration files. */, 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "exclude": ["node_modules", "test", "test-dir"] 71 | } 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TypescriptDeclarationPlugin = require('typescript-declaration-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: path.join(__dirname, 'src', 'differify.ts'), 6 | output: { 7 | path: path.join(__dirname), 8 | filename: 'index.js', 9 | library: 'Differify', 10 | libraryExport: 'default', 11 | libraryTarget: 'umd', 12 | globalObject: 'this', 13 | umdNamedDefine: true, 14 | }, 15 | mode: 'production', 16 | module: { 17 | rules: [ 18 | { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env'], 22 | }, 23 | }, 24 | { 25 | test: /\.ts$/, 26 | loader: 'ts-loader', 27 | include: [/src/], 28 | }, 29 | { 30 | test: /\.(j|t)s$/, 31 | loader: 'webpack-comment-remover-loader', 32 | exclude: /node_modules/, 33 | }, 34 | ], 35 | }, 36 | plugins: [ 37 | new TypescriptDeclarationPlugin({ 38 | out: 'index.d.ts', 39 | }), 40 | ], 41 | resolve: { 42 | extensions: ['.ts', '.js'], 43 | }, 44 | }; 45 | --------------------------------------------------------------------------------