├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── bower.json ├── commonjs ├── core.js ├── duplex.js └── helpers.js ├── dist ├── fast-json-patch.js └── fast-json-patch.min.js ├── index.d.ts ├── index.js ├── index.mjs ├── index.ts ├── jasmine-run.mjs ├── module ├── core.d.ts ├── core.mjs ├── duplex.d.ts ├── duplex.mjs ├── helpers.d.ts └── helpers.mjs ├── package-lock.json ├── package.json ├── src ├── core.ts ├── duplex.ts └── helpers.ts ├── test ├── Sauce │ ├── CapabilityRunner.js │ └── Runner.js ├── index.html ├── jasmine.json ├── lib │ ├── benchmark_console_reporter.js │ ├── benchmark_reporter.css │ └── benchmark_reporter.js └── spec │ ├── commonjs │ └── requireSpec.js │ ├── coreBenchmark.js │ ├── coreSpec.mjs │ ├── duplexBenchmark.js │ ├── duplexSpec.mjs │ ├── json-patch-tests │ ├── .gitignore │ ├── README.md │ ├── spec_tests.json.mjs │ └── tests.json.mjs │ ├── jsonPatchTestsSpec.mjs │ ├── typings │ └── typingsSpec.ts │ ├── validateSpec.mjs │ └── webpack │ ├── importSpec.build.js │ └── importSpec.src.js ├── tsc-to-mjs.sh ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules/* 6 | npm-debug.log 7 | 8 | # VS Code 9 | .vscode/* 10 | !.vscode/settings.json 11 | !.vscode/tasks.json 12 | !.vscode/launch.json 13 | !.vscode/extensions.json 14 | *.code-workspace 15 | 16 | # Local History for Visual Studio Code 17 | .history/ 18 | 19 | # WebStorm 20 | .idea 21 | 22 | # Typescript 23 | commonjs/*.d.ts 24 | test/spec/typings/typingsSpec.js 25 | 26 | # SauceLabs logs 27 | *.log 28 | 29 | .artifacts 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | doc/ 3 | examples/ 4 | test/ 5 | .travis.yml 6 | !*.d.ts 7 | src/* 8 | tsconfig.json 9 | 10 | # Mac OS X 11 | .DS_Store 12 | 13 | # Node.js 14 | .npmignore 15 | node_modules/ 16 | npm-debug.log 17 | CONTRIBUTING.md 18 | 19 | # Git 20 | .git* 21 | 22 | # bower 23 | bower.json 24 | 25 | # grunt 26 | gruntfile.js 27 | 28 | #vs-code 29 | .vscode/* 30 | !.vscode/settings.json 31 | !.vscode/tasks.json 32 | !.vscode/launch.json 33 | !.vscode/extensions.json 34 | *.code-workspace 35 | 36 | # Local History for Visual Studio Code 37 | .history/ 38 | 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | node_js: node 4 | before_script: 5 | - npm install 6 | - npm run serve & 7 | script: 8 | - npm run test 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Making changes in TypeScript 4 | 5 | Make sure that you edit the source files in TypeScript (`*.ts`) files. 6 | 7 | To install the TypeScript compiler, run `npm install -g typescript`. 8 | 9 | You can build TypeScript to JavaScript using the [`TypeScript compiler`](https://www.typescriptlang.org/docs/tutorial.html) 10 | (and run it with npm script: `npm run tsc`). 11 | 12 | ## Testing 13 | 14 | ### In a web browser 15 | 16 | #### Testing 17 | 18 | - Load `test/` in your web browser 19 | 20 | Each of the test suite files contains *Jasmine* unit test suite and *Benchmark.js* performance test suite. 21 | 22 | To run *Benchmark.js* performance tests, press "Run Tests" button. 23 | 24 | ### In Node.js 25 | 26 | 1. Go to directory where you have cloned the repo 27 | 2. Install dev dependencies (Jasmine Node.js module) by running command `npm install` 28 | 3. Run test `npm run test` 29 | - Testing **`core`** only: `npm run test-core` 30 | - Testing **`duplex`** only: `npm run test-duplex` 31 | 4. Run test `npm run bench` (Please, consider performance when making any change) 32 | - Testing **`core*`* only: `npm run bench-core` 33 | - Testing **`duplex`** only: `npm run bench-duplex` 34 | 35 | 36 | ## Releasing a new version 37 | 38 | **The release is done from `master` branch.** 39 | 40 | 1. Don't break too much. See [how many projects depend on this](https://www.npmjs.com/browse/depended/fast-json-patch). 41 | 2. Make sure that the browser tests pass in Chrome, Firefox, Safari, Edge and IE11 42 | 3. Make sure that the NodeJS tests pass `npm install && npm run test` 43 | 4. Execute `npm run build` to transpile, bundle and minify. 44 | 5. Execute `npm version` like (`npm version [ major | minor | patch | premajor | preminor | prepatch | prerelease]`) 45 | 6. Call `git push` to push the changes to `origin master` 46 | 7. Call `git push --tags` to push the tag to `origin master` 47 | 8. Call `npm publish` to push the new version to NPM. [Read more](https://docs.npmjs.com/getting-started/publishing-npm-packages) 48 | 9. Call `npm view fast-json-patch dist-tags` to verify that the new version was published in NPM. 49 | 10. Explain the changes (at least a summary of the commit log) in [GitHub Releases](https://github.com/Starcounter-Jack/JSON-Patch/releases). 50 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013, 2014, 2020 Joachim Wester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON-Patch 2 | =============== 3 | 4 | > A leaner and meaner implementation of JSON-Patch. Small footprint. High performance. 5 | 6 | [![Build Status](https://travis-ci.org/Starcounter-Jack/JSON-Patch.svg?branch=master)](https://travis-ci.org/Starcounter-Jack/JSON-Patch) 7 | 8 | With JSON-Patch, you can: 9 | - **apply** patches (arrays) and single operations on JS object 10 | - **validate** a sequence of patches 11 | - **observe** for changes and **generate** patches when a change is detected 12 | - **compare** two objects to obtain the difference 13 | 14 | Tested in Firefox, Chrome, Edge, Safari, IE11, Deno and Node.js 15 | 16 | 17 | ## Why you should use JSON-Patch 18 | 19 | JSON-Patch [(RFC6902)](http://tools.ietf.org/html/rfc6902) is a standard format that 20 | allows you to update a JSON document by sending the changes rather than the whole document. 21 | JSON Patch plays well with the HTTP PATCH verb (method) and REST style programming. 22 | 23 | Mark Nottingham has a [nice blog]( http://www.mnot.net/blog/2012/09/05/patch) about it. 24 | 25 | 26 | ## Install 27 | 28 | [Download as ZIP](https://github.com/Starcounter-Jack/JSON-Patch/archive/master.zip) or install the current version using a package manager (and save it as a dependency): 29 | 30 | ```sh 31 | # NPM 32 | npm install fast-json-patch --save 33 | ``` 34 | 35 | 36 | ## Adding to your project 37 | 38 | ### In a web browser 39 | 40 | Load the bundled distribution script: 41 | 42 | ```html 43 | 44 | ``` 45 | 46 | In [browsers that support ECMAScript modules](https://caniuse.com/#feat=es6-module), the below code uses this library as a module: 47 | 48 | ```html 49 | 53 | ``` 54 | 55 | ### In Node.js 56 | 57 | In Node 12+ with `--experimental-modules` flag, the below code uses this library as an ECMAScript module: 58 | 59 | ```js 60 | import * as jsonpatch from 'fast-json-patch/index.mjs'; 61 | import { applyOperation } from 'fast-json-patch/index.mjs'; 62 | ``` 63 | 64 | In Webpack (and most surely other bundlers based on Babel), the below code uses this library as an ECMAScript module: 65 | 66 | ```js 67 | import * as jsonpatch from 'fast-json-patch'; 68 | import { applyOperation } from 'fast-json-patch'; 69 | ``` 70 | 71 | In standard Node, the below code uses this library as a CommonJS module: 72 | 73 | ```js 74 | const { applyOperation } = require('fast-json-patch'); 75 | const applyOperation = require('fast-json-patch').applyOperation; 76 | ``` 77 | 78 | ## Directories 79 | 80 | Directories used in this package: 81 | 82 | - `dist/` - contains ES5 files for a Web browser 83 | - `commonjs/` - contains CommonJS module and typings 84 | - `module/` - contains ECMAScript module and typings 85 | - `src/` - contains TypeScript source files 86 | 87 | ## API 88 | 89 | #### `function applyPatch(document: T, patch: Operation[], validateOperation?: boolean | Validator, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): PatchResult` 90 | 91 | Applies `patch` array on `obj`. 92 | 93 | - `document` The document to patch 94 | - `patch` a JSON-Patch array of operations to apply 95 | - `validateOperation` Boolean for whether to validate each operation with our default validator, or to pass a validator callback 96 | - `mutateDocument` Whether to mutate the original document or clone it before applying 97 | - `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. 98 | 99 | An invalid patch results in throwing an error (see `jsonpatch.validate` for more information about the error object). 100 | 101 | It modifies the `document` object and `patch` - it gets the values by reference. 102 | If you would like to avoid touching your `patch` array values, clone them: `jsonpatch.applyPatch(document, jsonpatch.deepClone(patch))`. 103 | 104 | Returns an array of [`OperationResult`](#operationresult-type) objects - one item for each item in `patches`, each item is an object `{newDocument: any, test?: boolean, removed?: any}`. 105 | 106 | * `test` - boolean result of the test 107 | * `remove`, `replace` and `move` - original object that has been removed 108 | * `add` (only when adding to an array) - index at which item has been inserted (useful when using `-` alias) 109 | 110 | - ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** 111 | - ** Note II: the returned array has `newDocument` property that you can use as the final state of the patched document **. 112 | - ** Note III: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. 113 | 114 | - See [Validation notes](#validation-notes). 115 | 116 | Example: 117 | 118 | ```js 119 | var document = { firstName: "Albert", contactDetails: { phoneNumbers: [] } }; 120 | var patch = [ 121 | { op: "replace", path: "/firstName", value: "Joachim" }, 122 | { op: "add", path: "/lastName", value: "Wester" }, 123 | { op: "add", path: "/contactDetails/phoneNumbers/0", value: { number: "555-123" } } 124 | ]; 125 | document = jsonpatch.applyPatch(document, patch).newDocument; 126 | // document == { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [{number:"555-123"}] } }; 127 | ``` 128 | 129 | #### `function applyOperation(document: T, operation: Operation, validateOperation: boolean | Validator = false, mutateDocument: boolean = true, banPrototypeModifications: boolean = true, index: number = 0): OperationResult` 130 | 131 | Applies single operation object `operation` on `document`. 132 | 133 | - `document` The document to patch 134 | - `operation` The operation to apply 135 | - `validateOperation` Whether to validate the operation, or to pass a validator callback 136 | - `mutateDocument` Whether to mutate the original document or clone it before applying 137 | - `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. 138 | - `index` The index of the operation in your patch array. Useful for better error reporting when that operation fails to apply. 139 | 140 | It modifies the `document` object and `operation` - it gets the values by reference. 141 | If you would like to avoid touching your values, clone them: `jsonpatch.applyOperation(document, jsonpatch.deepClone(operation))`. 142 | 143 | Returns an [`OperationResult`](#operationresult-type) object `{newDocument: any, test?: boolean, removed?: any}`. 144 | 145 | - ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** 146 | - ** Note II: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. 147 | 148 | - See [Validation notes](#validation-notes). 149 | 150 | Example: 151 | 152 | ```js 153 | var document = { firstName: "Albert", contactDetails: { phoneNumbers: [] } }; 154 | var operation = { op: "replace", path: "/firstName", value: "Joachim" }; 155 | document = jsonpatch.applyOperation(document, operation).newDocument; 156 | // document == { firstName: "Joachim", contactDetails: { phoneNumbers: [] }} 157 | ``` 158 | 159 | #### `jsonpatch.applyReducer(document: T, operation: Operation, index: number): T` 160 | 161 | **Ideal for `patch.reduce(jsonpatch.applyReducer, document)`**. 162 | 163 | Applies single operation object `operation` on `document`. 164 | 165 | Returns the a modified document. 166 | 167 | Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. 168 | 169 | Example: 170 | 171 | ```js 172 | var document = { firstName: "Albert", contactDetails: { phoneNumbers: [ ] } }; 173 | var patch = [ 174 | { op:"replace", path: "/firstName", value: "Joachim" }, 175 | { op:"add", path: "/lastName", value: "Wester" }, 176 | { op:"add", path: "/contactDetails/phoneNumbers/0", value: { number: "555-123" } } 177 | ]; 178 | var updatedDocument = patch.reduce(applyReducer, document); 179 | // updatedDocument == { firstName:"Joachim", lastName:"Wester", contactDetails:{ phoneNumbers[ {number:"555-123"} ] } }; 180 | ``` 181 | 182 | #### `jsonpatch.deepClone(value: any): any` 183 | 184 | Returns deeply cloned value. 185 | 186 | #### `jsonpatch.escapePathComponent(path: string): string` 187 | 188 | Returns the escaped path. 189 | 190 | #### `jsonpatch.unescapePathComponent(path: string): string` 191 | 192 | Returns the unescaped path. 193 | 194 | #### `jsonpatch.getValueByPointer(document: object, pointer: string)` 195 | 196 | Retrieves a value from a JSON document by a JSON pointer. 197 | 198 | Returns the value. 199 | 200 | #### `jsonpatch.observe(document: any, callback?: Function): Observer` 201 | 202 | Sets up an deep observer on `document` that listens for changes in object tree. When changes are detected, the optional 203 | callback is called with the generated patches array as the parameter. 204 | 205 | Returns `observer`. 206 | 207 | #### `jsonpatch.generate(document: any, observer: Observer, invertible = false): Operation[]` 208 | 209 | If there are pending changes in `obj`, returns them synchronously. If a `callback` was defined in `observe` 210 | method, it will be triggered synchronously as well. If `invertible` is true, then each change will be preceded by a test operation of the value before the change. 211 | 212 | If there are no pending changes in `obj`, returns an empty array (length 0). 213 | 214 | Example: 215 | 216 | ```js 217 | var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; 218 | var observer = jsonpatch.observe(document); 219 | document.firstName = "Albert"; 220 | document.contactDetails.phoneNumbers[0].number = "123"; 221 | document.contactDetails.phoneNumbers.push({ number:"456" }); 222 | var patch = jsonpatch.generate(observer); 223 | // patch == [ 224 | // { op: "replace", path: "/firstName", value: "Albert"}, 225 | // { op: "replace", path: "/contactDetails/phoneNumbers/0/number", value: "123" }, 226 | // { op: "add", path: "/contactDetails/phoneNumbers/1", value: {number:"456"}} 227 | // ]; 228 | ``` 229 | 230 | Example of generating patches with test operations for values in the first object: 231 | 232 | ```js 233 | var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; 234 | var observer = jsonpatch.observe(document); 235 | document.firstName = "Albert"; 236 | document.contactDetails.phoneNumbers[0].number = "123"; 237 | document.contactDetails.phoneNumbers.push({ number:"456" }); 238 | var patch = jsonpatch.generate(observer, true); 239 | // patch == [ 240 | // { op: "test", path: "/firstName", value: "Joachim"}, 241 | // { op: "replace", path: "/firstName", value: "Albert"}, 242 | // { op: "test", path: "/contactDetails/phoneNumbers/0/number", value: "555-123" }, 243 | // { op: "replace", path: "/contactDetails/phoneNumbers/0/number", value: "123" }, 244 | // { op: "add", path: "/contactDetails/phoneNumbers/1", value: {number:"456"}} 245 | // ]; 246 | ``` 247 | 248 | #### `jsonpatch.unobserve(document, observer)` 249 | ```typescript 250 | jsonpatch.unobserve(document: any, observer: Observer): void 251 | 252 | type JsonableObj = { [key:string]: Jsonable }; 253 | type JsonableArr = Jsonable[]; 254 | type Jsonable = JsonableArr | JsonableObj | string | number | boolean | null; 255 | ``` 256 | 257 | Destroys the observer set up on `document`. 258 | 259 | Any remaining changes are delivered synchronously (as in `jsonpatch.generate`). Note: this is different that ES6/7 `Object.unobserve`, which delivers remaining changes asynchronously. 260 | 261 | #### `jsonpatch.compare(document1, document2, invertible)` 262 | 263 | ```typescript 264 | jsonpatch.compare(document1: Jsonable, document2: Jsonable, invertible = false): Operation[] 265 | 266 | type JsonableObj = { [key:string]: Jsonable }; 267 | type JsonableArr = Jsonable[]; 268 | type Jsonable = JsonableArr | JsonableObj | string | number | boolean | null; 269 | ``` 270 | 271 | Compares object trees `document1` and `document2` and returns the difference relative to `document1` as a patches array. If `invertible` is true, then each change will be preceded by a test operation of the value in `document1`. 272 | 273 | If there are no differences, returns an empty array (length 0). 274 | 275 | Example: 276 | 277 | ```js 278 | var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; 279 | var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; 280 | var diff = jsonpatch.compare(documentA, documentB); 281 | //diff == [{op: "replace", path: "/user/lastName", value: "Collins"}] 282 | ``` 283 | 284 | Example of comparing two object trees with test operations for values in the first object: 285 | 286 | ```js 287 | var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; 288 | var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; 289 | var diff = jsonpatch.compare(documentA, documentB, true); 290 | //diff == [ 291 | // {op: "test", path: "/user/lastName", value: "Einstein"}, 292 | // {op: "replace", path: "/user/lastName", value: "Collins"} 293 | // ]; 294 | ``` 295 | 296 | #### `jsonpatch.validate(patch: Operation[], document?: any, validator?: Function): JsonPatchError` 297 | 298 | See [Validation notes](#validation-notes) 299 | 300 | Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object tree. 301 | 302 | If there are no errors, returns undefined. If there is an errors, returns a JsonPatchError object with the following properties: 303 | 304 | - `name` String - short error code 305 | - `message` String - long human readable error message 306 | - `index` Number - index of the operation in the sequence 307 | - `operation` Object - reference to the operation 308 | - `tree` Object - reference to the tree 309 | 310 | Possible errors: 311 | 312 | Error name | Error message 313 | ------------------------------|------------ 314 | SEQUENCE_NOT_AN_ARRAY | Patch sequence must be an array 315 | OPERATION_NOT_AN_OBJECT | Operation is not an object 316 | OPERATION_OP_INVALID | Operation `op` property is not one of operations defined in RFC-6902 317 | OPERATION_PATH_INVALID | Operation `path` property is not a valid string 318 | OPERATION_FROM_REQUIRED | Operation `from` property is not present (applicable in `move` and `copy` operations) 319 | OPERATION_VALUE_REQUIRED | Operation `value` property is not present, or `undefined` (applicable in `add`, `replace` and `test` operations) 320 | OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED | Operation `value` property object has at least one `undefined` value (applicable in `add`, `replace` and `test` operations) 321 | OPERATION_PATH_CANNOT_ADD | Cannot perform an `add` operation at the desired path 322 | OPERATION_PATH_UNRESOLVABLE | Cannot perform the operation at a path that does not exist 323 | OPERATION_FROM_UNRESOLVABLE | Cannot perform the operation from a path that does not exist 324 | OPERATION_PATH_ILLEGAL_ARRAY_INDEX | Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index 325 | OPERATION_VALUE_OUT_OF_BOUNDS | The specified index MUST NOT be greater than the number of elements in the array 326 | TEST_OPERATION_FAILED | When operation is `test` and the test fails, applies to `applyReducer`. 327 | 328 | Example: 329 | 330 | ```js 331 | var obj = {user: {firstName: "Albert"}}; 332 | var patches = [{op: "replace", path: "/user/firstName", value: "Albert"}, {op: "replace", path: "/user/lastName", value: "Einstein"}]; 333 | var errors = jsonpatch.validate(patches, obj); 334 | if (errors.length == 0) { 335 | //there are no errors! 336 | } 337 | else { 338 | for (var i=0; i < errors.length; i++) { 339 | if (!errors[i]) { 340 | console.log("Valid patch at index", i, patches[i]); 341 | } 342 | else { 343 | console.error("Invalid patch at index", i, errors[i], patches[i]); 344 | } 345 | } 346 | } 347 | ``` 348 | 349 | ## `OperationResult` Type 350 | 351 | Functions `applyPatch` and `applyOperation` both return `OperationResult` object. This object is: 352 | 353 | ```ts 354 | {newDocument: any, test?: boolean, removed?: any} 355 | ``` 356 | 357 | Where: 358 | 359 | - `newDocument`: the new state of the document after the patch/operation is applied. 360 | - `test`: if the operation was a `test` operation. This will be its result. 361 | - `removed`: contains the removed, moved, or replaced values from the document after a `remove`, `move` or `replace` operation. 362 | 363 | 364 | ## Validation Notes 365 | 366 | Functions `applyPatch`, `applyOperation`, and `validate` accept a `validate`/ `validator` parameter: 367 | 368 | - If the `validateOperation` parameter is set to `false`, validation will not occur. 369 | - If set to `true`, the patch is extensively validated before applying using jsonpatch's default validation. 370 | - If set to a `function` callback, the patch is validated using that function. 371 | 372 | If you pass a validator, it will be called with four parameters for each operation, `function(operation, index, tree, existingPath)` and it is expected to throw `JsonPatchError` when your conditions are not met. 373 | 374 | - `operation` The operation it self. 375 | - `index` `operation`'s index in the patch array (if application). 376 | - `tree` The object that is supposed to be patched. 377 | - `existingPath` the path `operation` points to. 378 | 379 | ## Overwriting and `move` Operation 380 | 381 | When the target of the move operation already exists, it is cached, deep cloned and returned as `removed` in `OperationResult`. 382 | 383 | ## `undefined`s (JS to JSON projection) 384 | 385 | As `undefined` type does not exist in JSON, it's also not a valid value of JSON Patch operation. Therefore `jsonpatch` will not generate JSON Patches that sets anything to `undefined`. 386 | 387 | Whenever a value is set to `undefined` in JS, JSON-Patch methods `generate` and `compare` will treat it similarly to how JavaScript method [`JSON.stringify` (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) treats them: 388 | 389 | > If `undefined` (...) is encountered during conversion it is either omitted (when it is found in an object) or censored to `null` (when it is found in an array). 390 | 391 | See the [ECMAScript spec](http://www.ecma-international.org/ecma-262/6.0/index.html#sec-json.stringify) for details. 392 | 393 | ## Specs/tests 394 | 395 | - [Run in browser](http://starcounter-jack.github.io/JSON-Patch/test/) 396 | 397 | ## [Contributing](CONTRIBUTING.md) 398 | 399 | ## Changelog 400 | 401 | To see the list of recent changes, see [Releases](https://github.com/Starcounter-Jack/JSON-Patch/releases). 402 | 403 | ## Footprint 404 | 4 KB minified and gzipped (12 KB minified) 405 | 406 | ## Performance 407 | 408 | ##### [`add` benchmark](https://run.perf.zone/view/JSON-Patch-Add-Operation-1535541298893) 409 | 410 | ![image](https://user-images.githubusercontent.com/17054134/44784357-aa422480-ab8d-11e8-8a7e-037e692dd842.png) 411 | 412 | ##### [`replace` benchmark](https://run.perf.zone/view/JSON-Patch-Replace-Operation-1535540952263) 413 | 414 | ![image](https://user-images.githubusercontent.com/17054134/44784275-5fc0a800-ab8d-11e8-8a90-e87b8d5409d0.png) 415 | 416 | Tested on 29.08.2018. Compared libraries: 417 | 418 | - [Starcounter-Jack/JSON-Patch](https://www.npmjs.com/package/fast-json-patch) 2.0.6 419 | - [bruth/jsonpatch-js](https://www.npmjs.com/package/json-patch) 0.7.0 420 | - [dharmafly/jsonpatch.js](https://www.npmjs.com/package/jsonpatch) 3.0.1 421 | - [jiff](https://www.npmjs.com/package/jiff) 0.7.3 422 | - [RFC6902](https://www.npmjs.com/package/rfc6902) 2.4.0 423 | 424 | We aim the tests to be fair. Our library puts performance as the #1 priority, while other libraries can have different priorities. If you'd like to update the benchmarks or add a library, please fork the [perf.zone](https://perf.zone) benchmarks linked above and open an issue to include new results. 425 | 426 | ## License 427 | 428 | MIT 429 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-json-patch", 3 | "version": "3.0.2", 4 | "homepage": "https://github.com/Starcounter-Jack/JSON-Patch", 5 | "description": "Fast implementation of JSON-Patch (RFC-6902) with duplex (observe changes) capabilities", 6 | "main": "dist/fast-json-patch.js", 7 | "keywords": ["json", "patch", "http", "rest"], 8 | "license": "MIT", 9 | "ignore": [ 10 | ".npmignore", 11 | ".gitignore", 12 | "test", 13 | ".travis.yml", 14 | "gruntfile.js", 15 | "node_modules", 16 | "package.json" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /commonjs/duplex.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { value: true }); 2 | /*! 3 | * https://github.com/Starcounter-Jack/JSON-Patch 4 | * (c) 2017-2021 Joachim Wester 5 | * MIT license 6 | */ 7 | var helpers_js_1 = require("./helpers.js"); 8 | var core_js_1 = require("./core.js"); 9 | var beforeDict = new WeakMap(); 10 | var Mirror = /** @class */ (function () { 11 | function Mirror(obj) { 12 | this.observers = new Map(); 13 | this.obj = obj; 14 | } 15 | return Mirror; 16 | }()); 17 | var ObserverInfo = /** @class */ (function () { 18 | function ObserverInfo(callback, observer) { 19 | this.callback = callback; 20 | this.observer = observer; 21 | } 22 | return ObserverInfo; 23 | }()); 24 | function getMirror(obj) { 25 | return beforeDict.get(obj); 26 | } 27 | function getObserverFromMirror(mirror, callback) { 28 | return mirror.observers.get(callback); 29 | } 30 | function removeObserverFromMirror(mirror, observer) { 31 | mirror.observers.delete(observer.callback); 32 | } 33 | /** 34 | * Detach an observer from an object 35 | */ 36 | function unobserve(root, observer) { 37 | observer.unobserve(); 38 | } 39 | exports.unobserve = unobserve; 40 | /** 41 | * Observes changes made to an object, which can then be retrieved using generate 42 | */ 43 | function observe(obj, callback) { 44 | var patches = []; 45 | var observer; 46 | var mirror = getMirror(obj); 47 | if (!mirror) { 48 | mirror = new Mirror(obj); 49 | beforeDict.set(obj, mirror); 50 | } 51 | else { 52 | var observerInfo = getObserverFromMirror(mirror, callback); 53 | observer = observerInfo && observerInfo.observer; 54 | } 55 | if (observer) { 56 | return observer; 57 | } 58 | observer = {}; 59 | mirror.value = helpers_js_1._deepClone(obj); 60 | if (callback) { 61 | observer.callback = callback; 62 | observer.next = null; 63 | var dirtyCheck = function () { 64 | generate(observer); 65 | }; 66 | var fastCheck = function () { 67 | clearTimeout(observer.next); 68 | observer.next = setTimeout(dirtyCheck); 69 | }; 70 | if (typeof window !== 'undefined') { //not Node 71 | window.addEventListener('mouseup', fastCheck); 72 | window.addEventListener('keyup', fastCheck); 73 | window.addEventListener('mousedown', fastCheck); 74 | window.addEventListener('keydown', fastCheck); 75 | window.addEventListener('change', fastCheck); 76 | } 77 | } 78 | observer.patches = patches; 79 | observer.object = obj; 80 | observer.unobserve = function () { 81 | generate(observer); 82 | clearTimeout(observer.next); 83 | removeObserverFromMirror(mirror, observer); 84 | if (typeof window !== 'undefined') { 85 | window.removeEventListener('mouseup', fastCheck); 86 | window.removeEventListener('keyup', fastCheck); 87 | window.removeEventListener('mousedown', fastCheck); 88 | window.removeEventListener('keydown', fastCheck); 89 | window.removeEventListener('change', fastCheck); 90 | } 91 | }; 92 | mirror.observers.set(callback, new ObserverInfo(callback, observer)); 93 | return observer; 94 | } 95 | exports.observe = observe; 96 | /** 97 | * Generate an array of patches from an observer 98 | */ 99 | function generate(observer, invertible) { 100 | if (invertible === void 0) { invertible = false; } 101 | var mirror = beforeDict.get(observer.object); 102 | _generate(mirror.value, observer.object, observer.patches, "", invertible); 103 | if (observer.patches.length) { 104 | core_js_1.applyPatch(mirror.value, observer.patches); 105 | } 106 | var temp = observer.patches; 107 | if (temp.length > 0) { 108 | observer.patches = []; 109 | if (observer.callback) { 110 | observer.callback(temp); 111 | } 112 | } 113 | return temp; 114 | } 115 | exports.generate = generate; 116 | // Dirty check if obj is different from mirror, generate patches and update mirror 117 | function _generate(mirror, obj, patches, path, invertible) { 118 | if (obj === mirror) { 119 | return; 120 | } 121 | if (typeof obj.toJSON === "function") { 122 | obj = obj.toJSON(); 123 | } 124 | var newKeys = helpers_js_1._objectKeys(obj); 125 | var oldKeys = helpers_js_1._objectKeys(mirror); 126 | var changed = false; 127 | var deleted = false; 128 | //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" 129 | for (var t = oldKeys.length - 1; t >= 0; t--) { 130 | var key = oldKeys[t]; 131 | var oldVal = mirror[key]; 132 | if (helpers_js_1.hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { 133 | var newVal = obj[key]; 134 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) { 135 | _generate(oldVal, newVal, patches, path + "/" + helpers_js_1.escapePathComponent(key), invertible); 136 | } 137 | else { 138 | if (oldVal !== newVal) { 139 | changed = true; 140 | if (invertible) { 141 | patches.push({ op: "test", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(oldVal) }); 142 | } 143 | patches.push({ op: "replace", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(newVal) }); 144 | } 145 | } 146 | } 147 | else if (Array.isArray(mirror) === Array.isArray(obj)) { 148 | if (invertible) { 149 | patches.push({ op: "test", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(oldVal) }); 150 | } 151 | patches.push({ op: "remove", path: path + "/" + helpers_js_1.escapePathComponent(key) }); 152 | deleted = true; // property has been deleted 153 | } 154 | else { 155 | if (invertible) { 156 | patches.push({ op: "test", path: path, value: mirror }); 157 | } 158 | patches.push({ op: "replace", path: path, value: obj }); 159 | changed = true; 160 | } 161 | } 162 | if (!deleted && newKeys.length == oldKeys.length) { 163 | return; 164 | } 165 | for (var t = 0; t < newKeys.length; t++) { 166 | var key = newKeys[t]; 167 | if (!helpers_js_1.hasOwnProperty(mirror, key) && obj[key] !== undefined) { 168 | patches.push({ op: "add", path: path + "/" + helpers_js_1.escapePathComponent(key), value: helpers_js_1._deepClone(obj[key]) }); 169 | } 170 | } 171 | } 172 | /** 173 | * Create an array of patches from the differences in two objects 174 | */ 175 | function compare(tree1, tree2, invertible) { 176 | if (invertible === void 0) { invertible = false; } 177 | var patches = []; 178 | _generate(tree1, tree2, patches, '', invertible); 179 | return patches; 180 | } 181 | exports.compare = compare; 182 | -------------------------------------------------------------------------------- /commonjs/helpers.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2022 Joachim Wester 4 | * MIT licensed 5 | */ 6 | var __extends = (this && this.__extends) || (function () { 7 | var extendStatics = function (d, b) { 8 | extendStatics = Object.setPrototypeOf || 9 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 10 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 11 | return extendStatics(d, b); 12 | }; 13 | return function (d, b) { 14 | extendStatics(d, b); 15 | function __() { this.constructor = d; } 16 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 17 | }; 18 | })(); 19 | Object.defineProperty(exports, "__esModule", { value: true }); 20 | var _hasOwnProperty = Object.prototype.hasOwnProperty; 21 | function hasOwnProperty(obj, key) { 22 | return _hasOwnProperty.call(obj, key); 23 | } 24 | exports.hasOwnProperty = hasOwnProperty; 25 | function _objectKeys(obj) { 26 | if (Array.isArray(obj)) { 27 | var keys_1 = new Array(obj.length); 28 | for (var k = 0; k < keys_1.length; k++) { 29 | keys_1[k] = "" + k; 30 | } 31 | return keys_1; 32 | } 33 | if (Object.keys) { 34 | return Object.keys(obj); 35 | } 36 | var keys = []; 37 | for (var i in obj) { 38 | if (hasOwnProperty(obj, i)) { 39 | keys.push(i); 40 | } 41 | } 42 | return keys; 43 | } 44 | exports._objectKeys = _objectKeys; 45 | ; 46 | /** 47 | * Deeply clone the object. 48 | * https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) 49 | * @param {any} obj value to clone 50 | * @return {any} cloned obj 51 | */ 52 | function _deepClone(obj) { 53 | switch (typeof obj) { 54 | case "object": 55 | return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 56 | case "undefined": 57 | return null; //this is how JSON.stringify behaves for array items 58 | default: 59 | return obj; //no need to clone primitives 60 | } 61 | } 62 | exports._deepClone = _deepClone; 63 | //3x faster than cached /^\d+$/.test(str) 64 | function isInteger(str) { 65 | var i = 0; 66 | var len = str.length; 67 | var charCode; 68 | while (i < len) { 69 | charCode = str.charCodeAt(i); 70 | if (charCode >= 48 && charCode <= 57) { 71 | i++; 72 | continue; 73 | } 74 | return false; 75 | } 76 | return true; 77 | } 78 | exports.isInteger = isInteger; 79 | /** 80 | * Escapes a json pointer path 81 | * @param path The raw pointer 82 | * @return the Escaped path 83 | */ 84 | function escapePathComponent(path) { 85 | if (path.indexOf('/') === -1 && path.indexOf('~') === -1) 86 | return path; 87 | return path.replace(/~/g, '~0').replace(/\//g, '~1'); 88 | } 89 | exports.escapePathComponent = escapePathComponent; 90 | /** 91 | * Unescapes a json pointer path 92 | * @param path The escaped pointer 93 | * @return The unescaped path 94 | */ 95 | function unescapePathComponent(path) { 96 | return path.replace(/~1/g, '/').replace(/~0/g, '~'); 97 | } 98 | exports.unescapePathComponent = unescapePathComponent; 99 | function _getPathRecursive(root, obj) { 100 | var found; 101 | for (var key in root) { 102 | if (hasOwnProperty(root, key)) { 103 | if (root[key] === obj) { 104 | return escapePathComponent(key) + '/'; 105 | } 106 | else if (typeof root[key] === 'object') { 107 | found = _getPathRecursive(root[key], obj); 108 | if (found != '') { 109 | return escapePathComponent(key) + '/' + found; 110 | } 111 | } 112 | } 113 | } 114 | return ''; 115 | } 116 | exports._getPathRecursive = _getPathRecursive; 117 | function getPath(root, obj) { 118 | if (root === obj) { 119 | return '/'; 120 | } 121 | var path = _getPathRecursive(root, obj); 122 | if (path === '') { 123 | throw new Error("Object not found in root"); 124 | } 125 | return "/" + path; 126 | } 127 | exports.getPath = getPath; 128 | /** 129 | * Recursively checks whether an object has any undefined values inside. 130 | */ 131 | function hasUndefined(obj) { 132 | if (obj === undefined) { 133 | return true; 134 | } 135 | if (obj) { 136 | if (Array.isArray(obj)) { 137 | for (var i_1 = 0, len = obj.length; i_1 < len; i_1++) { 138 | if (hasUndefined(obj[i_1])) { 139 | return true; 140 | } 141 | } 142 | } 143 | else if (typeof obj === "object") { 144 | var objKeys = _objectKeys(obj); 145 | var objKeysLength = objKeys.length; 146 | for (var i = 0; i < objKeysLength; i++) { 147 | if (hasUndefined(obj[objKeys[i]])) { 148 | return true; 149 | } 150 | } 151 | } 152 | } 153 | return false; 154 | } 155 | exports.hasUndefined = hasUndefined; 156 | function patchErrorMessageFormatter(message, args) { 157 | var messageParts = [message]; 158 | for (var key in args) { 159 | var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print 160 | if (typeof value !== 'undefined') { 161 | messageParts.push(key + ": " + value); 162 | } 163 | } 164 | return messageParts.join('\n'); 165 | } 166 | var PatchError = /** @class */ (function (_super) { 167 | __extends(PatchError, _super); 168 | function PatchError(message, name, index, operation, tree) { 169 | var _newTarget = this.constructor; 170 | var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; 171 | _this.name = name; 172 | _this.index = index; 173 | _this.operation = operation; 174 | _this.tree = tree; 175 | Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 176 | _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); 177 | return _this; 178 | } 179 | return PatchError; 180 | }(Error)); 181 | exports.PatchError = PatchError; 182 | -------------------------------------------------------------------------------- /dist/fast-json-patch.min.js: -------------------------------------------------------------------------------- 1 | /*! fast-json-patch, version: 3.1.1 */ 2 | var jsonpatch=function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=2)}([function(e,t){ 3 | /*! 4 | * https://github.com/Starcounter-Jack/JSON-Patch 5 | * (c) 2017-2022 Joachim Wester 6 | * MIT licensed 7 | */ 8 | var r,n=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0});var o=Object.prototype.hasOwnProperty;function a(e,t){return o.call(e,t)}function i(e){if(Array.isArray(e)){for(var t=new Array(e.length),r=0;r=48&&t<=57))return!1;r++}return!0},t.escapePathComponent=p,t.unescapePathComponent=function(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")},t._getPathRecursive=u,t.getPath=function(e,t){if(e===t)return"/";var r=u(e,t);if(""===r)throw new Error("Object not found in root");return"/"+r},t.hasUndefined=function e(t){if(void 0===t)return!0;if(t)if(Array.isArray(t)){for(var r=0,n=t.length;r0&&"constructor"==d[y-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(p&&void 0===_&&(void 0===v[O]?_=d.slice(0,y).join("/"):y==w-1&&(_=r.path),void 0!==_&&m(r,0,e,_)),y++,Array.isArray(v)){if("-"===O)O=v.length;else{if(p&&!n.isInteger(O))throw new t.JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",h,r,e);n.isInteger(O)&&(O=~~O)}if(y>=w){if(p&&"add"===r.op&&O>v.length)throw new t.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",h,r,e);if(!1===(l=a[r.op].call(r,v,O,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",h,r,e);return l}}else if(y>=w){if(!1===(l=o[r.op].call(r,v,O,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",h,r,e);return l}if(v=v[O],p&&y0)throw new t.JsonPatchError('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",r,e,a);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new t.JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",r,e,a);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",r,e,a);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&n.hasUndefined(e.value))throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",r,e,a);if(a)if("add"==e.op){var p=e.path.split("/").length,u=i.split("/").length;if(p!==u+1&&p!==u)throw new t.JsonPatchError("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",r,e,a)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==i)throw new t.JsonPatchError("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",r,e,a)}else if("move"===e.op||"copy"===e.op){var s=c([{op:"_get",path:e.from,value:void 0}],a);if(s&&"OPERATION_PATH_UNRESOLVABLE"===s.name)throw new t.JsonPatchError("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",r,e,a)}}function c(e,r,o){try{if(!Array.isArray(e))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(r)u(n._deepClone(r),n._deepClone(e),o||!0);else{o=o||s;for(var a=0;a0&&(e.patches=[],e.callback&&e.callback(n)),n}function s(e,t,r,o,a){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=n._objectKeys(t),p=n._objectKeys(e),u=!1,c=p.length-1;c>=0;c--){var f=e[l=p[c]];if(!n.hasOwnProperty(t,l)||void 0===t[l]&&void 0!==f&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(a&&r.push({op:"test",path:o+"/"+n.escapePathComponent(l),value:n._deepClone(f)}),r.push({op:"remove",path:o+"/"+n.escapePathComponent(l)}),u=!0):(a&&r.push({op:"test",path:o,value:e}),r.push({op:"replace",path:o,value:t}),!0);else{var h=t[l];"object"==typeof f&&null!=f&&"object"==typeof h&&null!=h&&Array.isArray(f)===Array.isArray(h)?s(f,h,r,o+"/"+n.escapePathComponent(l),a):f!==h&&(!0,a&&r.push({op:"test",path:o+"/"+n.escapePathComponent(l),value:n._deepClone(f)}),r.push({op:"replace",path:o+"/"+n.escapePathComponent(l),value:n._deepClone(h)}))}}if(u||i.length!=p.length)for(c=0;c(root: T, observer: duplex.Observer): void; 23 | observe(obj: Object | T[], callback?: (patches: core.Operation[]) => void): duplex.Observer; 24 | generate(observer: duplex.Observer, invertible?: boolean): core.Operation[]; 25 | compare(tree1: Object | any[], tree2: Object | any[], invertible?: boolean): core.Operation[]; 26 | getValueByPointer(document: any, pointer: string): any; 27 | applyOperation(document: T, operation: core.Operation, validateOperation?: boolean | core.Validator, mutateDocument?: boolean, banPrototypeModifications?: boolean, index?: number): core.OperationResult; 28 | applyPatch(document: T, patch: core.Operation[], validateOperation?: boolean | core.Validator, mutateDocument?: boolean, banPrototypeModifications?: boolean): core.PatchResult; 29 | applyReducer(document: T, operation: core.Operation, index: number): T; 30 | validator(operation: core.Operation, index: number, document?: any, existingPathFragment?: string): void; 31 | validate(sequence: core.Operation[], document?: T, externalValidator?: core.Validator): JsonPatchError; 32 | _areEquals(a: any, b: any): boolean; 33 | }; 34 | export default _default; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var core = require("./commonjs/core.js"); 2 | Object.assign(exports, core); 3 | 4 | var duplex = require("./commonjs/duplex.js"); 5 | Object.assign(exports, duplex); 6 | 7 | var helpers = require("./commonjs/helpers.js"); 8 | exports.JsonPatchError = helpers.PatchError; 9 | exports.deepClone = helpers._deepClone; 10 | exports.escapePathComponent = helpers.escapePathComponent; 11 | exports.unescapePathComponent = helpers.unescapePathComponent; 12 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | export * from './module/core.mjs'; 2 | export * from './module/duplex.mjs'; 3 | export { 4 | PatchError as JsonPatchError, 5 | _deepClone as deepClone, 6 | escapePathComponent, 7 | unescapePathComponent 8 | } from './module/helpers.mjs'; 9 | 10 | 11 | /** 12 | * Default export for backwards compat 13 | */ 14 | 15 | import * as core from './module/core.mjs'; 16 | import * as duplex from './module/duplex.mjs'; 17 | import { 18 | PatchError as JsonPatchError, 19 | _deepClone as deepClone, 20 | escapePathComponent, 21 | unescapePathComponent 22 | } from './module/helpers.mjs'; 23 | 24 | export default Object.assign({}, core, duplex, { 25 | JsonPatchError, 26 | deepClone, 27 | escapePathComponent, 28 | unescapePathComponent 29 | }); -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/core'; 2 | export * from './src/duplex'; 3 | export { 4 | PatchError as JsonPatchError, 5 | _deepClone as deepClone, 6 | escapePathComponent, 7 | unescapePathComponent 8 | } from './src/helpers'; 9 | 10 | 11 | /** 12 | * Default export for backwards compat 13 | */ 14 | 15 | import * as core from './src/core'; 16 | import * as duplex from './src/duplex'; 17 | import { 18 | PatchError as JsonPatchError, 19 | _deepClone as deepClone, 20 | escapePathComponent, 21 | unescapePathComponent 22 | } from './src/helpers'; 23 | 24 | export default { 25 | ...core, 26 | ...duplex, 27 | JsonPatchError, 28 | deepClone, 29 | escapePathComponent, 30 | unescapePathComponent 31 | } -------------------------------------------------------------------------------- /jasmine-run.mjs: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import Jasmine from 'jasmine'; 3 | 4 | const jasmine = new Jasmine(); 5 | jasmine.loadConfigFile('test/jasmine.json'); 6 | 7 | const pattern = process.argv[2] || 'test/spec/*.js'; 8 | 9 | // Load your specs 10 | glob(pattern, function (er, files) { 11 | Promise.all( 12 | files 13 | // Use relative paths 14 | .map(f => f.replace(/^([^\/])/, './$1')) 15 | .map(f => import(f) 16 | .catch(e => { 17 | console.error('** Error loading ' + f + ': '); 18 | console.error(e); 19 | process.exit(1); 20 | })) 21 | ) 22 | .then(() => jasmine.execute()); 23 | }); -------------------------------------------------------------------------------- /module/core.d.ts: -------------------------------------------------------------------------------- 1 | import { PatchError, _deepClone } from './helpers.js'; 2 | export declare const JsonPatchError: typeof PatchError; 3 | export declare const deepClone: typeof _deepClone; 4 | export declare type Operation = AddOperation | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation; 5 | export interface Validator { 6 | (operation: Operation, index: number, document: T, existingPathFragment: string): void; 7 | } 8 | export interface OperationResult { 9 | removed?: any; 10 | test?: boolean; 11 | newDocument: T; 12 | } 13 | export interface BaseOperation { 14 | path: string; 15 | } 16 | export interface AddOperation extends BaseOperation { 17 | op: 'add'; 18 | value: T; 19 | } 20 | export interface RemoveOperation extends BaseOperation { 21 | op: 'remove'; 22 | } 23 | export interface ReplaceOperation extends BaseOperation { 24 | op: 'replace'; 25 | value: T; 26 | } 27 | export interface MoveOperation extends BaseOperation { 28 | op: 'move'; 29 | from: string; 30 | } 31 | export interface CopyOperation extends BaseOperation { 32 | op: 'copy'; 33 | from: string; 34 | } 35 | export interface TestOperation extends BaseOperation { 36 | op: 'test'; 37 | value: T; 38 | } 39 | export interface GetOperation extends BaseOperation { 40 | op: '_get'; 41 | value: T; 42 | } 43 | export interface PatchResult extends Array> { 44 | newDocument: T; 45 | } 46 | /** 47 | * Retrieves a value from a JSON document by a JSON pointer. 48 | * Returns the value. 49 | * 50 | * @param document The document to get the value from 51 | * @param pointer an escaped JSON pointer 52 | * @return The retrieved value 53 | */ 54 | export declare function getValueByPointer(document: any, pointer: string): any; 55 | /** 56 | * Apply a single JSON Patch Operation on a JSON document. 57 | * Returns the {newDocument, result} of the operation. 58 | * It modifies the `document` and `operation` objects - it gets the values by reference. 59 | * If you would like to avoid touching your values, clone them: 60 | * `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`. 61 | * 62 | * @param document The document to patch 63 | * @param operation The operation to apply 64 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 65 | * @param mutateDocument Whether to mutate the original document or clone it before applying 66 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 67 | * @return `{newDocument, result}` after the operation 68 | */ 69 | export declare function applyOperation(document: T, operation: Operation, validateOperation?: boolean | Validator, mutateDocument?: boolean, banPrototypeModifications?: boolean, index?: number): OperationResult; 70 | /** 71 | * Apply a full JSON Patch array on a JSON document. 72 | * Returns the {newDocument, result} of the patch. 73 | * It modifies the `document` object and `patch` - it gets the values by reference. 74 | * If you would like to avoid touching your values, clone them: 75 | * `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`. 76 | * 77 | * @param document The document to patch 78 | * @param patch The patch to apply 79 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 80 | * @param mutateDocument Whether to mutate the original document or clone it before applying 81 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 82 | * @return An array of `{newDocument, result}` after the patch 83 | */ 84 | export declare function applyPatch(document: T, patch: ReadonlyArray, validateOperation?: boolean | Validator, mutateDocument?: boolean, banPrototypeModifications?: boolean): PatchResult; 85 | /** 86 | * Apply a single JSON Patch Operation on a JSON document. 87 | * Returns the updated document. 88 | * Suitable as a reducer. 89 | * 90 | * @param document The document to patch 91 | * @param operation The operation to apply 92 | * @return The updated document 93 | */ 94 | export declare function applyReducer(document: T, operation: Operation, index: number): T; 95 | /** 96 | * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error. 97 | * @param {object} operation - operation object (patch) 98 | * @param {number} index - index of operation in the sequence 99 | * @param {object} [document] - object where the operation is supposed to be applied 100 | * @param {string} [existingPathFragment] - comes along with `document` 101 | */ 102 | export declare function validator(operation: Operation, index: number, document?: any, existingPathFragment?: string): void; 103 | /** 104 | * Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document. 105 | * If error is encountered, returns a JsonPatchError object 106 | * @param sequence 107 | * @param document 108 | * @returns {JsonPatchError|undefined} 109 | */ 110 | export declare function validate(sequence: ReadonlyArray, document?: T, externalValidator?: Validator): PatchError; 111 | export declare function _areEquals(a: any, b: any): boolean; 112 | -------------------------------------------------------------------------------- /module/core.mjs: -------------------------------------------------------------------------------- 1 | import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined } from './helpers.mjs'; 2 | export var JsonPatchError = PatchError; 3 | export var deepClone = _deepClone; 4 | /* We use a Javascript hash to store each 5 | function. Each hash entry (property) uses 6 | the operation identifiers specified in rfc6902. 7 | In this way, we can map each patch operation 8 | to its dedicated function in efficient way. 9 | */ 10 | /* The operations applicable to an object */ 11 | var objOps = { 12 | add: function (obj, key, document) { 13 | obj[key] = this.value; 14 | return { newDocument: document }; 15 | }, 16 | remove: function (obj, key, document) { 17 | var removed = obj[key]; 18 | delete obj[key]; 19 | return { newDocument: document, removed: removed }; 20 | }, 21 | replace: function (obj, key, document) { 22 | var removed = obj[key]; 23 | obj[key] = this.value; 24 | return { newDocument: document, removed: removed }; 25 | }, 26 | move: function (obj, key, document) { 27 | /* in case move target overwrites an existing value, 28 | return the removed value, this can be taxing performance-wise, 29 | and is potentially unneeded */ 30 | var removed = getValueByPointer(document, this.path); 31 | if (removed) { 32 | removed = _deepClone(removed); 33 | } 34 | var originalValue = applyOperation(document, { op: "remove", path: this.from }).removed; 35 | applyOperation(document, { op: "add", path: this.path, value: originalValue }); 36 | return { newDocument: document, removed: removed }; 37 | }, 38 | copy: function (obj, key, document) { 39 | var valueToCopy = getValueByPointer(document, this.from); 40 | // enforce copy by value so further operations don't affect source (see issue #177) 41 | applyOperation(document, { op: "add", path: this.path, value: _deepClone(valueToCopy) }); 42 | return { newDocument: document }; 43 | }, 44 | test: function (obj, key, document) { 45 | return { newDocument: document, test: _areEquals(obj[key], this.value) }; 46 | }, 47 | _get: function (obj, key, document) { 48 | this.value = obj[key]; 49 | return { newDocument: document }; 50 | } 51 | }; 52 | /* The operations applicable to an array. Many are the same as for the object */ 53 | var arrOps = { 54 | add: function (arr, i, document) { 55 | if (isInteger(i)) { 56 | arr.splice(i, 0, this.value); 57 | } 58 | else { // array props 59 | arr[i] = this.value; 60 | } 61 | // this may be needed when using '-' in an array 62 | return { newDocument: document, index: i }; 63 | }, 64 | remove: function (arr, i, document) { 65 | var removedList = arr.splice(i, 1); 66 | return { newDocument: document, removed: removedList[0] }; 67 | }, 68 | replace: function (arr, i, document) { 69 | var removed = arr[i]; 70 | arr[i] = this.value; 71 | return { newDocument: document, removed: removed }; 72 | }, 73 | move: objOps.move, 74 | copy: objOps.copy, 75 | test: objOps.test, 76 | _get: objOps._get 77 | }; 78 | /** 79 | * Retrieves a value from a JSON document by a JSON pointer. 80 | * Returns the value. 81 | * 82 | * @param document The document to get the value from 83 | * @param pointer an escaped JSON pointer 84 | * @return The retrieved value 85 | */ 86 | export function getValueByPointer(document, pointer) { 87 | if (pointer == '') { 88 | return document; 89 | } 90 | var getOriginalDestination = { op: "_get", path: pointer }; 91 | applyOperation(document, getOriginalDestination); 92 | return getOriginalDestination.value; 93 | } 94 | /** 95 | * Apply a single JSON Patch Operation on a JSON document. 96 | * Returns the {newDocument, result} of the operation. 97 | * It modifies the `document` and `operation` objects - it gets the values by reference. 98 | * If you would like to avoid touching your values, clone them: 99 | * `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`. 100 | * 101 | * @param document The document to patch 102 | * @param operation The operation to apply 103 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 104 | * @param mutateDocument Whether to mutate the original document or clone it before applying 105 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 106 | * @return `{newDocument, result}` after the operation 107 | */ 108 | export function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications, index) { 109 | if (validateOperation === void 0) { validateOperation = false; } 110 | if (mutateDocument === void 0) { mutateDocument = true; } 111 | if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } 112 | if (index === void 0) { index = 0; } 113 | if (validateOperation) { 114 | if (typeof validateOperation == 'function') { 115 | validateOperation(operation, 0, document, operation.path); 116 | } 117 | else { 118 | validator(operation, 0); 119 | } 120 | } 121 | /* ROOT OPERATIONS */ 122 | if (operation.path === "") { 123 | var returnValue = { newDocument: document }; 124 | if (operation.op === 'add') { 125 | returnValue.newDocument = operation.value; 126 | return returnValue; 127 | } 128 | else if (operation.op === 'replace') { 129 | returnValue.newDocument = operation.value; 130 | returnValue.removed = document; //document we removed 131 | return returnValue; 132 | } 133 | else if (operation.op === 'move' || operation.op === 'copy') { // it's a move or copy to root 134 | returnValue.newDocument = getValueByPointer(document, operation.from); // get the value by json-pointer in `from` field 135 | if (operation.op === 'move') { // report removed item 136 | returnValue.removed = document; 137 | } 138 | return returnValue; 139 | } 140 | else if (operation.op === 'test') { 141 | returnValue.test = _areEquals(document, operation.value); 142 | if (returnValue.test === false) { 143 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 144 | } 145 | returnValue.newDocument = document; 146 | return returnValue; 147 | } 148 | else if (operation.op === 'remove') { // a remove on root 149 | returnValue.removed = document; 150 | returnValue.newDocument = null; 151 | return returnValue; 152 | } 153 | else if (operation.op === '_get') { 154 | operation.value = document; 155 | return returnValue; 156 | } 157 | else { /* bad operation */ 158 | if (validateOperation) { 159 | throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); 160 | } 161 | else { 162 | return returnValue; 163 | } 164 | } 165 | } /* END ROOT OPERATIONS */ 166 | else { 167 | if (!mutateDocument) { 168 | document = _deepClone(document); 169 | } 170 | var path = operation.path || ""; 171 | var keys = path.split('/'); 172 | var obj = document; 173 | var t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift 174 | var len = keys.length; 175 | var existingPathFragment = undefined; 176 | var key = void 0; 177 | var validateFunction = void 0; 178 | if (typeof validateOperation == 'function') { 179 | validateFunction = validateOperation; 180 | } 181 | else { 182 | validateFunction = validator; 183 | } 184 | while (true) { 185 | key = keys[t]; 186 | if (key && key.indexOf('~') != -1) { 187 | key = unescapePathComponent(key); 188 | } 189 | if (banPrototypeModifications && 190 | (key == '__proto__' || 191 | (key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) { 192 | throw new TypeError('JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'); 193 | } 194 | if (validateOperation) { 195 | if (existingPathFragment === undefined) { 196 | if (obj[key] === undefined) { 197 | existingPathFragment = keys.slice(0, t).join('/'); 198 | } 199 | else if (t == len - 1) { 200 | existingPathFragment = operation.path; 201 | } 202 | if (existingPathFragment !== undefined) { 203 | validateFunction(operation, 0, document, existingPathFragment); 204 | } 205 | } 206 | } 207 | t++; 208 | if (Array.isArray(obj)) { 209 | if (key === '-') { 210 | key = obj.length; 211 | } 212 | else { 213 | if (validateOperation && !isInteger(key)) { 214 | throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document); 215 | } // only parse key when it's an integer for `arr.prop` to work 216 | else if (isInteger(key)) { 217 | key = ~~key; 218 | } 219 | } 220 | if (t >= len) { 221 | if (validateOperation && operation.op === "add" && key > obj.length) { 222 | throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); 223 | } 224 | var returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch 225 | if (returnValue.test === false) { 226 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 227 | } 228 | return returnValue; 229 | } 230 | } 231 | else { 232 | if (t >= len) { 233 | var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch 234 | if (returnValue.test === false) { 235 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 236 | } 237 | return returnValue; 238 | } 239 | } 240 | obj = obj[key]; 241 | // If we have more keys in the path, but the next value isn't a non-null object, 242 | // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. 243 | if (validateOperation && t < len && (!obj || typeof obj !== "object")) { 244 | throw new JsonPatchError('Cannot perform operation at the desired path', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); 245 | } 246 | } 247 | } 248 | } 249 | /** 250 | * Apply a full JSON Patch array on a JSON document. 251 | * Returns the {newDocument, result} of the patch. 252 | * It modifies the `document` object and `patch` - it gets the values by reference. 253 | * If you would like to avoid touching your values, clone them: 254 | * `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`. 255 | * 256 | * @param document The document to patch 257 | * @param patch The patch to apply 258 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 259 | * @param mutateDocument Whether to mutate the original document or clone it before applying 260 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 261 | * @return An array of `{newDocument, result}` after the patch 262 | */ 263 | export function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) { 264 | if (mutateDocument === void 0) { mutateDocument = true; } 265 | if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } 266 | if (validateOperation) { 267 | if (!Array.isArray(patch)) { 268 | throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); 269 | } 270 | } 271 | if (!mutateDocument) { 272 | document = _deepClone(document); 273 | } 274 | var results = new Array(patch.length); 275 | for (var i = 0, length_1 = patch.length; i < length_1; i++) { 276 | // we don't need to pass mutateDocument argument because if it was true, we already deep cloned the object, we'll just pass `true` 277 | results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i); 278 | document = results[i].newDocument; // in case root was replaced 279 | } 280 | results.newDocument = document; 281 | return results; 282 | } 283 | /** 284 | * Apply a single JSON Patch Operation on a JSON document. 285 | * Returns the updated document. 286 | * Suitable as a reducer. 287 | * 288 | * @param document The document to patch 289 | * @param operation The operation to apply 290 | * @return The updated document 291 | */ 292 | export function applyReducer(document, operation, index) { 293 | var operationResult = applyOperation(document, operation); 294 | if (operationResult.test === false) { // failed test 295 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 296 | } 297 | return operationResult.newDocument; 298 | } 299 | /** 300 | * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error. 301 | * @param {object} operation - operation object (patch) 302 | * @param {number} index - index of operation in the sequence 303 | * @param {object} [document] - object where the operation is supposed to be applied 304 | * @param {string} [existingPathFragment] - comes along with `document` 305 | */ 306 | export function validator(operation, index, document, existingPathFragment) { 307 | if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { 308 | throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); 309 | } 310 | else if (!objOps[operation.op]) { 311 | throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); 312 | } 313 | else if (typeof operation.path !== 'string') { 314 | throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); 315 | } 316 | else if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { 317 | // paths that aren't empty string should start with "/" 318 | throw new JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); 319 | } 320 | else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') { 321 | throw new JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, document); 322 | } 323 | else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) { 324 | throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, document); 325 | } 326 | else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && hasUndefined(operation.value)) { 327 | throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, document); 328 | } 329 | else if (document) { 330 | if (operation.op == "add") { 331 | var pathLen = operation.path.split("/").length; 332 | var existingPathLen = existingPathFragment.split("/").length; 333 | if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) { 334 | throw new JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, document); 335 | } 336 | } 337 | else if (operation.op === 'replace' || operation.op === 'remove' || operation.op === '_get') { 338 | if (operation.path !== existingPathFragment) { 339 | throw new JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); 340 | } 341 | } 342 | else if (operation.op === 'move' || operation.op === 'copy') { 343 | var existingValue = { op: "_get", path: operation.from, value: undefined }; 344 | var error = validate([existingValue], document); 345 | if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') { 346 | throw new JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, document); 347 | } 348 | } 349 | } 350 | } 351 | /** 352 | * Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document. 353 | * If error is encountered, returns a JsonPatchError object 354 | * @param sequence 355 | * @param document 356 | * @returns {JsonPatchError|undefined} 357 | */ 358 | export function validate(sequence, document, externalValidator) { 359 | try { 360 | if (!Array.isArray(sequence)) { 361 | throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); 362 | } 363 | if (document) { 364 | //clone document and sequence so that we can safely try applying operations 365 | applyPatch(_deepClone(document), _deepClone(sequence), externalValidator || true); 366 | } 367 | else { 368 | externalValidator = externalValidator || validator; 369 | for (var i = 0; i < sequence.length; i++) { 370 | externalValidator(sequence[i], i, document, undefined); 371 | } 372 | } 373 | } 374 | catch (e) { 375 | if (e instanceof JsonPatchError) { 376 | return e; 377 | } 378 | else { 379 | throw e; 380 | } 381 | } 382 | } 383 | // based on https://github.com/epoberezkin/fast-deep-equal 384 | // MIT License 385 | // Copyright (c) 2017 Evgeny Poberezkin 386 | // Permission is hereby granted, free of charge, to any person obtaining a copy 387 | // of this software and associated documentation files (the "Software"), to deal 388 | // in the Software without restriction, including without limitation the rights 389 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 390 | // copies of the Software, and to permit persons to whom the Software is 391 | // furnished to do so, subject to the following conditions: 392 | // The above copyright notice and this permission notice shall be included in all 393 | // copies or substantial portions of the Software. 394 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 395 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 396 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 397 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 398 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 399 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 400 | // SOFTWARE. 401 | export function _areEquals(a, b) { 402 | if (a === b) 403 | return true; 404 | if (a && b && typeof a == 'object' && typeof b == 'object') { 405 | var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key; 406 | if (arrA && arrB) { 407 | length = a.length; 408 | if (length != b.length) 409 | return false; 410 | for (i = length; i-- !== 0;) 411 | if (!_areEquals(a[i], b[i])) 412 | return false; 413 | return true; 414 | } 415 | if (arrA != arrB) 416 | return false; 417 | var keys = Object.keys(a); 418 | length = keys.length; 419 | if (length !== Object.keys(b).length) 420 | return false; 421 | for (i = length; i-- !== 0;) 422 | if (!b.hasOwnProperty(keys[i])) 423 | return false; 424 | for (i = length; i-- !== 0;) { 425 | key = keys[i]; 426 | if (!_areEquals(a[key], b[key])) 427 | return false; 428 | } 429 | return true; 430 | } 431 | return a !== a && b !== b; 432 | } 433 | ; 434 | -------------------------------------------------------------------------------- /module/duplex.d.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from './core.js'; 2 | export interface Observer { 3 | object: T; 4 | patches: Operation[]; 5 | unobserve: () => void; 6 | callback: (patches: Operation[]) => void; 7 | } 8 | /** 9 | * Detach an observer from an object 10 | */ 11 | export declare function unobserve(root: T, observer: Observer): void; 12 | /** 13 | * Observes changes made to an object, which can then be retrieved using generate 14 | */ 15 | export declare function observe(obj: Object | Array, callback?: (patches: Operation[]) => void): Observer; 16 | /** 17 | * Generate an array of patches from an observer 18 | */ 19 | export declare function generate(observer: Observer, invertible?: boolean): Operation[]; 20 | /** 21 | * Create an array of patches from the differences in two objects 22 | */ 23 | export declare function compare(tree1: Object | Array, tree2: Object | Array, invertible?: boolean): Operation[]; 24 | -------------------------------------------------------------------------------- /module/duplex.mjs: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2021 Joachim Wester 4 | * MIT license 5 | */ 6 | import { _deepClone, _objectKeys, escapePathComponent, hasOwnProperty } from './helpers.mjs'; 7 | import { applyPatch } from './core.mjs'; 8 | var beforeDict = new WeakMap(); 9 | var Mirror = /** @class */ (function () { 10 | function Mirror(obj) { 11 | this.observers = new Map(); 12 | this.obj = obj; 13 | } 14 | return Mirror; 15 | }()); 16 | var ObserverInfo = /** @class */ (function () { 17 | function ObserverInfo(callback, observer) { 18 | this.callback = callback; 19 | this.observer = observer; 20 | } 21 | return ObserverInfo; 22 | }()); 23 | function getMirror(obj) { 24 | return beforeDict.get(obj); 25 | } 26 | function getObserverFromMirror(mirror, callback) { 27 | return mirror.observers.get(callback); 28 | } 29 | function removeObserverFromMirror(mirror, observer) { 30 | mirror.observers.delete(observer.callback); 31 | } 32 | /** 33 | * Detach an observer from an object 34 | */ 35 | export function unobserve(root, observer) { 36 | observer.unobserve(); 37 | } 38 | /** 39 | * Observes changes made to an object, which can then be retrieved using generate 40 | */ 41 | export function observe(obj, callback) { 42 | var patches = []; 43 | var observer; 44 | var mirror = getMirror(obj); 45 | if (!mirror) { 46 | mirror = new Mirror(obj); 47 | beforeDict.set(obj, mirror); 48 | } 49 | else { 50 | var observerInfo = getObserverFromMirror(mirror, callback); 51 | observer = observerInfo && observerInfo.observer; 52 | } 53 | if (observer) { 54 | return observer; 55 | } 56 | observer = {}; 57 | mirror.value = _deepClone(obj); 58 | if (callback) { 59 | observer.callback = callback; 60 | observer.next = null; 61 | var dirtyCheck = function () { 62 | generate(observer); 63 | }; 64 | var fastCheck = function () { 65 | clearTimeout(observer.next); 66 | observer.next = setTimeout(dirtyCheck); 67 | }; 68 | if (typeof window !== 'undefined') { //not Node 69 | window.addEventListener('mouseup', fastCheck); 70 | window.addEventListener('keyup', fastCheck); 71 | window.addEventListener('mousedown', fastCheck); 72 | window.addEventListener('keydown', fastCheck); 73 | window.addEventListener('change', fastCheck); 74 | } 75 | } 76 | observer.patches = patches; 77 | observer.object = obj; 78 | observer.unobserve = function () { 79 | generate(observer); 80 | clearTimeout(observer.next); 81 | removeObserverFromMirror(mirror, observer); 82 | if (typeof window !== 'undefined') { 83 | window.removeEventListener('mouseup', fastCheck); 84 | window.removeEventListener('keyup', fastCheck); 85 | window.removeEventListener('mousedown', fastCheck); 86 | window.removeEventListener('keydown', fastCheck); 87 | window.removeEventListener('change', fastCheck); 88 | } 89 | }; 90 | mirror.observers.set(callback, new ObserverInfo(callback, observer)); 91 | return observer; 92 | } 93 | /** 94 | * Generate an array of patches from an observer 95 | */ 96 | export function generate(observer, invertible) { 97 | if (invertible === void 0) { invertible = false; } 98 | var mirror = beforeDict.get(observer.object); 99 | _generate(mirror.value, observer.object, observer.patches, "", invertible); 100 | if (observer.patches.length) { 101 | applyPatch(mirror.value, observer.patches); 102 | } 103 | var temp = observer.patches; 104 | if (temp.length > 0) { 105 | observer.patches = []; 106 | if (observer.callback) { 107 | observer.callback(temp); 108 | } 109 | } 110 | return temp; 111 | } 112 | // Dirty check if obj is different from mirror, generate patches and update mirror 113 | function _generate(mirror, obj, patches, path, invertible) { 114 | if (obj === mirror) { 115 | return; 116 | } 117 | if (typeof obj.toJSON === "function") { 118 | obj = obj.toJSON(); 119 | } 120 | var newKeys = _objectKeys(obj); 121 | var oldKeys = _objectKeys(mirror); 122 | var changed = false; 123 | var deleted = false; 124 | //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" 125 | for (var t = oldKeys.length - 1; t >= 0; t--) { 126 | var key = oldKeys[t]; 127 | var oldVal = mirror[key]; 128 | if (hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { 129 | var newVal = obj[key]; 130 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) { 131 | _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible); 132 | } 133 | else { 134 | if (oldVal !== newVal) { 135 | changed = true; 136 | if (invertible) { 137 | patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); 138 | } 139 | patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) }); 140 | } 141 | } 142 | } 143 | else if (Array.isArray(mirror) === Array.isArray(obj)) { 144 | if (invertible) { 145 | patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); 146 | } 147 | patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); 148 | deleted = true; // property has been deleted 149 | } 150 | else { 151 | if (invertible) { 152 | patches.push({ op: "test", path: path, value: mirror }); 153 | } 154 | patches.push({ op: "replace", path: path, value: obj }); 155 | changed = true; 156 | } 157 | } 158 | if (!deleted && newKeys.length == oldKeys.length) { 159 | return; 160 | } 161 | for (var t = 0; t < newKeys.length; t++) { 162 | var key = newKeys[t]; 163 | if (!hasOwnProperty(mirror, key) && obj[key] !== undefined) { 164 | patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: _deepClone(obj[key]) }); 165 | } 166 | } 167 | } 168 | /** 169 | * Create an array of patches from the differences in two objects 170 | */ 171 | export function compare(tree1, tree2, invertible) { 172 | if (invertible === void 0) { invertible = false; } 173 | var patches = []; 174 | _generate(tree1, tree2, patches, '', invertible); 175 | return patches; 176 | } 177 | -------------------------------------------------------------------------------- /module/helpers.d.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2022 Joachim Wester 4 | * MIT licensed 5 | */ 6 | export declare function hasOwnProperty(obj: any, key: any): any; 7 | export declare function _objectKeys(obj: any): any[]; 8 | /** 9 | * Deeply clone the object. 10 | * https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) 11 | * @param {any} obj value to clone 12 | * @return {any} cloned obj 13 | */ 14 | export declare function _deepClone(obj: any): any; 15 | export declare function isInteger(str: string): boolean; 16 | /** 17 | * Escapes a json pointer path 18 | * @param path The raw pointer 19 | * @return the Escaped path 20 | */ 21 | export declare function escapePathComponent(path: string): string; 22 | /** 23 | * Unescapes a json pointer path 24 | * @param path The escaped pointer 25 | * @return The unescaped path 26 | */ 27 | export declare function unescapePathComponent(path: string): string; 28 | export declare function _getPathRecursive(root: Object, obj: Object): string; 29 | export declare function getPath(root: Object, obj: Object): string; 30 | /** 31 | * Recursively checks whether an object has any undefined values inside. 32 | */ 33 | export declare function hasUndefined(obj: any): boolean; 34 | export declare type JsonPatchErrorName = 'SEQUENCE_NOT_AN_ARRAY' | 'OPERATION_NOT_AN_OBJECT' | 'OPERATION_OP_INVALID' | 'OPERATION_PATH_INVALID' | 'OPERATION_FROM_REQUIRED' | 'OPERATION_VALUE_REQUIRED' | 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED' | 'OPERATION_PATH_CANNOT_ADD' | 'OPERATION_PATH_UNRESOLVABLE' | 'OPERATION_FROM_UNRESOLVABLE' | 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX' | 'OPERATION_VALUE_OUT_OF_BOUNDS' | 'TEST_OPERATION_FAILED'; 35 | export declare class PatchError extends Error { 36 | name: JsonPatchErrorName; 37 | index?: number; 38 | operation?: any; 39 | tree?: any; 40 | constructor(message: string, name: JsonPatchErrorName, index?: number, operation?: any, tree?: any); 41 | } 42 | -------------------------------------------------------------------------------- /module/helpers.mjs: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2022 Joachim Wester 4 | * MIT licensed 5 | */ 6 | var __extends = (this && this.__extends) || (function () { 7 | var extendStatics = function (d, b) { 8 | extendStatics = Object.setPrototypeOf || 9 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 10 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 11 | return extendStatics(d, b); 12 | }; 13 | return function (d, b) { 14 | extendStatics(d, b); 15 | function __() { this.constructor = d; } 16 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 17 | }; 18 | })(); 19 | var _hasOwnProperty = Object.prototype.hasOwnProperty; 20 | export function hasOwnProperty(obj, key) { 21 | return _hasOwnProperty.call(obj, key); 22 | } 23 | export function _objectKeys(obj) { 24 | if (Array.isArray(obj)) { 25 | var keys_1 = new Array(obj.length); 26 | for (var k = 0; k < keys_1.length; k++) { 27 | keys_1[k] = "" + k; 28 | } 29 | return keys_1; 30 | } 31 | if (Object.keys) { 32 | return Object.keys(obj); 33 | } 34 | var keys = []; 35 | for (var i in obj) { 36 | if (hasOwnProperty(obj, i)) { 37 | keys.push(i); 38 | } 39 | } 40 | return keys; 41 | } 42 | ; 43 | /** 44 | * Deeply clone the object. 45 | * https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) 46 | * @param {any} obj value to clone 47 | * @return {any} cloned obj 48 | */ 49 | export function _deepClone(obj) { 50 | switch (typeof obj) { 51 | case "object": 52 | return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 53 | case "undefined": 54 | return null; //this is how JSON.stringify behaves for array items 55 | default: 56 | return obj; //no need to clone primitives 57 | } 58 | } 59 | //3x faster than cached /^\d+$/.test(str) 60 | export function isInteger(str) { 61 | var i = 0; 62 | var len = str.length; 63 | var charCode; 64 | while (i < len) { 65 | charCode = str.charCodeAt(i); 66 | if (charCode >= 48 && charCode <= 57) { 67 | i++; 68 | continue; 69 | } 70 | return false; 71 | } 72 | return true; 73 | } 74 | /** 75 | * Escapes a json pointer path 76 | * @param path The raw pointer 77 | * @return the Escaped path 78 | */ 79 | export function escapePathComponent(path) { 80 | if (path.indexOf('/') === -1 && path.indexOf('~') === -1) 81 | return path; 82 | return path.replace(/~/g, '~0').replace(/\//g, '~1'); 83 | } 84 | /** 85 | * Unescapes a json pointer path 86 | * @param path The escaped pointer 87 | * @return The unescaped path 88 | */ 89 | export function unescapePathComponent(path) { 90 | return path.replace(/~1/g, '/').replace(/~0/g, '~'); 91 | } 92 | export function _getPathRecursive(root, obj) { 93 | var found; 94 | for (var key in root) { 95 | if (hasOwnProperty(root, key)) { 96 | if (root[key] === obj) { 97 | return escapePathComponent(key) + '/'; 98 | } 99 | else if (typeof root[key] === 'object') { 100 | found = _getPathRecursive(root[key], obj); 101 | if (found != '') { 102 | return escapePathComponent(key) + '/' + found; 103 | } 104 | } 105 | } 106 | } 107 | return ''; 108 | } 109 | export function getPath(root, obj) { 110 | if (root === obj) { 111 | return '/'; 112 | } 113 | var path = _getPathRecursive(root, obj); 114 | if (path === '') { 115 | throw new Error("Object not found in root"); 116 | } 117 | return "/" + path; 118 | } 119 | /** 120 | * Recursively checks whether an object has any undefined values inside. 121 | */ 122 | export function hasUndefined(obj) { 123 | if (obj === undefined) { 124 | return true; 125 | } 126 | if (obj) { 127 | if (Array.isArray(obj)) { 128 | for (var i_1 = 0, len = obj.length; i_1 < len; i_1++) { 129 | if (hasUndefined(obj[i_1])) { 130 | return true; 131 | } 132 | } 133 | } 134 | else if (typeof obj === "object") { 135 | var objKeys = _objectKeys(obj); 136 | var objKeysLength = objKeys.length; 137 | for (var i = 0; i < objKeysLength; i++) { 138 | if (hasUndefined(obj[objKeys[i]])) { 139 | return true; 140 | } 141 | } 142 | } 143 | } 144 | return false; 145 | } 146 | function patchErrorMessageFormatter(message, args) { 147 | var messageParts = [message]; 148 | for (var key in args) { 149 | var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print 150 | if (typeof value !== 'undefined') { 151 | messageParts.push(key + ": " + value); 152 | } 153 | } 154 | return messageParts.join('\n'); 155 | } 156 | var PatchError = /** @class */ (function (_super) { 157 | __extends(PatchError, _super); 158 | function PatchError(message, name, index, operation, tree) { 159 | var _newTarget = this.constructor; 160 | var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; 161 | _this.name = name; 162 | _this.index = index; 163 | _this.operation = operation; 164 | _this.tree = tree; 165 | Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 166 | _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); 167 | return _this; 168 | } 169 | return PatchError; 170 | }(Error)); 171 | export { PatchError }; 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-json-patch", 3 | "version": "3.1.1", 4 | "description": "Fast implementation of JSON-Patch (RFC-6902) with duplex (observe changes) capabilities", 5 | "homepage": "https://github.com/Starcounter-Jack/JSON-Patch", 6 | "keywords": [ 7 | "json", 8 | "patch", 9 | "http", 10 | "rest" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/Starcounter-Jack/JSON-Patch.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/Starcounter-Jack/JSON-Patch/issues" 18 | }, 19 | "author": { 20 | "name": "Joachim Wester", 21 | "email": "joachimwester@me.com", 22 | "url": "http://www.starcounter.com/" 23 | }, 24 | "license": "MIT", 25 | "main": "index.js", 26 | "module": "index.mjs", 27 | "typings": "index.d.ts", 28 | "devDependencies": { 29 | "benchmark": "^2.1.4", 30 | "bluebird": "^3.5.5", 31 | "bluebird-retry": "^0.11.0", 32 | "chalk": "^2.4.2", 33 | "event-target-shim": "^5.0.1", 34 | "fast-deep-equal": "^2.0.1", 35 | "http-server": "^0.12.3", 36 | "jasmine": "^3.4.0", 37 | "request": "^2.88.0", 38 | "sauce-connect-launcher": "^1.2.7", 39 | "saucelabs": "^2.1.9", 40 | "selenium-webdriver": "^4.0.0-alpha.4", 41 | "typescript": "~3.5.2", 42 | "webpack": "^4.35.0", 43 | "webpack-cli": "^3.3.5" 44 | }, 45 | "scripts": { 46 | "tsc": "npm run tsc-common && npm run tsc-module", 47 | "tsc-common": "tsc", 48 | "tsc-module": "tsc --module esnext --moduleResolution node --outDir \"module/\" && npm run tsc-to-mjs", 49 | "tsc-to-mjs": "bash tsc-to-mjs.sh", 50 | "version": "npm run tsc && webpack && git add -A", 51 | "build": "npm run tsc && webpack", 52 | "serve": "http-server -p 5000 --silent", 53 | "tsc-watch": "tsc -w", 54 | "test": "npm run tsc && npm run test-core && npm run test-duplex && npm run test-commonjs && npm run test-webpack-import && npm run test-typings", 55 | "test-sauce": "npm run build && node test/Sauce/Runner.js", 56 | "test-commonjs": "jasmine test/spec/commonjs/requireSpec.js", 57 | "test-webpack-import": "webpack --env.NODE_ENV=test && jasmine test/spec/webpack/importSpec.build.js", 58 | "test-typings": "tsc test/spec/typings/typingsSpec.ts", 59 | "test-duplex": "node --experimental-modules jasmine-run.mjs test/**/*[sS]pec.mjs", 60 | "test-core": "node --experimental-modules jasmine-run.mjs 'test/spec/{jsonPatchTestsSpec,coreSpec,validateSpec}.mjs'", 61 | "bench": "npm run bench-core && npm run bench-duplex", 62 | "bench-core": "node test/spec/coreBenchmark.js", 63 | "bench-duplex": "node test/spec/coreBenchmark.js && node test/spec/duplexBenchmark.js" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2013-2021 Joachim Wester 4 | * MIT license 5 | */ 6 | declare var require: any; 7 | 8 | import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined } from './helpers.js'; 9 | 10 | export const JsonPatchError = PatchError; 11 | export const deepClone = _deepClone; 12 | 13 | export type Operation = AddOperation | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation; 14 | 15 | export interface Validator { 16 | (operation: Operation, index: number, document: T, existingPathFragment: string): void; 17 | } 18 | 19 | export interface OperationResult { 20 | removed?: any, 21 | test?: boolean, 22 | newDocument: T; 23 | } 24 | 25 | export interface BaseOperation { 26 | path: string; 27 | } 28 | 29 | export interface AddOperation extends BaseOperation { 30 | op: 'add'; 31 | value: T; 32 | } 33 | 34 | export interface RemoveOperation extends BaseOperation { 35 | op: 'remove'; 36 | } 37 | 38 | export interface ReplaceOperation extends BaseOperation { 39 | op: 'replace'; 40 | value: T; 41 | } 42 | 43 | export interface MoveOperation extends BaseOperation { 44 | op: 'move'; 45 | from: string; 46 | } 47 | 48 | export interface CopyOperation extends BaseOperation { 49 | op: 'copy'; 50 | from: string; 51 | } 52 | 53 | export interface TestOperation extends BaseOperation { 54 | op: 'test'; 55 | value: T; 56 | } 57 | 58 | export interface GetOperation extends BaseOperation { 59 | op: '_get'; 60 | value: T; 61 | } 62 | export interface PatchResult extends Array> { 63 | newDocument: T; 64 | } 65 | 66 | /* We use a Javascript hash to store each 67 | function. Each hash entry (property) uses 68 | the operation identifiers specified in rfc6902. 69 | In this way, we can map each patch operation 70 | to its dedicated function in efficient way. 71 | */ 72 | 73 | /* The operations applicable to an object */ 74 | const objOps = { 75 | add: function (obj, key, document) { 76 | obj[key] = this.value; 77 | return { newDocument: document }; 78 | }, 79 | remove: function (obj, key, document) { 80 | var removed = obj[key]; 81 | delete obj[key]; 82 | return { newDocument: document, removed } 83 | }, 84 | replace: function (obj, key, document) { 85 | var removed = obj[key]; 86 | obj[key] = this.value; 87 | return { newDocument: document, removed }; 88 | }, 89 | move: function (obj, key, document) { 90 | /* in case move target overwrites an existing value, 91 | return the removed value, this can be taxing performance-wise, 92 | and is potentially unneeded */ 93 | let removed = getValueByPointer(document, this.path); 94 | 95 | if (removed) { 96 | removed = _deepClone(removed); 97 | } 98 | 99 | const originalValue = applyOperation(document, 100 | { op: "remove", path: this.from } 101 | ).removed; 102 | 103 | applyOperation(document, 104 | { op: "add", path: this.path, value: originalValue } 105 | ); 106 | 107 | return { newDocument: document, removed }; 108 | }, 109 | copy: function (obj, key, document) { 110 | const valueToCopy = getValueByPointer(document, this.from); 111 | // enforce copy by value so further operations don't affect source (see issue #177) 112 | applyOperation(document, 113 | { op: "add", path: this.path, value: _deepClone(valueToCopy) } 114 | ); 115 | return { newDocument: document } 116 | }, 117 | test: function (obj, key, document) { 118 | return { newDocument: document, test: _areEquals(obj[key], this.value) } 119 | }, 120 | _get: function (obj, key, document) { 121 | this.value = obj[key]; 122 | return { newDocument: document } 123 | } 124 | }; 125 | 126 | /* The operations applicable to an array. Many are the same as for the object */ 127 | var arrOps = { 128 | add: function (arr, i, document) { 129 | if(isInteger(i)) { 130 | arr.splice(i, 0, this.value); 131 | } else { // array props 132 | arr[i] = this.value; 133 | } 134 | // this may be needed when using '-' in an array 135 | return { newDocument: document, index: i } 136 | }, 137 | remove: function (arr, i, document) { 138 | var removedList = arr.splice(i, 1); 139 | return { newDocument: document, removed: removedList[0] }; 140 | }, 141 | replace: function (arr, i, document) { 142 | var removed = arr[i]; 143 | arr[i] = this.value; 144 | return { newDocument: document, removed }; 145 | }, 146 | move: objOps.move, 147 | copy: objOps.copy, 148 | test: objOps.test, 149 | _get: objOps._get 150 | }; 151 | 152 | /** 153 | * Retrieves a value from a JSON document by a JSON pointer. 154 | * Returns the value. 155 | * 156 | * @param document The document to get the value from 157 | * @param pointer an escaped JSON pointer 158 | * @return The retrieved value 159 | */ 160 | export function getValueByPointer(document: any, pointer: string): any { 161 | if (pointer == '') { 162 | return document; 163 | } 164 | var getOriginalDestination = >{ op: "_get", path: pointer }; 165 | applyOperation(document, getOriginalDestination); 166 | return getOriginalDestination.value; 167 | } 168 | /** 169 | * Apply a single JSON Patch Operation on a JSON document. 170 | * Returns the {newDocument, result} of the operation. 171 | * It modifies the `document` and `operation` objects - it gets the values by reference. 172 | * If you would like to avoid touching your values, clone them: 173 | * `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`. 174 | * 175 | * @param document The document to patch 176 | * @param operation The operation to apply 177 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 178 | * @param mutateDocument Whether to mutate the original document or clone it before applying 179 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 180 | * @return `{newDocument, result}` after the operation 181 | */ 182 | export function applyOperation(document: T, operation: Operation, validateOperation: boolean | Validator = false, mutateDocument: boolean = true, banPrototypeModifications: boolean = true, index: number = 0): OperationResult { 183 | if (validateOperation) { 184 | if (typeof validateOperation == 'function') { 185 | validateOperation(operation, 0, document, operation.path); 186 | } 187 | else { 188 | validator(operation, 0); 189 | } 190 | } 191 | /* ROOT OPERATIONS */ 192 | if (operation.path === "") { 193 | let returnValue: OperationResult = { newDocument: document }; 194 | if (operation.op === 'add') { 195 | returnValue.newDocument = operation.value; 196 | return returnValue; 197 | } else if (operation.op === 'replace') { 198 | returnValue.newDocument = operation.value; 199 | returnValue.removed = document; //document we removed 200 | return returnValue; 201 | } 202 | else if (operation.op === 'move' || operation.op === 'copy') { // it's a move or copy to root 203 | returnValue.newDocument = getValueByPointer(document, operation.from); // get the value by json-pointer in `from` field 204 | if (operation.op === 'move') { // report removed item 205 | returnValue.removed = document; 206 | } 207 | return returnValue; 208 | } else if (operation.op === 'test') { 209 | returnValue.test = _areEquals(document, operation.value); 210 | if (returnValue.test === false) { 211 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 212 | } 213 | returnValue.newDocument = document; 214 | return returnValue; 215 | } else if (operation.op === 'remove') { // a remove on root 216 | returnValue.removed = document; 217 | returnValue.newDocument = null; 218 | return returnValue; 219 | } else if (operation.op === '_get') { 220 | operation.value = document; 221 | return returnValue; 222 | } else { /* bad operation */ 223 | if (validateOperation) { 224 | throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); 225 | } else { 226 | return returnValue; 227 | } 228 | } 229 | } /* END ROOT OPERATIONS */ 230 | else { 231 | if (!mutateDocument) { 232 | document = _deepClone(document); 233 | } 234 | const path = operation.path || ""; 235 | const keys = path.split('/'); 236 | let obj = document; 237 | let t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift 238 | let len = keys.length; 239 | let existingPathFragment = undefined; 240 | let key: string | number; 241 | let validateFunction; 242 | if (typeof validateOperation == 'function') { 243 | validateFunction = validateOperation; 244 | } 245 | else { 246 | validateFunction = validator; 247 | } 248 | while (true) { 249 | key = keys[t]; 250 | if (key && key.indexOf('~') != -1) { 251 | key = unescapePathComponent(key); 252 | } 253 | 254 | if(banPrototypeModifications && 255 | (key == '__proto__' || 256 | (key == 'prototype' && t>0 && keys[t-1] == 'constructor')) 257 | ) { 258 | throw new TypeError('JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'); 259 | } 260 | 261 | if (validateOperation) { 262 | if (existingPathFragment === undefined) { 263 | if (obj[key] === undefined) { 264 | existingPathFragment = keys.slice(0, t).join('/'); 265 | } 266 | else if (t == len - 1) { 267 | existingPathFragment = operation.path; 268 | } 269 | if (existingPathFragment !== undefined) { 270 | validateFunction(operation, 0, document, existingPathFragment); 271 | } 272 | } 273 | } 274 | t++; 275 | if (Array.isArray(obj)) { 276 | if (key === '-') { 277 | key = obj.length; 278 | } 279 | else { 280 | if (validateOperation && !isInteger(key)) { 281 | throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document); 282 | } // only parse key when it's an integer for `arr.prop` to work 283 | else if(isInteger(key)) { 284 | key = ~~key; 285 | } 286 | } 287 | if (t >= len) { 288 | if (validateOperation && operation.op === "add" && key > obj.length) { 289 | throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); 290 | } 291 | const returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch 292 | if (returnValue.test === false) { 293 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 294 | } 295 | return returnValue; 296 | } 297 | } 298 | else { 299 | if (t >= len) { 300 | const returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch 301 | if (returnValue.test === false) { 302 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 303 | } 304 | return returnValue; 305 | } 306 | } 307 | obj = obj[key]; 308 | // If we have more keys in the path, but the next value isn't a non-null object, 309 | // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. 310 | if (validateOperation && t < len && (!obj || typeof obj !== "object")){ 311 | throw new JsonPatchError('Cannot perform operation at the desired path', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); 312 | } 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * Apply a full JSON Patch array on a JSON document. 319 | * Returns the {newDocument, result} of the patch. 320 | * It modifies the `document` object and `patch` - it gets the values by reference. 321 | * If you would like to avoid touching your values, clone them: 322 | * `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`. 323 | * 324 | * @param document The document to patch 325 | * @param patch The patch to apply 326 | * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. 327 | * @param mutateDocument Whether to mutate the original document or clone it before applying 328 | * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. 329 | * @return An array of `{newDocument, result}` after the patch 330 | */ 331 | export function applyPatch(document: T, patch: ReadonlyArray, validateOperation?: boolean | Validator, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): PatchResult { 332 | if(validateOperation) { 333 | if(!Array.isArray(patch)) { 334 | throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); 335 | } 336 | } 337 | if (!mutateDocument) { 338 | document = _deepClone(document); 339 | } 340 | const results = new Array(patch.length) as PatchResult; 341 | 342 | for (let i = 0, length = patch.length; i < length; i++) { 343 | // we don't need to pass mutateDocument argument because if it was true, we already deep cloned the object, we'll just pass `true` 344 | results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i); 345 | document = results[i].newDocument; // in case root was replaced 346 | } 347 | results.newDocument = document; 348 | return results; 349 | } 350 | 351 | /** 352 | * Apply a single JSON Patch Operation on a JSON document. 353 | * Returns the updated document. 354 | * Suitable as a reducer. 355 | * 356 | * @param document The document to patch 357 | * @param operation The operation to apply 358 | * @return The updated document 359 | */ 360 | export function applyReducer(document: T, operation: Operation, index: number): T { 361 | const operationResult: OperationResult = applyOperation(document, operation) 362 | if (operationResult.test === false) { // failed test 363 | throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); 364 | } 365 | return operationResult.newDocument; 366 | } 367 | 368 | /** 369 | * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error. 370 | * @param {object} operation - operation object (patch) 371 | * @param {number} index - index of operation in the sequence 372 | * @param {object} [document] - object where the operation is supposed to be applied 373 | * @param {string} [existingPathFragment] - comes along with `document` 374 | */ 375 | export function validator(operation: Operation, index: number, document?: any, existingPathFragment?: string): void { 376 | if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { 377 | throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); 378 | } 379 | 380 | else if (!objOps[operation.op]) { 381 | throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); 382 | } 383 | 384 | else if (typeof operation.path !== 'string') { 385 | throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); 386 | } 387 | 388 | else if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { 389 | // paths that aren't empty string should start with "/" 390 | throw new JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); 391 | } 392 | 393 | else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') { 394 | throw new JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, document); 395 | } 396 | 397 | else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) { 398 | throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, document); 399 | } 400 | 401 | else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && hasUndefined(operation.value)) { 402 | throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, document); 403 | } 404 | 405 | else if (document) { 406 | if (operation.op == "add") { 407 | var pathLen = operation.path.split("/").length; 408 | var existingPathLen = existingPathFragment.split("/").length; 409 | if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) { 410 | throw new JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, document); 411 | } 412 | } 413 | else if (operation.op === 'replace' || operation.op === 'remove' || (operation.op) === '_get') { 414 | if (operation.path !== existingPathFragment) { 415 | throw new JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); 416 | } 417 | } 418 | else if (operation.op === 'move' || operation.op === 'copy') { 419 | var existingValue: any = { op: "_get", path: operation.from, value: undefined }; 420 | var error = validate([existingValue], document); 421 | if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') { 422 | throw new JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, document); 423 | } 424 | } 425 | } 426 | } 427 | 428 | /** 429 | * Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document. 430 | * If error is encountered, returns a JsonPatchError object 431 | * @param sequence 432 | * @param document 433 | * @returns {JsonPatchError|undefined} 434 | */ 435 | export function validate(sequence: ReadonlyArray, document?: T, externalValidator?: Validator): PatchError { 436 | try { 437 | if (!Array.isArray(sequence)) { 438 | throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); 439 | } 440 | if (document) { 441 | //clone document and sequence so that we can safely try applying operations 442 | applyPatch(_deepClone(document), _deepClone(sequence), externalValidator || true); 443 | } 444 | else { 445 | externalValidator = externalValidator || validator; 446 | for (var i = 0; i < sequence.length; i++) { 447 | externalValidator(sequence[i], i, document, undefined); 448 | } 449 | } 450 | } 451 | catch (e) { 452 | if (e instanceof JsonPatchError) { 453 | return e; 454 | } 455 | else { 456 | throw e; 457 | } 458 | } 459 | } 460 | 461 | // based on https://github.com/epoberezkin/fast-deep-equal 462 | // MIT License 463 | 464 | // Copyright (c) 2017 Evgeny Poberezkin 465 | 466 | // Permission is hereby granted, free of charge, to any person obtaining a copy 467 | // of this software and associated documentation files (the "Software"), to deal 468 | // in the Software without restriction, including without limitation the rights 469 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 470 | // copies of the Software, and to permit persons to whom the Software is 471 | // furnished to do so, subject to the following conditions: 472 | 473 | // The above copyright notice and this permission notice shall be included in all 474 | // copies or substantial portions of the Software. 475 | 476 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 477 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 478 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 479 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 480 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 481 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 482 | // SOFTWARE. 483 | export function _areEquals(a: any, b: any): boolean { 484 | if (a === b) return true; 485 | 486 | if (a && b && typeof a == 'object' && typeof b == 'object') { 487 | var arrA = Array.isArray(a) 488 | , arrB = Array.isArray(b) 489 | , i 490 | , length 491 | , key; 492 | 493 | if (arrA && arrB) { 494 | length = a.length; 495 | if (length != b.length) return false; 496 | for (i = length; i-- !== 0;) 497 | if (!_areEquals(a[i], b[i])) return false; 498 | return true; 499 | } 500 | 501 | if (arrA != arrB) return false; 502 | 503 | var keys = Object.keys(a); 504 | length = keys.length; 505 | 506 | if (length !== Object.keys(b).length) 507 | return false; 508 | 509 | for (i = length; i-- !== 0;) 510 | if (!b.hasOwnProperty(keys[i])) return false; 511 | 512 | for (i = length; i-- !== 0;) { 513 | key = keys[i]; 514 | if (!_areEquals(a[key], b[key])) return false; 515 | } 516 | 517 | return true; 518 | } 519 | 520 | return a!==a && b!==b; 521 | }; 522 | -------------------------------------------------------------------------------- /src/duplex.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2021 Joachim Wester 4 | * MIT license 5 | */ 6 | import { _deepClone, _objectKeys, escapePathComponent, hasOwnProperty } from './helpers.js'; 7 | import { applyPatch, Operation } from './core.js'; 8 | 9 | export interface Observer { 10 | object: T; 11 | patches: Operation[]; 12 | unobserve: () => void; 13 | callback: (patches: Operation[]) => void; 14 | } 15 | 16 | var beforeDict = new WeakMap(); 17 | 18 | class Mirror { 19 | obj: any; 20 | observers : Map = new Map(); 21 | value: Object | Array; 22 | 23 | constructor(obj: Object) { 24 | this.obj = obj; 25 | } 26 | } 27 | 28 | class ObserverInfo { 29 | callback: Function; 30 | observer: ObserverInfo; 31 | 32 | constructor(callback: Function, observer: ObserverInfo) { 33 | this.callback = callback; 34 | this.observer = observer; 35 | } 36 | } 37 | 38 | function getMirror(obj: Object): Mirror { 39 | return beforeDict.get(obj); 40 | } 41 | 42 | function getObserverFromMirror(mirror: Mirror, callback): ObserverInfo { 43 | return mirror.observers.get(callback) 44 | } 45 | 46 | function removeObserverFromMirror(mirror: Mirror, observer): void { 47 | mirror.observers.delete(observer.callback); 48 | } 49 | 50 | /** 51 | * Detach an observer from an object 52 | */ 53 | export function unobserve(root: T, observer: Observer) { 54 | observer.unobserve(); 55 | } 56 | 57 | /** 58 | * Observes changes made to an object, which can then be retrieved using generate 59 | */ 60 | export function observe(obj: Object|Array, callback?: (patches: Operation[]) => void): Observer { 61 | var patches = []; 62 | var observer; 63 | var mirror = getMirror(obj); 64 | 65 | if (!mirror) { 66 | mirror = new Mirror(obj); 67 | beforeDict.set(obj, mirror); 68 | } else { 69 | const observerInfo = getObserverFromMirror(mirror, callback); 70 | observer = observerInfo && observerInfo.observer; 71 | } 72 | 73 | if (observer) { 74 | return observer; 75 | } 76 | 77 | observer = {}; 78 | 79 | mirror.value = _deepClone(obj); 80 | 81 | if (callback) { 82 | observer.callback = callback; 83 | observer.next = null; 84 | 85 | var dirtyCheck = () => { 86 | generate(observer); 87 | }; 88 | var fastCheck = () => { 89 | clearTimeout(observer.next); 90 | observer.next = setTimeout(dirtyCheck); 91 | }; 92 | if (typeof window !== 'undefined') { //not Node 93 | window.addEventListener('mouseup', fastCheck); 94 | window.addEventListener('keyup', fastCheck); 95 | window.addEventListener('mousedown', fastCheck); 96 | window.addEventListener('keydown', fastCheck); 97 | window.addEventListener('change', fastCheck); 98 | } 99 | } 100 | observer.patches = patches; 101 | observer.object = obj; 102 | 103 | observer.unobserve = () => { 104 | generate(observer); 105 | clearTimeout(observer.next); 106 | removeObserverFromMirror(mirror, observer); 107 | 108 | if (typeof window !== 'undefined') { 109 | window.removeEventListener('mouseup', fastCheck); 110 | window.removeEventListener('keyup', fastCheck); 111 | window.removeEventListener('mousedown', fastCheck); 112 | window.removeEventListener('keydown', fastCheck); 113 | window.removeEventListener('change', fastCheck); 114 | } 115 | }; 116 | 117 | mirror.observers.set(callback, new ObserverInfo(callback, observer)); 118 | 119 | return observer; 120 | } 121 | 122 | /** 123 | * Generate an array of patches from an observer 124 | */ 125 | export function generate(observer: Observer, invertible = false): Operation[] { 126 | var mirror = beforeDict.get(observer.object); 127 | 128 | _generate(mirror.value, observer.object, observer.patches, "", invertible); 129 | if (observer.patches.length) { 130 | applyPatch(mirror.value, observer.patches); 131 | } 132 | var temp = observer.patches; 133 | if (temp.length > 0) { 134 | observer.patches = []; 135 | if (observer.callback) { 136 | observer.callback(temp); 137 | } 138 | } 139 | return temp; 140 | } 141 | 142 | // Dirty check if obj is different from mirror, generate patches and update mirror 143 | function _generate(mirror, obj, patches, path, invertible) { 144 | if (obj === mirror) { 145 | return; 146 | } 147 | 148 | if (typeof obj.toJSON === "function") { 149 | obj = obj.toJSON(); 150 | } 151 | 152 | var newKeys = _objectKeys(obj); 153 | var oldKeys = _objectKeys(mirror); 154 | var changed = false; 155 | var deleted = false; 156 | 157 | //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" 158 | 159 | for (var t = oldKeys.length - 1; t >= 0; t--) { 160 | var key = oldKeys[t]; 161 | var oldVal = mirror[key]; 162 | 163 | if (hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { 164 | var newVal = obj[key]; 165 | 166 | if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null && Array.isArray(oldVal) === Array.isArray(newVal)) { 167 | _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible); 168 | } 169 | else { 170 | if (oldVal !== newVal) { 171 | changed = true; 172 | if (invertible) { 173 | patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); 174 | } 175 | patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) }); 176 | } 177 | } 178 | } 179 | else if(Array.isArray(mirror) === Array.isArray(obj)) { 180 | if (invertible) { 181 | patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); 182 | } 183 | patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); 184 | deleted = true; // property has been deleted 185 | } else { 186 | if (invertible) { 187 | patches.push({ op: "test", path, value: mirror }); 188 | } 189 | patches.push({ op: "replace", path, value: obj }); 190 | changed = true; 191 | } 192 | } 193 | 194 | if (!deleted && newKeys.length == oldKeys.length) { 195 | return; 196 | } 197 | 198 | for (var t = 0; t < newKeys.length; t++) { 199 | var key = newKeys[t]; 200 | if (!hasOwnProperty(mirror, key) && obj[key] !== undefined) { 201 | patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: _deepClone(obj[key]) }); 202 | } 203 | } 204 | } 205 | /** 206 | * Create an array of patches from the differences in two objects 207 | */ 208 | export function compare(tree1: Object | Array, tree2: Object | Array, invertible = false): Operation[] { 209 | var patches = []; 210 | _generate(tree1, tree2, patches, '', invertible); 211 | return patches; 212 | } 213 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/Starcounter-Jack/JSON-Patch 3 | * (c) 2017-2022 Joachim Wester 4 | * MIT licensed 5 | */ 6 | 7 | const _hasOwnProperty = Object.prototype.hasOwnProperty; 8 | export function hasOwnProperty(obj, key) { 9 | return _hasOwnProperty.call(obj, key); 10 | } 11 | export function _objectKeys(obj) { 12 | if (Array.isArray(obj)) { 13 | const keys = new Array(obj.length); 14 | for (let k = 0; k < keys.length; k++) { 15 | keys[k] = "" + k; 16 | } 17 | return keys; 18 | } 19 | if (Object.keys) { 20 | return Object.keys(obj); 21 | } 22 | let keys = []; 23 | for (let i in obj) { 24 | if (hasOwnProperty(obj, i)) { 25 | keys.push(i); 26 | } 27 | } 28 | return keys; 29 | }; 30 | /** 31 | * Deeply clone the object. 32 | * https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) 33 | * @param {any} obj value to clone 34 | * @return {any} cloned obj 35 | */ 36 | export function _deepClone(obj) { 37 | switch (typeof obj) { 38 | case "object": 39 | return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 40 | case "undefined": 41 | return null; //this is how JSON.stringify behaves for array items 42 | default: 43 | return obj; //no need to clone primitives 44 | } 45 | } 46 | //3x faster than cached /^\d+$/.test(str) 47 | export function isInteger(str: string): boolean { 48 | let i = 0; 49 | const len = str.length; 50 | let charCode; 51 | while (i < len) { 52 | charCode = str.charCodeAt(i); 53 | if (charCode >= 48 && charCode <= 57) { 54 | i++; 55 | continue; 56 | } 57 | return false; 58 | } 59 | return true; 60 | } 61 | /** 62 | * Escapes a json pointer path 63 | * @param path The raw pointer 64 | * @return the Escaped path 65 | */ 66 | export function escapePathComponent(path: string): string { 67 | if (path.indexOf('/') === -1 && path.indexOf('~') === -1) return path; 68 | return path.replace(/~/g, '~0').replace(/\//g, '~1'); 69 | } 70 | /** 71 | * Unescapes a json pointer path 72 | * @param path The escaped pointer 73 | * @return The unescaped path 74 | */ 75 | export function unescapePathComponent(path: string): string { 76 | return path.replace(/~1/g, '/').replace(/~0/g, '~'); 77 | } 78 | 79 | export function _getPathRecursive(root: Object, obj: Object): string { 80 | let found; 81 | for (let key in root) { 82 | if (hasOwnProperty(root, key)) { 83 | if (root[key] === obj) { 84 | return escapePathComponent(key) + '/'; 85 | } 86 | else if (typeof root[key] === 'object') { 87 | found = _getPathRecursive(root[key], obj); 88 | if (found != '') { 89 | return escapePathComponent(key) + '/' + found; 90 | } 91 | } 92 | } 93 | } 94 | return ''; 95 | } 96 | 97 | export function getPath(root: Object, obj: Object): string { 98 | if (root === obj) { 99 | return '/'; 100 | } 101 | const path = _getPathRecursive(root, obj); 102 | if (path === '') { 103 | throw new Error("Object not found in root"); 104 | } 105 | return `/${path}`; 106 | } 107 | /** 108 | * Recursively checks whether an object has any undefined values inside. 109 | */ 110 | export function hasUndefined(obj: any): boolean { 111 | if (obj === undefined) { 112 | return true; 113 | } 114 | if (obj) { 115 | if (Array.isArray(obj)) { 116 | for (let i = 0, len = obj.length; i < len; i++) { 117 | if (hasUndefined(obj[i])) { 118 | return true; 119 | } 120 | } 121 | } 122 | else if (typeof obj === "object") { 123 | const objKeys = _objectKeys(obj); 124 | const objKeysLength = objKeys.length; 125 | for (var i = 0; i < objKeysLength; i++) { 126 | if (hasUndefined(obj[objKeys[i]])) { 127 | return true; 128 | } 129 | } 130 | } 131 | } 132 | return false; 133 | } 134 | 135 | export type JsonPatchErrorName = 'SEQUENCE_NOT_AN_ARRAY' | 136 | 'OPERATION_NOT_AN_OBJECT' | 137 | 'OPERATION_OP_INVALID' | 138 | 'OPERATION_PATH_INVALID' | 139 | 'OPERATION_FROM_REQUIRED' | 140 | 'OPERATION_VALUE_REQUIRED' | 141 | 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED' | 142 | 'OPERATION_PATH_CANNOT_ADD' | 143 | 'OPERATION_PATH_UNRESOLVABLE' | 144 | 'OPERATION_FROM_UNRESOLVABLE' | 145 | 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX' | 146 | 'OPERATION_VALUE_OUT_OF_BOUNDS' | 147 | 'TEST_OPERATION_FAILED'; 148 | 149 | function patchErrorMessageFormatter(message: String, args: Object): string { 150 | const messageParts = [message]; 151 | for (const key in args) { 152 | const value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print 153 | if (typeof value !== 'undefined') { 154 | messageParts.push(`${key}: ${value}`); 155 | } 156 | } 157 | return messageParts.join('\n'); 158 | } 159 | export class PatchError extends Error { 160 | constructor(message: string, public name: JsonPatchErrorName, public index?: number, public operation?: any, public tree?: any) { 161 | super(patchErrorMessageFormatter(message, { name, index, operation, tree })); 162 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 163 | this.message = patchErrorMessageFormatter(message, { name, index, operation, tree }); 164 | } 165 | } -------------------------------------------------------------------------------- /test/Sauce/CapabilityRunner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SauceLabs Jasmine CapabilityRunner 3 | * CapabilityRunner.js 0.0.0 4 | * (c) 2017 Omar Alshaker, Marcin Warpechowski 5 | * MIT license 6 | */ 7 | 8 | const SauceLabs = require("saucelabs").default; 9 | const webdriver = require("selenium-webdriver"); 10 | const Promise = require("bluebird"); 11 | const retryUntil = require("bluebird-retry"); 12 | 13 | function CapabilityRunner(caps) { 14 | return new Promise(function(resolve, reject) { 15 | console.log(""); 16 | console.log(caps.name); 17 | 18 | const username = caps.username; 19 | const accessKey = caps.accessKey; 20 | 21 | const saucelabs = new SauceLabs({ 22 | username: username, 23 | password: accessKey 24 | }); 25 | 26 | const By = webdriver.By; 27 | 28 | let driver = new webdriver.Builder() 29 | .withCapabilities(caps) 30 | .usingServer( 31 | "http://" + username + ":" + accessKey + "@localhost:4445/wd/hub" 32 | ) 33 | .build(); 34 | 35 | driver.get( 36 | "http://localhost:5000/test/index.html" 37 | ); 38 | 39 | const symbols = { passed: "√", pending: "-", failed: "x" }; 40 | 41 | function checkIfDone() { 42 | return new Promise(function(resolve, reject) { 43 | driver 44 | .executeScript("return window.jsApiReporter && window.jsApiReporter.finished && window.jsApiReporter.specs();") 45 | .then(function(results) { 46 | if (results) { 47 | resolve(results); 48 | } else { 49 | reject(); 50 | } 51 | }); 52 | }); 53 | } 54 | /* get session ID and keep checking if tests are finished */ 55 | driver.getSession().then(sessionID => { 56 | /*set driver ID to end session later */ 57 | driver.sessionID = sessionID.id_; 58 | retryUntil(checkIfDone, { interval: 15000 }).then(testResults => { 59 | console.log("Specs finished"); 60 | analyzeResults(testResults); 61 | }).catch(error => { 62 | console.log(`${caps.name}: ${error}`); 63 | process.exit(1); 64 | }); 65 | }); 66 | 67 | async function analyzeResults(results) { 68 | const resultsSummary = { passed: 0, pending: 0, failed: 0 }; 69 | var hadErrored = 0; 70 | results.forEach(spec => { 71 | resultsSummary[spec.status]++; 72 | console.log( 73 | " " + symbols[spec.status] + " " + spec.fullName 74 | ); 75 | if (spec.status === "failed") { 76 | hadErrored = 1; 77 | console.log(`Spec "${spec.fullName}" failed, the error was`, spec.failedExpectations); 78 | } 79 | }); 80 | console.log(""); 81 | console.log( 82 | ("Summary for (" + caps.name + ")") 83 | ); 84 | console.log(resultsSummary); 85 | console.log(""); 86 | console.log("Ending session: " + driver.sessionID); 87 | 88 | const result = { 89 | name: "Summary: Passed: " + 90 | resultsSummary.passed + 91 | ", pending: " + 92 | resultsSummary.pending + 93 | ", failed: " + 94 | resultsSummary.failed, 95 | passed: hadErrored === 0 96 | }; 97 | 98 | driver.quit(); 99 | 100 | await saucelabs.updateJob(username, driver.sessionID, result); 101 | resolve(); 102 | } 103 | }); 104 | } 105 | 106 | module.exports = CapabilityRunner; 107 | -------------------------------------------------------------------------------- /test/Sauce/Runner.js: -------------------------------------------------------------------------------- 1 | const CapabilityRunner = require("./CapabilityRunner"); 2 | const sauceConnectLauncher = require('sauce-connect-launcher'); 3 | 4 | const username = process.env.SAUCE_USERNAME; //JSON-Patch repo uses "json-patch" user which is a subaccount of the Starcounter SauceLabs account 5 | const accessKey = process.env.SAUCE_ACCESS_KEY; 6 | 7 | const tunnelIdentifier = "tunnel" + (process.env.TRAVIS_JOB_NUMBER || "-local"); 8 | 9 | if (!username) { 10 | console.error( 11 | "You need Sauce Labs access to run these specs, if you're a Palindrom org member, please contact @alshakero for this information, if not you need to add your own SauceLabs auth info to your system environment variables." 12 | ); 13 | process.exit(1); 14 | 15 | } else { 16 | const allCaps = [ 17 | { 18 | browserName: "chrome", 19 | platform: "Windows 10", 20 | username: username, 21 | accessKey: accessKey, 22 | name: "Chrome: Running tests", 23 | "tunnel-identifier": tunnelIdentifier 24 | }, 25 | { 26 | browserName: "firefox", 27 | platform: "Windows 10", 28 | username: username, 29 | accessKey: accessKey, 30 | name: "Firefox: Running tests", 31 | "tunnel-identifier": tunnelIdentifier 32 | }, 33 | { 34 | browserName: "MicrosoftEdge", 35 | platform: "Windows 10", 36 | username: username, 37 | accessKey: accessKey, 38 | name: "MicrosoftEdge: Running tests", 39 | "tunnel-identifier": tunnelIdentifier 40 | } 41 | ]; 42 | 43 | (async function() { 44 | try { 45 | console.log("Preparing Sauce Connect"); 46 | sauceConnectLauncher({ 47 | tunnelIdentifier 48 | }, async function (err, sauceConnectProcess) { 49 | if (err) { 50 | console.error(err.message); 51 | return; 52 | } 53 | console.log("Sauce Connect ready"); 54 | 55 | await CapabilityRunner(allCaps[0]); 56 | await CapabilityRunner(allCaps[1]); 57 | await CapabilityRunner(allCaps[2]); 58 | 59 | sauceConnectProcess.close(function () { 60 | console.log("Closed Sauce Connect process"); 61 | process.exit(0); 62 | }) 63 | 64 | console.log('Done!'); 65 | }); 66 | 67 | } catch (error) { 68 | console.log(error); 69 | process.exit(1); 70 | } 71 | })(); 72 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "test", 3 | "helpers": [ 4 | "helpers/**/*.js" 5 | ], 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | -------------------------------------------------------------------------------- /test/lib/benchmark_console_reporter.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | function benchmarkResultsToConsole(suite){ 3 | console.log("\n"); 4 | console.log("=================="); 5 | console.log("Benchmark results:"); 6 | console.log("------------------"); 7 | var bench; 8 | for(var testNo = 0; testNo < suite.length; testNo++){ 9 | bench = suite[testNo]; 10 | console.log(chalk.green.underline(bench.name) + 11 | "\n Ops/sec: " + chalk.bold.magenta(bench.hz.toFixed(bench.hz < 100 ? 2 : 0)) + 12 | chalk.dim(' ±' + bench.stats.rme.toFixed(2) + '% ') + 13 | chalk.gray('Ran ' + bench.count + ' times in ' + 14 | bench.times.cycle.toFixed(3) + ' seconds.')); 15 | } 16 | console.log("==================="); 17 | } 18 | if (typeof exports !== "undefined") { 19 | exports.benchmarkResultsToConsole = benchmarkResultsToConsole; 20 | } 21 | -------------------------------------------------------------------------------- /test/lib/benchmark_reporter.css: -------------------------------------------------------------------------------- 1 | #test_results { 2 | margin-top: 10px; 3 | font-size: 12px; 4 | font-family: sans-serif; 5 | border-collapse: collapse; 6 | border-spacing: 0; 7 | } 8 | 9 | #test_results th, #test_results td { 10 | border: solid 1px #ccc; 11 | vertical-align: top; 12 | padding: 3px; 13 | } 14 | 15 | #test_results th { 16 | vertical-align: bottom; 17 | background-color: #ccc; 18 | padding: 1px; 19 | font-size: 10px; 20 | } 21 | 22 | #test_results #test_platform { 23 | color: #444; 24 | text-align: center; 25 | } 26 | 27 | #test_results .test_row { 28 | color: #006; 29 | cursor: pointer; 30 | } 31 | 32 | #test_results .test_nonlooping { 33 | border-left-style: dotted; 34 | border-left-width: 2px; 35 | } 36 | 37 | #test_results .test_looping { 38 | border-left-style: solid; 39 | border-left-width: 2px; 40 | } 41 | 42 | #test_results .test_name { 43 | white-space: nowrap; 44 | } 45 | 46 | #test_results .test_pending { 47 | } 48 | 49 | #test_results .test_running { 50 | font-style: italic; 51 | } 52 | 53 | #test_results .test_done { 54 | } 55 | 56 | #test_results .test_done { 57 | text-align: right; 58 | font-family: monospace; 59 | } 60 | 61 | #test_results .test_error { 62 | color: #600; 63 | } 64 | 65 | #test_results .test_error .error_head { 66 | font-weight: bold; 67 | } 68 | 69 | #test_results .test_error .error_body { 70 | font-size: 85%; 71 | } 72 | 73 | #test_results .test_row:hover td { 74 | background-color: #ffc; 75 | text-decoration: underline; 76 | } -------------------------------------------------------------------------------- /test/lib/benchmark_reporter.js: -------------------------------------------------------------------------------- 1 | function benchmarkReporter(suite) { 2 | var table = document.createElement('table'); 3 | 4 | function renderBenchmarkCol(parent, elemName, innerHTML) { 5 | var elem = document.createElement(elemName); 6 | elem.innerHTML = innerHTML; 7 | parent.appendChild(elem); 8 | } 9 | 10 | function renderBenchmark() { 11 | while (table.hasChildNodes()) { 12 | table.removeChild(table.lastChild); 13 | } 14 | 15 | var tr, txt; 16 | 17 | tr = document.createElement('tr'); 18 | renderBenchmarkCol(tr, "th", "Test"); 19 | renderBenchmarkCol(tr, "th", "Ops/sec"); 20 | table.appendChild(tr); 21 | 22 | for (var i = 0, ilen = suite.length; i < ilen; i++) { 23 | var bench = suite[i]; 24 | tr = document.createElement('tr'); 25 | renderBenchmarkCol(tr, "td", bench.name); 26 | 27 | if (bench.running) { 28 | txt = 'running…'; 29 | } 30 | // status: completed 31 | else if (bench.hz) { 32 | // obscure details until the suite has completed 33 | txt = '' + Benchmark.formatNumber(bench.hz.toFixed(bench.hz < 100 ? 2 : 0)) + 35 | ' ±' + bench.stats.rme.toFixed(2) + '%' + ''; 36 | } 37 | else { 38 | txt = "ready"; 39 | } 40 | 41 | renderBenchmarkCol(tr, "td", txt); 42 | table.appendChild(tr); 43 | } 44 | } 45 | 46 | if (window.addEventListener) { 47 | window.addEventListener("load", function () { 48 | var button = document.createElement('button'); 49 | button.innerHTML = "Run benchmark"; 50 | button.addEventListener("click", function () { 51 | suite.run(); 52 | }); 53 | document.body.appendChild(button); 54 | 55 | var p = document.createElement('p'); 56 | p.innerHTML = Benchmark.platform.toString(); 57 | document.body.appendChild(p); 58 | 59 | table.id = "test_results"; 60 | document.body.appendChild(table); 61 | 62 | renderBenchmark(); 63 | }); 64 | } 65 | 66 | suite.on('cycle', function (event) { 67 | //renderBenchmark(); 68 | }); 69 | suite.on('complete', function () { 70 | renderBenchmark(); 71 | }); 72 | } -------------------------------------------------------------------------------- /test/spec/commonjs/requireSpec.js: -------------------------------------------------------------------------------- 1 | const jsonpatch = require('../../..'); 2 | 3 | describe('CommonJS', function () { 4 | describe('require', function () { 5 | it('should have the expected structure', function () { 6 | expect(typeof jsonpatch).withContext("result from require() should be an object").toEqual("object"); 7 | expect(typeof jsonpatch).withContext("result from require() should not be a function").not.toEqual("function"); 8 | expect(jsonpatch.applyOperation).withContext("applyOperation should be a method within the object").toBeDefined(); 9 | expect(jsonpatch.applyPatch).withContext("applyPatch should be a method within the object").toBeDefined(); 10 | expect(jsonpatch.applyReducer).withContext("applyReducer should be a method within the object").toBeDefined(); 11 | expect(jsonpatch.getValueByPointer).withContext("getValueByPointer should be a method within the object").toBeDefined(); 12 | expect(jsonpatch.validate).withContext("validate should be a method within the object").toBeDefined(); 13 | expect(jsonpatch.validator).withContext("validator should be a method within the object").toBeDefined(); 14 | expect(jsonpatch._areEquals).withContext("_areEquals should be a method within the object").toBeDefined(); 15 | expect(jsonpatch.JsonPatchError).withContext("JsonPatchError should be a method within the object").toBeDefined(); 16 | expect(jsonpatch.deepClone).withContext("deepClone should be a method within the object").toBeDefined(); 17 | expect(jsonpatch.escapePathComponent).withContext("escapePathComponent should be a method within the object").toBeDefined(); 18 | expect(jsonpatch.unescapePathComponent).withContext("unescapePathComponent should be a method within the object").toBeDefined(); 19 | expect(jsonpatch.unobserve).withContext("unobserve should be a method within the object").toBeDefined(); 20 | expect(jsonpatch.observe).withContext("observe should be a method within the object").toBeDefined(); 21 | expect(jsonpatch.generate).withContext("generate should be a method within the object").toBeDefined(); 22 | expect(jsonpatch.compare).withContext("compare should be a method within the object").toBeDefined(); 23 | }); 24 | }); 25 | }); -------------------------------------------------------------------------------- /test/spec/coreBenchmark.js: -------------------------------------------------------------------------------- 1 | if (typeof jsonpatch === 'undefined') { 2 | jsonpatch = require('./../..'); 3 | } 4 | if (typeof Benchmark === 'undefined') { 5 | global.Benchmark = require('benchmark'); 6 | global.benchmarkResultsToConsole = require('./../lib/benchmark_console_reporter.js').benchmarkResultsToConsole; 7 | } 8 | 9 | 10 | 11 | const coreSuite = new Benchmark.Suite; 12 | coreSuite.add('add operation', { 13 | setup: function(){ 14 | const obj = { 15 | foo: 1, 16 | baz: [{ 17 | qux: 'hello' 18 | }] 19 | }; 20 | }, 21 | fn: function() { 22 | jsonpatch.applyPatch(obj, [{ 23 | op: 'add', 24 | path: '/bar', 25 | value: [1, 2, 3, 4] 26 | }]); 27 | } 28 | }); 29 | coreSuite.add('remove operation', { 30 | setup: function(){ 31 | const obj = { 32 | foo: 1, 33 | baz: [{ 34 | qux: 'hello' 35 | }], 36 | bar: [1, 2, 3, 4] 37 | }; 38 | }, 39 | fn: function() { 40 | jsonpatch.applyPatch(obj, [{ 41 | op: 'remove', 42 | path: '/bar' 43 | }]); 44 | } 45 | }); 46 | coreSuite.add('replace operation', { 47 | setup: function(){ 48 | const obj = { 49 | foo: 1, 50 | baz: [{ 51 | qux: 'hello' 52 | }] 53 | }; 54 | }, 55 | fn: function() { 56 | jsonpatch.applyPatch(obj, [{ 57 | op: 'replace', 58 | path: '/foo', 59 | value: [1, 2, 3, 4] 60 | }]); 61 | } 62 | }); 63 | coreSuite.add('move operation', { 64 | setup: function(){ 65 | const obj = { 66 | foo: 1, 67 | baz: [{ 68 | qux: 'hello' 69 | }], 70 | bar: [1, 2, 3, 4] 71 | }; 72 | }, 73 | fn: function() { 74 | jsonpatch.applyPatch(obj, [{ 75 | op: 'move', 76 | from: '/baz/0', 77 | path: '/bar/0' 78 | }]); 79 | } 80 | }); 81 | coreSuite.add('copy operation', { 82 | setup: function(){ 83 | const obj = { 84 | foo: 1, 85 | baz: [{ 86 | qux: 'hello' 87 | }], 88 | bar: [1, 2, 3, 4] 89 | }; 90 | }, 91 | fn: function() { 92 | jsonpatch.applyPatch(obj, [{ 93 | op: 'copy', 94 | from: '/baz/0', 95 | path: '/bar/0' 96 | }]); 97 | } 98 | }); 99 | coreSuite.add('test operation', { 100 | setup: function(){ 101 | const obj = { 102 | foo: 1, 103 | baz: [{ 104 | qux: 'hello' 105 | }] 106 | }; 107 | }, 108 | fn: function() { 109 | jsonpatch.applyPatch(obj, [{ 110 | op: 'test', 111 | path: '/baz', 112 | value: [{ 113 | qux: 'hello' 114 | }] 115 | }]); 116 | } 117 | }); 118 | 119 | // if we are in the browser with benchmark < 2.1.2 120 | if(typeof benchmarkReporter !== 'undefined'){ 121 | benchmarkReporter(coreSuite); 122 | } else { 123 | coreSuite.on('complete', function () { 124 | benchmarkResultsToConsole(coreSuite); 125 | }); 126 | coreSuite.run(); 127 | } 128 | -------------------------------------------------------------------------------- /test/spec/duplexBenchmark.js: -------------------------------------------------------------------------------- 1 | if (typeof window === 'undefined') { 2 | const jsdom = require("jsdom"); 3 | const { JSDOM } = jsdom; 4 | const dom = new JSDOM(); 5 | global.window = dom.window; 6 | global.document = dom.window.document; 7 | } 8 | 9 | if (typeof jsonpatch === 'undefined') { 10 | jsonpatch = require('./../..'); 11 | } 12 | 13 | if (typeof Benchmark === 'undefined') { 14 | global.Benchmark = require('benchmark'); 15 | global.benchmarkResultsToConsole = require('./../lib/benchmark_console_reporter.js').benchmarkResultsToConsole; 16 | } 17 | 18 | const duplexSuite = new Benchmark.Suite(); 19 | duplexSuite.add('generate operation', { 20 | setup: function() { 21 | const obj = { 22 | firstName: 'Albert', 23 | lastName: 'Einstein', 24 | phoneNumbers: [ 25 | { 26 | number: '12345' 27 | }, 28 | { 29 | number: '45353' 30 | } 31 | ] 32 | }; 33 | const observer = jsonpatch.observe(obj); 34 | }, 35 | fn: function() { 36 | obj.firstName = 'Joachim'; 37 | obj.lastName = 'Wester'; 38 | obj.phoneNumbers[0].number = '123'; 39 | obj.phoneNumbers[1].number = '456'; 40 | 41 | const patches = jsonpatch.generate(observer); 42 | } 43 | }); 44 | duplexSuite.add('generate operation and re-apply', { 45 | setup: function() { 46 | const obj = { 47 | firstName: 'Albert', 48 | lastName: 'Einstein', 49 | phoneNumbers: [ 50 | { 51 | number: '12345' 52 | }, 53 | { 54 | number: '45353' 55 | } 56 | ] 57 | }; 58 | const observer = jsonpatch.observe(obj); 59 | }, 60 | fn: function() { 61 | obj.firstName = 'Joachim'; 62 | obj.lastName = 'Wester'; 63 | obj.phoneNumbers[0].number = '123'; 64 | obj.phoneNumbers[1].number = '456'; 65 | 66 | const patches = jsonpatch.generate(observer); 67 | obj2 = { 68 | firstName: 'Albert', 69 | lastName: 'Einstein', 70 | phoneNumbers: [ 71 | { 72 | number: '12345' 73 | }, 74 | { 75 | number: '45353' 76 | } 77 | ] 78 | }; 79 | 80 | jsonpatch.applyPatch(obj2, patches); 81 | } 82 | }); 83 | duplexSuite.add('compare operation', { 84 | setup: function() { 85 | const obj = { 86 | firstName: 'Albert', 87 | lastName: 'Einstein', 88 | phoneNumbers: [ 89 | { 90 | number: '12345' 91 | }, 92 | { 93 | number: '45353' 94 | } 95 | ] 96 | }; 97 | const obj2 = { 98 | firstName: 'Joachim', 99 | lastName: 'Wester', 100 | mobileNumbers: [ 101 | { 102 | number: '12345' 103 | }, 104 | { 105 | number: '45353' 106 | } 107 | ] 108 | }; 109 | }, 110 | fn: function() { 111 | const patches = jsonpatch.compare(obj, obj2); 112 | } 113 | }); 114 | 115 | duplexSuite.add('compare operation same but deep objects', { 116 | setup: function() { 117 | const depth = 10; 118 | 119 | function shallowObj() { 120 | return { 121 | shallow: { 122 | firstName: 'Tomek', 123 | lastName: 'Wytrebowicz', 124 | mobileNumbers: [ 125 | { 126 | number: '12345' 127 | }, 128 | { 129 | number: '45353' 130 | } 131 | ] 132 | } 133 | }; 134 | } 135 | const obj = shallowObj(); 136 | const node = obj; 137 | while (depth-- > 0) { 138 | node.nested = shallowObj(); 139 | node = node.nested; 140 | } 141 | const obj2 = obj; 142 | }, 143 | fn: function() { 144 | const patches = jsonpatch.compare(obj, obj2); 145 | } 146 | }); 147 | 148 | // Benchmark generating test operations 149 | duplexSuite.add('generate operation, invertible = true', { 150 | setup: function() { 151 | const obj = { 152 | firstName: 'Albert', 153 | lastName: 'Einstein', 154 | phoneNumbers: [ 155 | { 156 | number: '12345' 157 | }, 158 | { 159 | number: '45353' 160 | } 161 | ] 162 | }; 163 | const observer = jsonpatch.observe(obj); 164 | }, 165 | fn: function() { 166 | obj.firstName = 'Joachim'; 167 | obj.lastName = 'Wester'; 168 | obj.phoneNumbers[0].number = '123'; 169 | obj.phoneNumbers[1].number = '456'; 170 | 171 | const patches = jsonpatch.generate(observer, true); 172 | } 173 | }); 174 | duplexSuite.add('generate operation and re-apply, invertible = true', { 175 | setup: function() { 176 | const obj = { 177 | firstName: 'Albert', 178 | lastName: 'Einstein', 179 | phoneNumbers: [ 180 | { 181 | number: '12345' 182 | }, 183 | { 184 | number: '45353' 185 | } 186 | ] 187 | }; 188 | const observer = jsonpatch.observe(obj); 189 | }, 190 | fn: function() { 191 | obj.firstName = 'Joachim'; 192 | obj.lastName = 'Wester'; 193 | obj.phoneNumbers[0].number = '123'; 194 | obj.phoneNumbers[1].number = '456'; 195 | 196 | const patches = jsonpatch.generate(observer, true); 197 | obj2 = { 198 | firstName: 'Albert', 199 | lastName: 'Einstein', 200 | phoneNumbers: [ 201 | { 202 | number: '12345' 203 | }, 204 | { 205 | number: '45353' 206 | } 207 | ] 208 | }; 209 | 210 | jsonpatch.applyPatch(obj2, patches); 211 | } 212 | }); 213 | duplexSuite.add('compare operation, invertible = true', { 214 | setup: function() { 215 | const obj = { 216 | firstName: 'Albert', 217 | lastName: 'Einstein', 218 | phoneNumbers: [ 219 | { 220 | number: '12345' 221 | }, 222 | { 223 | number: '45353' 224 | } 225 | ] 226 | }; 227 | const obj2 = { 228 | firstName: 'Joachim', 229 | lastName: 'Wester', 230 | mobileNumbers: [ 231 | { 232 | number: '12345' 233 | }, 234 | { 235 | number: '45353' 236 | } 237 | ] 238 | }; 239 | }, 240 | fn: function() { 241 | const patches = jsonpatch.compare(obj, obj2, true); 242 | } 243 | }); 244 | 245 | duplexSuite.add('compare operation same but deep objects, invertible = true', { 246 | setup: function() { 247 | const depth = 10; 248 | 249 | function shallowObj() { 250 | return { 251 | shallow: { 252 | firstName: 'Tomek', 253 | lastName: 'Wytrebowicz', 254 | mobileNumbers: [ 255 | { 256 | number: '12345' 257 | }, 258 | { 259 | number: '45353' 260 | } 261 | ] 262 | } 263 | }; 264 | } 265 | const obj = shallowObj(); 266 | const node = obj; 267 | while (depth-- > 0) { 268 | node.nested = shallowObj(); 269 | node = node.nested; 270 | } 271 | const obj2 = obj; 272 | }, 273 | fn: function() { 274 | const patches = jsonpatch.compare(obj, obj2, true); 275 | } 276 | }); 277 | 278 | // if we are in the browser with benchmark < 2.1.2 279 | if (typeof benchmarkReporter !== 'undefined') { 280 | benchmarkReporter(duplexSuite); 281 | } else { 282 | duplexSuite.on('complete', function() { 283 | benchmarkResultsToConsole(duplexSuite); 284 | }); 285 | duplexSuite.run(); 286 | } 287 | -------------------------------------------------------------------------------- /test/spec/json-patch-tests/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#* 3 | -------------------------------------------------------------------------------- /test/spec/json-patch-tests/README.md: -------------------------------------------------------------------------------- 1 | JSON Patch Tests 2 | ================ 3 | 4 | These are test cases for implementations of [IETF JSON Patch (RFC6902)](http://tools.ietf.org/html/rfc6902). 5 | 6 | Some implementations can be found at [jsonpatch.com](http://jsonpatch.com). 7 | 8 | 9 | Test Format 10 | ----------- 11 | 12 | Each test file is a JSON document that contains an array of test records. A 13 | test record is an object with the following members: 14 | 15 | - doc: The JSON document to test against 16 | - patch: The patch(es) to apply 17 | - expected: The expected resulting document, OR 18 | - error: A string describing an expected error 19 | - comment: A string describing the test 20 | - disabled: True if the test should be skipped 21 | 22 | All fields except 'doc' and 'patch' are optional. Test records consisting only 23 | of a comment are also OK. 24 | 25 | 26 | Files 27 | ----- 28 | 29 | - tests.json: the main test file 30 | - spec_tests.json: tests from the RFC6902 spec 31 | 32 | 33 | Writing Tests 34 | ------------- 35 | 36 | All tests should have a descriptive comment. Tests should be as 37 | simple as possible - just what's required to test a specific piece of 38 | behavior. If you want to test interacting behaviors, create tests for 39 | each behavior as well as the interaction. 40 | 41 | If an 'error' member is specified, the error text should describe the 42 | error the implementation should raise - *not* what's being tested. 43 | Implementation error strings will vary, but the suggested error should 44 | be easily matched to the implementation error string. Try to avoid 45 | creating error tests that might pass because an incorrect error was 46 | reported. 47 | 48 | Please feel free to contribute! 49 | 50 | 51 | Credits 52 | ------- 53 | 54 | The seed test set was adapted from Byron Ruth's 55 | [jsonpatch-js](https://github.com/bruth/jsonpatch-js/blob/master/test.js) and 56 | extended by [Mike McCabe](https://github.com/mikemccabe). 57 | 58 | 59 | License 60 | ------- 61 | 62 | Copyright 2014 The Authors 63 | 64 | Licensed under the Apache License, Version 2.0 (the "License"); 65 | you may not use this file except in compliance with the License. 66 | You may obtain a copy of the License at 67 | 68 | http://www.apache.org/licenses/LICENSE-2.0 69 | 70 | Unless required by applicable law or agreed to in writing, software 71 | distributed under the License is distributed on an "AS IS" BASIS, 72 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 73 | See the License for the specific language governing permissions and 74 | limitations under the License. 75 | 76 | -------------------------------------------------------------------------------- /test/spec/json-patch-tests/spec_tests.json.mjs: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | "comment": "4.1. add with missing object", 4 | "doc": { "q": { "bar": 2 } }, 5 | "patch": [ {"op": "add", "path": "/a/b", "value": 1} ], 6 | "error": 7 | "path /a does not exist -- missing objects are not created recursively" 8 | }, 9 | 10 | { 11 | "comment": "A.1. Adding an Object Member", 12 | "doc": { 13 | "foo": "bar" 14 | }, 15 | "patch": [ 16 | { "op": "add", "path": "/baz", "value": "qux" } 17 | ], 18 | "expected": { 19 | "baz": "qux", 20 | "foo": "bar" 21 | } 22 | }, 23 | 24 | { 25 | "comment": "A.2. Adding an Array Element", 26 | "doc": { 27 | "foo": [ "bar", "baz" ] 28 | }, 29 | "patch": [ 30 | { "op": "add", "path": "/foo/1", "value": "qux" } 31 | ], 32 | "expected": { 33 | "foo": [ "bar", "qux", "baz" ] 34 | } 35 | }, 36 | 37 | { 38 | "comment": "A.3. Removing an Object Member", 39 | "doc": { 40 | "baz": "qux", 41 | "foo": "bar" 42 | }, 43 | "patch": [ 44 | { "op": "remove", "path": "/baz" } 45 | ], 46 | "expected": { 47 | "foo": "bar" 48 | } 49 | }, 50 | 51 | { 52 | "comment": "A.4. Removing an Array Element", 53 | "doc": { 54 | "foo": [ "bar", "qux", "baz" ] 55 | }, 56 | "patch": [ 57 | { "op": "remove", "path": "/foo/1" } 58 | ], 59 | "expected": { 60 | "foo": [ "bar", "baz" ] 61 | } 62 | }, 63 | 64 | { 65 | "comment": "A.5. Replacing a Value", 66 | "doc": { 67 | "baz": "qux", 68 | "foo": "bar" 69 | }, 70 | "patch": [ 71 | { "op": "replace", "path": "/baz", "value": "boo" } 72 | ], 73 | "expected": { 74 | "baz": "boo", 75 | "foo": "bar" 76 | } 77 | }, 78 | 79 | { 80 | "comment": "A.6. Moving a Value", 81 | "doc": { 82 | "foo": { 83 | "bar": "baz", 84 | "waldo": "fred" 85 | }, 86 | "qux": { 87 | "corge": "grault" 88 | } 89 | }, 90 | "patch": [ 91 | { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" } 92 | ], 93 | "expected": { 94 | "foo": { 95 | "bar": "baz" 96 | }, 97 | "qux": { 98 | "corge": "grault", 99 | "thud": "fred" 100 | } 101 | } 102 | }, 103 | 104 | { 105 | "comment": "A.7. Moving an Array Element", 106 | "doc": { 107 | "foo": [ "all", "grass", "cows", "eat" ] 108 | }, 109 | "patch": [ 110 | { "op": "move", "from": "/foo/1", "path": "/foo/3" } 111 | ], 112 | "expected": { 113 | "foo": [ "all", "cows", "eat", "grass" ] 114 | } 115 | 116 | }, 117 | 118 | { 119 | "comment": "A.8. Testing a Value: Success", 120 | "doc": { 121 | "baz": "qux", 122 | "foo": [ "a", 2, "c" ] 123 | }, 124 | "patch": [ 125 | { "op": "test", "path": "/baz", "value": "qux" }, 126 | { "op": "test", "path": "/foo/1", "value": 2 } 127 | ], 128 | "expected": { 129 | "baz": "qux", 130 | "foo": [ "a", 2, "c" ] 131 | } 132 | }, 133 | 134 | { 135 | "comment": "A.9. Testing a Value: Error", 136 | "doc": { 137 | "baz": "qux" 138 | }, 139 | "patch": [ 140 | { "op": "test", "path": "/baz", "value": "bar" } 141 | ], 142 | "error": "string not equivalent" 143 | }, 144 | 145 | { 146 | "comment": "A.10. Adding a nested Member Object", 147 | "doc": { 148 | "foo": "bar" 149 | }, 150 | "patch": [ 151 | { "op": "add", "path": "/child", "value": { "grandchild": { } } } 152 | ], 153 | "expected": { 154 | "foo": "bar", 155 | "child": { 156 | "grandchild": { 157 | } 158 | } 159 | } 160 | }, 161 | 162 | { 163 | "comment": "A.11. Ignoring Unrecognized Elements", 164 | "doc": { 165 | "foo":"bar" 166 | }, 167 | "patch": [ 168 | { "op": "add", "path": "/baz", "value": "qux", "xyz": 123 } 169 | ], 170 | "expected": { 171 | "foo":"bar", 172 | "baz":"qux" 173 | } 174 | }, 175 | 176 | { 177 | "comment": "A.12. Adding to a Non-existent Target", 178 | "doc": { 179 | "foo": "bar" 180 | }, 181 | "patch": [ 182 | { "op": "add", "path": "/baz/bat", "value": "qux" } 183 | ], 184 | "error": "add to a non-existent target" 185 | }, 186 | 187 | { 188 | "comment": "A.13 Invalid JSON Patch Document", 189 | "doc": { 190 | "foo": "bar" 191 | }, 192 | "patch": [ 193 | { "op": "add", "path": "/baz", "value": "qux", "op": "remove" } 194 | ], 195 | "error": "operation has two 'op' members", 196 | "disabled": true 197 | }, 198 | 199 | { 200 | "comment": "A.14. ~ Escape Ordering", 201 | "doc": { 202 | "/": 9, 203 | "~1": 10 204 | }, 205 | "patch": [{"op": "test", "path": "/~01", "value": 10}], 206 | "expected": { 207 | "/": 9, 208 | "~1": 10 209 | } 210 | }, 211 | 212 | { 213 | "comment": "A.15. Comparing Strings and Numbers", 214 | "doc": { 215 | "/": 9, 216 | "~1": 10 217 | }, 218 | "patch": [{"op": "test", "path": "/~01", "value": "10"}], 219 | "error": "number is not equal to string" 220 | }, 221 | 222 | { 223 | "comment": "A.16. Adding an Array Value", 224 | "doc": { 225 | "foo": ["bar"] 226 | }, 227 | "patch": [{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }], 228 | "expected": { 229 | "foo": ["bar", ["abc", "def"]] 230 | } 231 | } 232 | 233 | ] 234 | -------------------------------------------------------------------------------- /test/spec/json-patch-tests/tests.json.mjs: -------------------------------------------------------------------------------- 1 | export default [ 2 | { "comment": "empty list, empty docs", 3 | "doc": {}, 4 | "patch": [], 5 | "expected": {} }, 6 | 7 | { "comment": "empty patch list", 8 | "doc": {"foo": 1}, 9 | "patch": [], 10 | "expected": {"foo": 1} }, 11 | 12 | { "comment": "rearrangements OK?", 13 | "doc": {"foo": 1, "bar": 2}, 14 | "patch": [], 15 | "expected": {"bar":2, "foo": 1} }, 16 | 17 | { "comment": "rearrangements OK? How about one level down ... array", 18 | "doc": [{"foo": 1, "bar": 2}], 19 | "patch": [], 20 | "expected": [{"bar":2, "foo": 1}] }, 21 | 22 | { "comment": "rearrangements OK? How about one level down...", 23 | "doc": {"foo":{"foo": 1, "bar": 2}}, 24 | "patch": [], 25 | "expected": {"foo":{"bar":2, "foo": 1}} }, 26 | 27 | { "comment": "add replaces any existing field", 28 | "doc": {"foo": null}, 29 | "patch": [{"op": "add", "path": "/foo", "value":1}], 30 | "expected": {"foo": 1} }, 31 | 32 | { "comment": "toplevel array", 33 | "doc": [], 34 | "patch": [{"op": "add", "path": "/0", "value": "foo"}], 35 | "expected": ["foo"] }, 36 | 37 | { "comment": "toplevel array, no change", 38 | "doc": ["foo"], 39 | "patch": [], 40 | "expected": ["foo"] }, 41 | 42 | { "comment": "toplevel object, numeric string", 43 | "doc": {}, 44 | "patch": [{"op": "add", "path": "/foo", "value": "1"}], 45 | "expected": {"foo":"1"} }, 46 | 47 | { "comment": "toplevel object, integer", 48 | "doc": {}, 49 | "patch": [{"op": "add", "path": "/foo", "value": 1}], 50 | "expected": {"foo":1} }, 51 | 52 | { "comment": "Toplevel scalar values OK?", 53 | "doc": "foo", 54 | "patch": [{"op": "replace", "path": "", "value": "bar"}], 55 | "expected": "bar", 56 | "disabled": true }, 57 | 58 | { "comment": "Add, / target", 59 | "doc": {}, 60 | "patch": [ {"op": "add", "path": "/", "value":1 } ], 61 | "expected": {"":1} }, 62 | 63 | { "comment": "Add composite value at top level", 64 | "doc": {"foo": 1}, 65 | "patch": [{"op": "add", "path": "/bar", "value": [1, 2]}], 66 | "expected": {"foo": 1, "bar": [1, 2]} }, 67 | 68 | { "comment": "Add into composite value", 69 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 70 | "patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}], 71 | "expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} }, 72 | 73 | { "doc": {"bar": [1, 2]}, 74 | "patch": [{"op": "add", "path": "/bar/8", "value": "5"}], 75 | "error": "Out of bounds (upper)" }, 76 | 77 | { "doc": {"bar": [1, 2]}, 78 | "patch": [{"op": "add", "path": "/bar/-1", "value": "5"}], 79 | "error": "Out of bounds (lower)" }, 80 | 81 | { "doc": {"foo": 1}, 82 | "patch": [{"op": "add", "path": "/bar", "value": true}], 83 | "expected": {"foo": 1, "bar": true} }, 84 | 85 | { "doc": {"foo": 1}, 86 | "patch": [{"op": "add", "path": "/bar", "value": false}], 87 | "expected": {"foo": 1, "bar": false} }, 88 | 89 | { "doc": {"foo": 1}, 90 | "patch": [{"op": "add", "path": "/bar", "value": null}], 91 | "expected": {"foo": 1, "bar": null} }, 92 | 93 | { "comment": "0 can be an array index or object element name", 94 | "doc": {"foo": 1}, 95 | "patch": [{"op": "add", "path": "/0", "value": "bar"}], 96 | "expected": {"foo": 1, "0": "bar" } }, 97 | 98 | { "doc": ["foo"], 99 | "patch": [{"op": "add", "path": "/1", "value": "bar"}], 100 | "expected": ["foo", "bar"] }, 101 | 102 | { "doc": ["foo", "sil"], 103 | "patch": [{"op": "add", "path": "/1", "value": "bar"}], 104 | "expected": ["foo", "bar", "sil"] }, 105 | 106 | { "doc": ["foo", "sil"], 107 | "patch": [{"op": "add", "path": "/0", "value": "bar"}], 108 | "expected": ["bar", "foo", "sil"] }, 109 | 110 | { "doc": ["foo", "sil"], 111 | "patch": [{"op":"add", "path": "/2", "value": "bar"}], 112 | "expected": ["foo", "sil", "bar"] }, 113 | 114 | { "comment": "test against implementation-specific numeric parsing", 115 | "doc": {"1e0": "foo"}, 116 | "patch": [{"op": "test", "path": "/1e0", "value": "foo"}], 117 | "expected": {"1e0": "foo"} }, 118 | 119 | { "comment": "test with bad number should fail", 120 | "doc": ["foo", "bar"], 121 | "patch": [{"op": "test", "path": "/1e0", "value": "bar"}], 122 | "error": "test op shouldn't get array element 1" }, 123 | 124 | { "doc": ["foo", "sil"], 125 | "patch": [{"op": "add", "path": "/bar", "value": 42}], 126 | "error": "Object operation on array target" }, 127 | 128 | { "doc": ["foo", "sil"], 129 | "patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}], 130 | "expected": ["foo", ["bar", "baz"], "sil"], 131 | "comment": "value in array add not flattened" }, 132 | 133 | { "doc": {"foo": 1, "bar": [1, 2, 3, 4]}, 134 | "patch": [{"op": "remove", "path": "/bar"}], 135 | "expected": {"foo": 1} }, 136 | 137 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 138 | "patch": [{"op": "remove", "path": "/baz/0/qux"}], 139 | "expected": {"foo": 1, "baz": [{}]} }, 140 | 141 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 142 | "patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}], 143 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} }, 144 | 145 | { "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]}, 146 | "patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}], 147 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} }, 148 | 149 | { "doc": ["foo"], 150 | "patch": [{"op": "replace", "path": "/0", "value": "bar"}], 151 | "expected": ["bar"] }, 152 | 153 | { "doc": [""], 154 | "patch": [{"op": "replace", "path": "/0", "value": 0}], 155 | "expected": [0] }, 156 | 157 | { "doc": [""], 158 | "patch": [{"op": "replace", "path": "/0", "value": true}], 159 | "expected": [true] }, 160 | 161 | { "doc": [""], 162 | "patch": [{"op": "replace", "path": "/0", "value": false}], 163 | "expected": [false] }, 164 | 165 | { "doc": [""], 166 | "patch": [{"op": "replace", "path": "/0", "value": null}], 167 | "expected": [null] }, 168 | 169 | { "doc": ["foo", "sil"], 170 | "patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}], 171 | "expected": ["foo", ["bar", "baz"]], 172 | "comment": "value in array replace not flattened" }, 173 | 174 | { "comment": "replace whole document", 175 | "doc": {"foo": "bar"}, 176 | "patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}], 177 | "expected": {"baz": "qux"} }, 178 | 179 | { "comment": "spurious patch properties", 180 | "doc": {"foo": 1}, 181 | "patch": [{"op": "test", "path": "/foo", "value": 1, "spurious": 1}], 182 | "expected": {"foo": 1} }, 183 | 184 | { "doc": {"foo": null}, 185 | "patch": [{"op": "test", "path": "/foo", "value": null}], 186 | "comment": "null value should be valid obj property" }, 187 | 188 | { "doc": {"foo": null}, 189 | "patch": [{"op": "replace", "path": "/foo", "value": "truthy"}], 190 | "expected": {"foo": "truthy"}, 191 | "comment": "null value should be valid obj property to be replaced with something truthy" }, 192 | 193 | { "doc": {"foo": null}, 194 | "patch": [{"op": "move", "from": "/foo", "path": "/bar"}], 195 | "expected": {"bar": null}, 196 | "comment": "null value should be valid obj property to be moved" }, 197 | 198 | { "doc": {"foo": null}, 199 | "patch": [{"op": "copy", "from": "/foo", "path": "/bar"}], 200 | "expected": {"foo": null, "bar": null}, 201 | "comment": "null value should be valid obj property to be copied" }, 202 | 203 | { "doc": {"foo": null}, 204 | "patch": [{"op": "remove", "path": "/foo"}], 205 | "expected": {}, 206 | "comment": "null value should be valid obj property to be removed" }, 207 | 208 | { "doc": {"foo": "bar"}, 209 | "patch": [{"op": "replace", "path": "/foo", "value": null}], 210 | "expected": {"foo": null}, 211 | "comment": "null value should still be valid obj property replace other value" }, 212 | 213 | { "doc": {"foo": {"foo": 1, "bar": 2}}, 214 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": 2, "foo": 1}}], 215 | "comment": "test should pass despite rearrangement" }, 216 | 217 | { "doc": {"foo": [{"foo": 1, "bar": 2}]}, 218 | "patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}], 219 | "comment": "test should pass despite (nested) rearrangement" }, 220 | 221 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, 222 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": [1, 2, 5, 4]}}], 223 | "comment": "test should pass - no error" }, 224 | 225 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}}, 226 | "patch": [{"op": "test", "path": "/foo", "value": [1, 2]}], 227 | "error": "test op should fail" }, 228 | 229 | { "comment": "Whole document", 230 | "doc": { "foo": 1 }, 231 | "patch": [{"op": "test", "path": "", "value": {"foo": 1}}], 232 | "disabled": true }, 233 | 234 | { "comment": "Empty-string element", 235 | "doc": { "": 1 }, 236 | "patch": [{"op": "test", "path": "/", "value": 1}] }, 237 | 238 | { "doc": { 239 | "foo": ["bar", "baz"], 240 | "": 0, 241 | "a/b": 1, 242 | "c%d": 2, 243 | "e^f": 3, 244 | "g|h": 4, 245 | "i\\j": 5, 246 | "k\"l": 6, 247 | " ": 7, 248 | "m~n": 8 249 | }, 250 | "patch": [{"op": "test", "path": "/foo", "value": ["bar", "baz"]}, 251 | {"op": "test", "path": "/foo/0", "value": "bar"}, 252 | {"op": "test", "path": "/", "value": 0}, 253 | {"op": "test", "path": "/a~1b", "value": 1}, 254 | {"op": "test", "path": "/c%d", "value": 2}, 255 | {"op": "test", "path": "/e^f", "value": 3}, 256 | {"op": "test", "path": "/g|h", "value": 4}, 257 | {"op": "test", "path": "/i\\j", "value": 5}, 258 | {"op": "test", "path": "/k\"l", "value": 6}, 259 | {"op": "test", "path": "/ ", "value": 7}, 260 | {"op": "test", "path": "/m~0n", "value": 8}] }, 261 | 262 | { "comment": "Move to same location has no effect", 263 | "doc": {"foo": 1}, 264 | "patch": [{"op": "move", "from": "/foo", "path": "/foo"}], 265 | "expected": {"foo": 1} }, 266 | 267 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 268 | "patch": [{"op": "move", "from": "/foo", "path": "/bar"}], 269 | "expected": {"baz": [{"qux": "hello"}], "bar": 1} }, 270 | 271 | { "comment": "Move handles escaped paths", 272 | "doc": {"foo/": {"bar/": 1, "baz": 1}}, 273 | "patch": [{"op": "move", "from": "/foo~1/bar~1", "path": "/bar"}], 274 | "expected": {"bar": 1, "foo/": {"baz": 1}} }, 275 | 276 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, 277 | "patch": [{"op": "move", "from": "/baz/0/qux", "path": "/baz/1"}], 278 | "expected": {"baz": [{}, "hello"], "bar": 1} }, 279 | 280 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1}, 281 | "patch": [{"op": "copy", "from": "/baz/0", "path": "/boo"}], 282 | "expected": {"baz":[{"qux":"hello"}],"bar":1,"boo":{"qux":"hello"}} }, 283 | 284 | { "comment": "replacing the root of the document is possible with add", 285 | "doc": {"foo": "bar"}, 286 | "patch": [{"op": "add", "path": "", "value": {"baz": "qux"}}], 287 | "expected": {"baz":"qux"}}, 288 | 289 | { "comment": "Adding to \"/-\" adds to the end of the array", 290 | "doc": [ 1, 2 ], 291 | "patch": [ { "op": "add", "path": "/-", "value": { "foo": [ "bar", "baz" ] } } ], 292 | "expected": [ 1, 2, { "foo": [ "bar", "baz" ] } ]}, 293 | 294 | { "comment": "Adding to \"/-\" adds to the end of the array, even n levels down", 295 | "doc": [ 1, 2, [ 3, [ 4, 5 ] ] ], 296 | "patch": [ { "op": "add", "path": "/2/1/-", "value": { "foo": [ "bar", "baz" ] } } ], 297 | "expected": [ 1, 2, [ 3, [ 4, 5, { "foo": [ "bar", "baz" ] } ] ] ]}, 298 | 299 | { "comment": "test remove with bad number should fail", 300 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]}, 301 | "patch": [{"op": "remove", "path": "/baz/1e0/qux"}], 302 | "error": "remove op shouldn't remove from array with bad number" }, 303 | 304 | { "comment": "test remove on array", 305 | "doc": [1, 2, 3, 4], 306 | "patch": [{"op": "remove", "path": "/0"}], 307 | "expected": [2, 3, 4] }, 308 | 309 | { "comment": "test repeated removes", 310 | "doc": [1, 2, 3, 4], 311 | "patch": [{ "op": "remove", "path": "/1" }, 312 | { "op": "remove", "path": "/2" }], 313 | "expected": [1, 3] }, 314 | 315 | { "comment": "test remove with bad index should fail", 316 | "doc": [1, 2, 3, 4], 317 | "patch": [{"op": "remove", "path": "/1e0"}], 318 | "error": "remove op shouldn't remove from array with bad number" }, 319 | 320 | { "comment": "test replace with bad number should fail", 321 | "doc": [""], 322 | "patch": [{"op": "replace", "path": "/1e0", "value": false}], 323 | "error": "replace op shouldn't replace in array with bad number" }, 324 | 325 | { "comment": "test copy with bad number should fail", 326 | "doc": {"baz": [1,2,3], "bar": 1}, 327 | "patch": [{"op": "copy", "from": "/baz/1e0", "path": "/boo"}], 328 | "error": "copy op shouldn't work with bad number" }, 329 | 330 | { "comment": "test move with bad number should fail", 331 | "doc": {"foo": 1, "baz": [1,2,3,4]}, 332 | "patch": [{"op": "move", "from": "/baz/1e0", "path": "/foo"}], 333 | "error": "move op shouldn't work with bad number" }, 334 | 335 | { "comment": "test add with bad number should fail", 336 | "doc": ["foo", "sil"], 337 | "patch": [{"op": "add", "path": "/1e0", "value": "bar"}], 338 | "error": "add op shouldn't add to array with bad number" }, 339 | 340 | { "comment": "missing 'value' parameter to add", 341 | "doc": [ 1 ], 342 | "patch": [ { "op": "add", "path": "/-" } ], 343 | "error": "missing 'value' parameter" }, 344 | 345 | { "comment": "missing 'value' parameter to replace", 346 | "doc": [ 1 ], 347 | "patch": [ { "op": "replace", "path": "/0" } ], 348 | "error": "missing 'value' parameter" }, 349 | 350 | { "comment": "missing 'value' parameter to test", 351 | "doc": [ null ], 352 | "patch": [ { "op": "test", "path": "/0" } ], 353 | "error": "missing 'value' parameter" }, 354 | 355 | { "comment": "missing value parameter to test - where undef is falsy", 356 | "doc": [ false ], 357 | "patch": [ { "op": "test", "path": "/0" } ], 358 | "error": "missing 'value' parameter" }, 359 | 360 | { "comment": "missing from parameter to copy", 361 | "doc": [ 1 ], 362 | "patch": [ { "op": "copy", "path": "/-" } ], 363 | "error": "missing 'from' parameter" }, 364 | 365 | { "comment": "missing from parameter to move", 366 | "doc": { "foo": 1 }, 367 | "patch": [ { "op": "move", "path": "" } ], 368 | "error": "missing 'from' parameter" }, 369 | 370 | { "comment": "duplicate ops", 371 | "doc": { "foo": "bar" }, 372 | "patch": [ { "op": "add", "path": "/baz", "value": "qux", 373 | "op": "move", "from":"/foo" } ], 374 | "error": "patch has two 'op' members", 375 | "disabled": true }, 376 | 377 | { "comment": "unrecognized op should fail", 378 | "doc": {"foo": 1}, 379 | "patch": [{"op": "spam", "path": "/foo", "value": 1}], 380 | "error": "Unrecognized op 'spam'" } 381 | ] 382 | -------------------------------------------------------------------------------- /test/spec/jsonPatchTestsSpec.mjs: -------------------------------------------------------------------------------- 1 | import * as jsonpatch from '../../index.mjs'; 2 | 3 | import tests_json from './json-patch-tests/tests.json.mjs'; 4 | import spec_tests_json from './json-patch-tests/spec_tests.json.mjs'; 5 | 6 | const JSONtests = [ 7 | { 8 | name: 'tests.json', 9 | tests: tests_json 10 | }, 11 | { 12 | name: 'spec_tests.json', 13 | tests: spec_tests_json 14 | } 15 | ]; 16 | 17 | if (typeof Array.prototype.forEach != 'function') { 18 | Array.prototype.forEach = function(callback) { 19 | for (const i = 0; i < this.length; i++) { 20 | callback.apply(this, [this[i], i, this]); 21 | } 22 | }; 23 | } 24 | 25 | describe('json-patch-tests', function() { 26 | JSONtests.forEach(function(jsonTest) { 27 | describe(jsonTest.name, function() { 28 | jsonTest.tests.forEach(function(test) { 29 | if (test.disabled) { 30 | return; 31 | } 32 | const testName = test.comment || test.error || JSON.stringify(test.patch); 33 | if (test.expected) { 34 | it('should succeed: ' + testName, function() { 35 | const results = jsonpatch.applyPatch(test.doc, test.patch, true); 36 | test.doc = results.newDocument; 37 | expect(test.doc).toEqual(test.expected); 38 | }); 39 | } else if (test.error || test.patch[0].op === 'test') { 40 | it('should throw an error: ' + testName, function() { 41 | let errors = 0; 42 | try { 43 | const res = jsonpatch.applyPatch(test.doc, test.patch, true); 44 | if (res.test === false) { 45 | throw new Error('Test failed'); 46 | } 47 | } catch (e) { 48 | errors++; 49 | } 50 | if (test.error) { 51 | expect(errors).toBe(1); 52 | } else { 53 | expect(errors).toBe(0); 54 | } 55 | }); 56 | } else { 57 | throw new Error('invalid test case'); 58 | } 59 | }); 60 | }); 61 | }); 62 | }); -------------------------------------------------------------------------------- /test/spec/typings/typingsSpec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Run using `npm run test-typings` 3 | * The sole fact that this file compiles means that typings work 4 | * This follows how DefinitelyTyped tests work 5 | * @see https://stackoverflow.com/questions/49296151/how-to-write-tests-for-typescript-typing-definition 6 | */ 7 | 8 | import jsonpatch from '../../..'; 9 | import * as jsonpatchStar from '../../..'; 10 | import { applyPatch, Operation } from '../../..'; 11 | 12 | const document = { firstName: "Albert", contactDetails: { phoneNumbers: [] } }; 13 | 14 | const typedPatch = new Array({ op: "replace", path: "/firstName", value: "Joachim" }); 15 | const untypedPatch = [{ op: "replace", path: "/firstName", value: "Joachim" }]; 16 | 17 | const test_jsonpatch = jsonpatch.applyPatch(document, typedPatch).newDocument; 18 | const test_jsonpatchStar = jsonpatchStar.applyPatch(document, typedPatch).newDocument; 19 | const test_applyPatch = applyPatch(document, typedPatch).newDocument; 20 | 21 | // the below line would NOT compile with TSC 22 | // const test_applyPatch = applyPatch(document, untypedPatch).newDocument; -------------------------------------------------------------------------------- /test/spec/validateSpec.mjs: -------------------------------------------------------------------------------- 1 | import * as jsonpatch from '../../index.mjs'; 2 | 3 | describe('validate', function() { 4 | it('should return an empty array if the patch is valid', function() { 5 | const patch = [ 6 | { 7 | op: 'test', 8 | path: '/a/b/c', 9 | value: 'foo' 10 | }, 11 | { 12 | op: 'remove', 13 | path: '/a/b/c' 14 | }, 15 | { 16 | op: 'add', 17 | path: '/a/b/c', 18 | value: ['foo', 'bar'] 19 | }, 20 | { 21 | op: 'replace', 22 | path: '/a/b/c', 23 | value: 42 24 | }, 25 | { 26 | op: 'move', 27 | from: '/a/b/c', 28 | path: '/a/b/d' 29 | }, 30 | { 31 | op: 'copy', 32 | from: '/a/b/d', 33 | path: '/a/b/e' 34 | } 35 | ]; 36 | const error = jsonpatch.validate(patch); 37 | expect(error).toBeUndefined(); 38 | }); 39 | 40 | it('applyPatch should throw an error if the patch is not an array and validate is `true`', function() { 41 | expect(() => jsonpatch.applyPatch({}, {}, true)).toThrow(new jsonpatch.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY')); 42 | }); 43 | it('applyPatch should throw an error if the patch is not an array and validate is `true`', function() { 44 | expect(() => jsonpatch.applyPatch({}, null, true)).toThrow(new jsonpatch.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY')); 45 | }); 46 | it('applyPatch should throw list the index and the object of the faulty operation in the patch', function() { 47 | expect(() => 48 | jsonpatch.applyPatch( 49 | {}, 50 | [ 51 | { op: 'add', path: '/root', value: [] }, 52 | { op: 'add', path: '/root/2', value: 2 } // out of bounds 53 | ], 54 | true 55 | ) 56 | ).toThrow( 57 | new jsonpatch.JsonPatchError( 58 | 'The specified index MUST NOT be greater than the number of elements in the array', 59 | 'OPERATION_VALUE_OUT_OF_BOUNDS', 60 | 1, // the index of the faulty operation 61 | { op: 'add', path: '/root/2', value: 2 }, // the faulty operation 62 | { root: [] } // the tree after the first operation 63 | ) 64 | ); 65 | }); 66 | it('JsonPatchError should have a nice formatted message', function() { 67 | const message = "Some error message"; 68 | const name = "SOME_ERROR_NAME"; 69 | const index = 1; // op index 70 | const operation = JSON.stringify({ op: "replace", path: '/root', value: {} }, null, 2); 71 | const tree = JSON.stringify({ root: [] }, null, 2); 72 | 73 | const expectedError = new jsonpatch.JsonPatchError(message, name, index, operation, tree); 74 | 75 | /* 76 | Some error message 77 | name: SOME_ERROR_NAME 78 | index: 1 79 | operation: { 80 | "op": "replace", 81 | "path": "/root", 82 | "value": {} 83 | } 84 | tree: { 85 | "root": [] 86 | }" 87 | */ 88 | const expectedFormattedErrorMessage = message 89 | .concat("\nname: ") 90 | .concat(name, "\nindex: ") 91 | .concat(index, "\noperation: ") 92 | .concat(operation, "\ntree: ") 93 | .concat(tree); // don't use `` to support the loveliest browser: IE 94 | 95 | expect(expectedError.message).toEqual(expectedFormattedErrorMessage); 96 | }); 97 | 98 | 99 | it('should return an empty array if the operation is a valid object', function() { 100 | const error = jsonpatch.validate([ 101 | { 102 | op: 'add', 103 | value: 'foo', 104 | path: '/bar' 105 | } 106 | ]); 107 | expect(error).toBeUndefined(); 108 | }); 109 | 110 | it('should return an error if the operation is null', function() { 111 | const error = jsonpatch.validate([null]); 112 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 113 | expect(error.name).toBe('OPERATION_NOT_AN_OBJECT'); 114 | }); 115 | 116 | it('should return an error which is instance of Error and jsonpatch.JsonPatchError', function() { 117 | const error = jsonpatch.validate({}); 118 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 119 | expect(error instanceof Error).toBe(true); 120 | expect(error.name).toBe('SEQUENCE_NOT_AN_ARRAY'); 121 | }); 122 | 123 | it('should return an error that contains the cloned patch and the patched object', function() { 124 | const tree = { 125 | name: 'Elvis', 126 | cars: [] 127 | }; 128 | const sequence = [ 129 | { 130 | op: 'remove', 131 | path: '/name/first' 132 | } 133 | ]; 134 | const error = jsonpatch.validate(sequence, tree); 135 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 136 | expect(JSON.stringify(error.operation)).toBe(JSON.stringify(sequence[0])); 137 | expect(JSON.stringify(error.tree)).toBe(JSON.stringify(tree)); 138 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 139 | }); 140 | 141 | it('should return an error if the operation is undefined', function() { 142 | const error = jsonpatch.validate([undefined]); 143 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 144 | expect(error.name).toBe('OPERATION_NOT_AN_OBJECT'); 145 | }); 146 | 147 | it('should return an error if the operation is an array', function() { 148 | const error = jsonpatch.validate([[]]); 149 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 150 | expect(error.name).toBe('OPERATION_NOT_AN_OBJECT'); 151 | }); 152 | 153 | it('should return an error if the operation "op" property is not a string', function() { 154 | const error = jsonpatch.validate([ 155 | { 156 | path: '/a/b/c' 157 | } 158 | ]); 159 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 160 | expect(error.name).toBe('OPERATION_OP_INVALID'); 161 | }); 162 | 163 | it('should return an error if the operation "path" property is not a string', function() { 164 | const error = jsonpatch.validate([ 165 | { 166 | op: 'remove', 167 | value: 'foo' 168 | } 169 | ]); 170 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 171 | expect(error.name).toBe('OPERATION_PATH_INVALID'); 172 | }); 173 | 174 | it('should return an error if an "add" operation is missing "value" property', function() { 175 | const error = jsonpatch.validate([ 176 | { 177 | op: 'add', 178 | path: '/a/b/c' 179 | } 180 | ]); 181 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 182 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 183 | }); 184 | 185 | it('should return an error if an "add" operation "value" property is "undefined"', function() { 186 | const error = jsonpatch.validate([ 187 | { 188 | op: 'add', 189 | path: '/a/b/c', 190 | value: undefined 191 | } 192 | ]); 193 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 194 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 195 | }); 196 | 197 | it('should return an error if a "replace" operation is missing "value" property', function() { 198 | const error = jsonpatch.validate([ 199 | { 200 | op: 'replace', 201 | path: '/a/b/c' 202 | } 203 | ]); 204 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 205 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 206 | }); 207 | 208 | it('should return an error if a "replace" operation "value" property is "undefined"', function() { 209 | const error = jsonpatch.validate([ 210 | { 211 | op: 'replace', 212 | path: '/a/b/c', 213 | value: undefined 214 | } 215 | ]); 216 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 217 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 218 | }); 219 | 220 | it('should return an error if a "test" operation is missing "value" property', function() { 221 | const error = jsonpatch.validate([ 222 | { 223 | op: 'test', 224 | path: '/a/b/c' 225 | } 226 | ]); 227 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 228 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 229 | }); 230 | 231 | it('should return an error if a "test" operation "value" property is "undefined"', function() { 232 | const error = jsonpatch.validate([ 233 | { 234 | op: 'test', 235 | path: '/a/b/c', 236 | value: undefined 237 | } 238 | ]); 239 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 240 | expect(error.name).toBe('OPERATION_VALUE_REQUIRED'); 241 | }); 242 | 243 | it('should return an error if an "add" operation "value" contains "undefined"', function() { 244 | const error = jsonpatch.validate([ 245 | { 246 | op: 'add', 247 | path: '/a/b/c', 248 | value: { 249 | foo: undefined 250 | } 251 | } 252 | ]); 253 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 254 | expect(error.name).toBe('OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED'); 255 | }); 256 | 257 | it('should return an error if a "replace" operation "value" contains "undefined"', function() { 258 | const error = jsonpatch.validate([ 259 | { 260 | op: 'replace', 261 | path: '/a/b/c', 262 | value: { 263 | foos: [undefined] 264 | } 265 | } 266 | ]); 267 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 268 | expect(error.name).toBe('OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED'); 269 | }); 270 | 271 | it('should return an error if a "test" operation "value" contains "undefined"', function() { 272 | const error = jsonpatch.validate([ 273 | { 274 | op: 'test', 275 | path: '/a/b/c', 276 | value: { 277 | foo: { 278 | bars: [undefined] 279 | } 280 | } 281 | } 282 | ]); 283 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 284 | expect(error.name).toBe('OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED'); 285 | }); 286 | 287 | it('should return an error if a "move" operation is missing "from" property', function() { 288 | const error = jsonpatch.validate([ 289 | { 290 | op: 'move', 291 | path: '/a/b/c' 292 | } 293 | ]); 294 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 295 | expect(error.name).toBe('OPERATION_FROM_REQUIRED'); 296 | }); 297 | 298 | it('should return an error if a "copy" operation is missing "from" property', function() { 299 | const error = jsonpatch.validate([ 300 | { 301 | op: 'copy', 302 | path: '/a/b/c' 303 | } 304 | ]); 305 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 306 | expect(error.name).toBe('OPERATION_FROM_REQUIRED'); 307 | }); 308 | 309 | it('should return an error if the "op" property is invalid', function() { 310 | const error = jsonpatch.validate([ 311 | { 312 | op: 'foobar', 313 | path: '/a/b/c' 314 | } 315 | ]); 316 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 317 | expect(error.name).toBe('OPERATION_OP_INVALID'); 318 | }); 319 | 320 | it('should return error replacing an unexisting path', function() { 321 | let sequence; 322 | let error; 323 | const tree = { 324 | '': 'empty string is a valid key', 325 | name: 'Elvis', 326 | cars: [ 327 | { 328 | brand: 'Jaguar' 329 | } 330 | ], 331 | address: {} 332 | }; 333 | 334 | sequence = [ 335 | { 336 | op: 'replace', 337 | path: '/name/first', 338 | value: '' 339 | } 340 | ]; 341 | error = jsonpatch.validate(sequence, tree); 342 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 343 | 344 | sequence = [ 345 | { 346 | op: 'replace', 347 | path: '/firstName', 348 | value: '' 349 | } 350 | ]; 351 | error = jsonpatch.validate(sequence, tree); 352 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 353 | 354 | sequence = [ 355 | { 356 | op: 'replace', 357 | path: '/cars/0/name', 358 | value: '' 359 | } 360 | ]; 361 | error = jsonpatch.validate(sequence, tree); 362 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 363 | }); 364 | 365 | it('should return error removing an unexisting path', function() { 366 | const tree = { 367 | name: 'Elvis', 368 | cars: [] 369 | }; 370 | const sequence = [ 371 | { 372 | op: 'remove', 373 | path: '/name/first' 374 | } 375 | ]; 376 | const error = jsonpatch.validate(sequence, tree); 377 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 378 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 379 | }); 380 | 381 | it('should allow adding property "b" in "a"', function() { 382 | const tree = { 383 | a: { 384 | foo: 1 385 | } 386 | }; 387 | const sequence = [ 388 | { 389 | op: 'add', 390 | path: '/a/b', 391 | value: 'sample' 392 | } 393 | ]; 394 | 395 | const error = jsonpatch.validate(sequence, tree); 396 | expect(error).toBeUndefined(); 397 | }); 398 | 399 | it('should report error because "a" does not exist', function() { 400 | const tree = { 401 | q: { 402 | bar: 2 403 | } 404 | }; 405 | const sequence = [ 406 | { 407 | op: 'add', 408 | path: '/a/b', 409 | value: 'sample' 410 | } 411 | ]; 412 | 413 | const error = jsonpatch.validate(sequence, tree); 414 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 415 | expect(error.name).toBe('OPERATION_PATH_CANNOT_ADD'); 416 | }); 417 | 418 | it('should report error because "a" is null', function() { 419 | const tree = { 420 | q: { 421 | bar: 2 422 | }, 423 | a: null 424 | }; 425 | const sequence = [ 426 | { 427 | op: 'add', 428 | path: '/a/b', 429 | value: 'sample' 430 | } 431 | ]; 432 | 433 | const error = jsonpatch.validate(sequence, tree); 434 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 435 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 436 | }); 437 | 438 | it('should report error because "a" is not an object', function() { 439 | const tree = { 440 | q: { 441 | bar: 2 442 | }, 443 | a: 42 444 | }; 445 | const sequence = [ 446 | { 447 | op: 'add', 448 | path: '/a/b', 449 | value: 'sample' 450 | } 451 | ]; 452 | 453 | const error = jsonpatch.validate(sequence, tree); 454 | expect(error instanceof jsonpatch.JsonPatchError).toBe(true); 455 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 456 | }); 457 | 458 | it('should return error when replacing a removed path', function() { 459 | const tree = { 460 | name: 'Elvis' 461 | }; 462 | const sequence = [ 463 | { 464 | op: 'remove', 465 | path: '/name' 466 | }, 467 | { 468 | op: 'replace', 469 | path: '/name', 470 | value: 'Freddie' 471 | } 472 | ]; 473 | const error = jsonpatch.validate(sequence, tree); 474 | expect(error.name).toBe('OPERATION_PATH_UNRESOLVABLE'); 475 | }); 476 | 477 | it('should allow to override validator to add custom validation', function() { 478 | const tree = { 479 | password: 'Elvis' 480 | }; 481 | const sequence = [ 482 | { 483 | op: 'replace', 484 | path: '/password', 485 | value: 'Elvis123' 486 | }, 487 | { 488 | op: 'replace', 489 | path: '/password', 490 | value: 'Presley123' 491 | }, 492 | { 493 | op: 'replace', 494 | path: '/password' 495 | } 496 | ]; 497 | 498 | 499 | const validator = function(operation, index, tree, existingPath) { 500 | throw new jsonpatch.JsonPatchError( 501 | 'Operation `value` property must not contain the old value', 502 | 'OPERATION_VALUE_MUST_NOT_CONTAIN_OLD_VALUE', 503 | index, 504 | operation, 505 | tree 506 | ); 507 | } 508 | const customError = jsonpatch.validate(sequence, tree, validator); 509 | expect(customError.index).toBe(0); 510 | expect(customError.name).toBe('OPERATION_VALUE_MUST_NOT_CONTAIN_OLD_VALUE'); 511 | }); 512 | 513 | it('should pass replacing the tree root', function() { 514 | const tree = { 515 | password: 'Elvis' 516 | }; 517 | const sequence = [ 518 | { 519 | op: 'replace', 520 | path: '', 521 | value: {} 522 | } 523 | ]; 524 | 525 | const error = jsonpatch.validate(sequence, tree); 526 | expect(error).toBeUndefined(); 527 | }); 528 | 529 | it('should return error moving from an unexisting path', function() { 530 | const tree = { 531 | name: 'Elvis' 532 | }; 533 | const sequence = [ 534 | { 535 | op: 'move', 536 | from: '/a/b/c', 537 | path: '/name' 538 | } 539 | ]; 540 | const error = jsonpatch.validate(sequence, tree); 541 | expect(error.name).toBe('OPERATION_FROM_UNRESOLVABLE'); 542 | }); 543 | 544 | it('should return error copying from an unexisting path', function() { 545 | const tree = { 546 | name: 'Elvis' 547 | }; 548 | const sequence = [ 549 | { 550 | op: 'copy', 551 | from: '/a/b/c', 552 | path: '/name' 553 | } 554 | ]; 555 | const error = jsonpatch.validate(sequence, tree); 556 | expect(error.name).toBe('OPERATION_FROM_UNRESOLVABLE'); 557 | }); 558 | 559 | it('should throw OPERATION_PATH_INVALID when applying patch without path', function() { 560 | const a = {}; 561 | let ex = null; 562 | 563 | try { 564 | jsonpatch.applyPatch( 565 | a, 566 | [ 567 | { 568 | op: 'replace', 569 | value: '' 570 | } 571 | ], 572 | true 573 | ); 574 | } catch (e) { 575 | ex = e; 576 | } 577 | 578 | expect(ex.name).toBe('OPERATION_PATH_INVALID'); 579 | }); 580 | 581 | it('should throw OPERATION_PATH_INVALID when applying patch with an invalid path. Issue #77.', function() { 582 | const a = {}; 583 | let ex = null; 584 | 585 | try { 586 | jsonpatch.applyPatch( 587 | a, 588 | [ 589 | { 590 | op: 'replace', 591 | value: '', 592 | path: 'foo' // no preceding "/" 593 | } 594 | ], 595 | true 596 | ); 597 | } catch (e) { 598 | ex = e; 599 | } 600 | expect(ex.name).toBe('OPERATION_PATH_INVALID'); 601 | }); 602 | 603 | it('should throw OPERATION_OP_INVALID when applying patch without operation', function() { 604 | const a = {}; 605 | let ex = null; 606 | 607 | try { 608 | jsonpatch.applyPatch( 609 | a, 610 | [ 611 | { 612 | path: '/foo', 613 | value: '' 614 | } 615 | ], 616 | true 617 | ); 618 | } catch (e) { 619 | ex = e; 620 | } 621 | 622 | expect(ex.name).toBe('OPERATION_OP_INVALID'); 623 | }); 624 | 625 | it('should throw OPERATION_VALUE_REQUIRED when applying patch without value', function() { 626 | const a = {}; 627 | let ex = null; 628 | 629 | try { 630 | jsonpatch.applyPatch( 631 | a, 632 | [ 633 | { 634 | path: '/foo', 635 | op: 'add' 636 | } 637 | ], 638 | true 639 | ); 640 | } catch (e) { 641 | ex = e; 642 | } 643 | 644 | expect(ex.name).toBe('OPERATION_VALUE_REQUIRED'); 645 | }); 646 | 647 | it('should not modify patch value of type array (issue #76)', function () { 648 | const patches = [ 649 | {op: 'add', path: '/foo', value: []}, 650 | {op: 'add', path: '/foo/-', value: 1} 651 | ]; 652 | jsonpatch.validate(patches, {}); 653 | 654 | expect(patches).toEqual([ 655 | {op: 'add', path: '/foo', value: []}, 656 | {op: 'add', path: '/foo/-', value: 1} 657 | ]); 658 | }); 659 | 660 | it('should not modify patch value of type object (issue #76)', function () { 661 | const patches = [ 662 | {op: 'add', path: '/foo', value: {}}, 663 | {op: 'add', path: '/foo/bar', value: 1} 664 | ]; 665 | jsonpatch.validate(patches, {}); 666 | 667 | expect(patches).toEqual([ 668 | {op: 'add', path: '/foo', value: {}}, 669 | {op: 'add', path: '/foo/bar', value: 1} 670 | ]); 671 | }); 672 | }); 673 | -------------------------------------------------------------------------------- /test/spec/webpack/importSpec.build.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);var o={};n.r(o),n.d(o,"JsonPatchError",function(){return y}),n.d(o,"deepClone",function(){return m}),n.d(o,"getValueByPointer",function(){return A}),n.d(o,"applyOperation",function(){return _}),n.d(o,"applyPatch",function(){return E}),n.d(o,"applyReducer",function(){return g}),n.d(o,"validator",function(){return P}),n.d(o,"validate",function(){return x}),n.d(o,"_areEquals",function(){return T});var r={};n.r(r),n.d(r,"unobserve",function(){return C}),n.d(r,"observe",function(){return R}),n.d(r,"generate",function(){return I}),n.d(r,"compare",function(){return B});var i={};n.r(i),n.d(i,"JsonPatchError",function(){return w}),n.d(i,"deepClone",function(){return s}),n.d(i,"escapePathComponent",function(){return h}),n.d(i,"unescapePathComponent",function(){return l}),n.d(i,"default",function(){return S}),n.d(i,"getValueByPointer",function(){return A}),n.d(i,"applyOperation",function(){return _}),n.d(i,"applyPatch",function(){return E}),n.d(i,"applyReducer",function(){return g}),n.d(i,"validator",function(){return P}),n.d(i,"validate",function(){return x}),n.d(i,"_areEquals",function(){return T}),n.d(i,"unobserve",function(){return C}),n.d(i,"observe",function(){return R}),n.d(i,"generate",function(){return I}),n.d(i,"compare",function(){return B}); 2 | /*! 3 | * https://github.com/Starcounter-Jack/JSON-Patch 4 | * (c) 2017-2022 Joachim Wester 5 | * MIT licensed 6 | */ 7 | var a,u=(a=function(e,t){return(a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}a(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),p=Object.prototype.hasOwnProperty;function c(e,t){return p.call(e,t)}function f(e){if(Array.isArray(e)){for(var t=new Array(e.length),n=0;n=48&&t<=57))return!1;n++}return!0}function h(e){return-1===e.indexOf("/")&&-1===e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function l(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}function v(e,t){var n=[e];for(var o in t){var r="object"==typeof t[o]?JSON.stringify(t[o],null,2):t[o];void 0!==r&&n.push(o+": "+r)}return n.join("\n")}var w=function(e){function t(t,n,o,r,i){var a=this.constructor,u=e.call(this,v(t,{name:n,index:o,operation:r,tree:i}))||this;return u.name=n,u.index=o,u.operation=r,u.tree=i,Object.setPrototypeOf(u,a.prototype),u.message=v(t,{name:n,index:o,operation:r,tree:i}),u}return u(t,e),t}(Error),y=w,m=s,b={add:function(e,t,n){return e[t]=this.value,{newDocument:n}},remove:function(e,t,n){var o=e[t];return delete e[t],{newDocument:n,removed:o}},replace:function(e,t,n){var o=e[t];return e[t]=this.value,{newDocument:n,removed:o}},move:function(e,t,n){var o=A(n,this.path);o&&(o=s(o));var r=_(n,{op:"remove",path:this.from}).removed;return _(n,{op:"add",path:this.path,value:r}),{newDocument:n,removed:o}},copy:function(e,t,n){var o=A(n,this.from);return _(n,{op:"add",path:this.path,value:s(o)}),{newDocument:n}},test:function(e,t,n){return{newDocument:n,test:T(e[t],this.value)}},_get:function(e,t,n){return this.value=e[t],{newDocument:n}}},O={add:function(e,t,n){return d(t)?e.splice(t,0,this.value):e[t]=this.value,{newDocument:n,index:t}},remove:function(e,t,n){return{newDocument:n,removed:e.splice(t,1)[0]}},replace:function(e,t,n){var o=e[t];return e[t]=this.value,{newDocument:n,removed:o}},move:b.move,copy:b.copy,test:b.test,_get:b._get};function A(e,t){if(""==t)return e;var n={op:"_get",path:t};return _(e,n),n.value}function _(e,t,n,o,r,i){if(void 0===n&&(n=!1),void 0===o&&(o=!0),void 0===r&&(r=!0),void 0===i&&(i=0),n&&("function"==typeof n?n(t,0,e,t.path):P(t,0)),""===t.path){var a={newDocument:e};if("add"===t.op)return a.newDocument=t.value,a;if("replace"===t.op)return a.newDocument=t.value,a.removed=e,a;if("move"===t.op||"copy"===t.op)return a.newDocument=A(e,t.from),"move"===t.op&&(a.removed=e),a;if("test"===t.op){if(a.test=T(e,t.value),!1===a.test)throw new y("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a.newDocument=e,a}if("remove"===t.op)return a.removed=e,a.newDocument=null,a;if("_get"===t.op)return t.value=e,a;if(n)throw new y("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",i,t,e);return a}o||(e=s(e));var u=(t.path||"").split("/"),p=e,c=1,f=u.length,h=void 0,v=void 0,w=void 0;for(w="function"==typeof n?n:P;;){if((v=u[c])&&-1!=v.indexOf("~")&&(v=l(v)),r&&("__proto__"==v||"prototype"==v&&c>0&&"constructor"==u[c-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(n&&void 0===h&&(void 0===p[v]?h=u.slice(0,c).join("/"):c==f-1&&(h=t.path),void 0!==h&&w(t,0,e,h)),c++,Array.isArray(p)){if("-"===v)v=p.length;else{if(n&&!d(v))throw new y("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",i,t,e);d(v)&&(v=~~v)}if(c>=f){if(n&&"add"===t.op&&v>p.length)throw new y("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",i,t,e);if(!1===(a=O[t.op].call(t,p,v,e)).test)throw new y("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a}}else if(c>=f){if(!1===(a=b[t.op].call(t,p,v,e)).test)throw new y("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a}if(p=p[v],n&&c0)throw new y('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,n);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new y("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",t,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new y("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",t,e,n);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&function e(t){if(void 0===t)return!0;if(t)if(Array.isArray(t)){for(var n=0,o=t.length;n0&&(e.patches=[],e.callback&&e.callback(o)),o}function L(e,t,n,o,r){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=f(t),a=f(e),u=!1,p=a.length-1;p>=0;p--){var d=e[v=a[p]];if(!c(t,v)||void 0===t[v]&&void 0!==d&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(r&&n.push({op:"test",path:o+"/"+h(v),value:s(d)}),n.push({op:"remove",path:o+"/"+h(v)}),u=!0):(r&&n.push({op:"test",path:o,value:e}),n.push({op:"replace",path:o,value:t}),!0);else{var l=t[v];"object"==typeof d&&null!=d&&"object"==typeof l&&null!=l&&Array.isArray(d)===Array.isArray(l)?L(d,l,n,o+"/"+h(v),r):d!==l&&(!0,r&&n.push({op:"test",path:o+"/"+h(v),value:s(d)}),n.push({op:"replace",path:o+"/"+h(v),value:s(l)}))}}if(u||i.length!=a.length)for(p=0;p { 6 | if(env && env.NODE_ENV === "test") { 7 | return [ 8 | { 9 | entry: './test/spec/webpack/importSpec.src.js', 10 | mode: 'production', 11 | output: { 12 | path: path.resolve(__dirname, 'test/spec/webpack'), 13 | filename: 'importSpec.build.js' 14 | }, 15 | target: 'node', 16 | resolve: { 17 | extensions: ['.js'] 18 | } 19 | } 20 | ]; 21 | } 22 | else { 23 | return [ 24 | { 25 | entry: './index.js', 26 | mode: 'production', 27 | optimization: { 28 | minimize: false 29 | }, 30 | output: { 31 | filename: 'fast-json-patch.js', 32 | library: 'jsonpatch', 33 | libraryTarget: 'var' 34 | }, 35 | resolve: { 36 | extensions: ['.js'] 37 | }, 38 | plugins: [ 39 | new webpack.BannerPlugin('fast-json-patch, version: ' + package['version']) 40 | ] 41 | }, 42 | { 43 | entry: './index.js', 44 | mode: 'production', 45 | output: { 46 | filename: 'fast-json-patch.min.js', 47 | library: 'jsonpatch', 48 | libraryTarget: 'var' 49 | }, 50 | resolve: { 51 | extensions: ['.js'] 52 | }, 53 | plugins: [ 54 | new webpack.BannerPlugin('fast-json-patch, version: ' + package['version']) 55 | ] 56 | } 57 | ]; 58 | } 59 | }; 60 | 61 | 62 | 63 | 64 | --------------------------------------------------------------------------------