diff --git a/EXTENDED.md b/EXTENDED.md new file mode 100644 index 0000000..dc218b8 --- /dev/null +++ b/EXTENDED.md @@ -0,0 +1,793 @@ +# Extended Patch Operations + +> ...or how to add non RFC-6902 operations to your patch chain + +## The story so far... + +The JSON-Patch library was built first and foremost as a compliant implementation of the RFC-6902 specification, which defines the rules and syntax for modifying (strict) JSON documents. + +The RFC includes the following operations: + +- **add** +- **copy** +- **move** +- **remove** +- **replace** +- **test** + +This allows for basic modification of values/structure relative to the source JSON document. In the vast majority of use-cases this is all one needs. There are limitations to the core funcionality however. + +For instance, an RFC-compliant patch operation does not have the ability to perform any logic/transformations relative to the value at a given path. In other words, you are only able to add new/replace existing key-value-pairs and array elements with a **static value supplied in the operation's `value` property**. You can also indiscriminately remove existing values. (assuming a valid path to the target key exists) + +The core RFC operations do not care about what the existing values (or absence thereof) mean. Nor should they. The intent of JSON Patch (the specification) is primarily structural in nature. It only cares about operations that make structural modifications (including basic value replacement) to the JSON document. + +What if you wanted to do more? What if there were operations that could perform structural and value mutations based on some interpretation of existing values/structure w/in the source document (or the outside world; a.k.a information external to the source JSON itself)? + +For example: + +- Add `25` to the number at a `path` +- Perform a `toUpperCase` to a string at a `path` +- If value at `path` is === 'foo', replace it with `5`, else leave it as is +- Update value at `path` with current unix timestamp at moment of operation processing +- Conditionally remove or add structure +- Conditionally insert a deeply-nested tree at a currently non-existent path +- Extract information from the document during patching (e.g. logging, reading/parsing for other processes) +- etc. + +Enter: + +## Extended Operations! + +The idea behind extended operations is to enable such scenarios as listed above by allowing a user (of this library) to define their own operations that **"extend"** the functionality already provided by the RFC-6902 specification. + +In this way, a user can patch a JSON document with a combination of RFC-compliant operations and their own operations without having to make separate passes or implementing their own JSON Pointer (RFC-6901) parser. Instead, they may simply register the extended operations with the library, and apply a patch including them. The library will route operation handling accordingly (core vs. extended). + +### Use of Extended Operations is 100% Opt-In + +- The core JSON-Patch API remains unchanged (function signatures are the same) +- By default, JSON-Patch will reject any non-RFC-6902 operations +- Extended Operations **NEVER** change how RFC-6902 operations are processed +- No surprises; Similar conventions; It _should_ be possible to recreate most of the functionality of the RFC-6902 operations as extended operations (although discouraged - keep it DRY, use existing operations when applicable; they're guaranteed to work as described) +- Small API footprint; just a few functions have been added, but they may be ignored if you don't need them. +- All core unit tests pass w/out modification/accommodation for extended operation functionality. + +### Extended Operation Object Structure + +Extended operation objects are structured very much like their RFC-6902 cousins and must follow specific naming conventions to avoid any ambiguity with RFC ops. + +A basic extended operation object **must** include the following **REQUIRED** properties: + +- **op** - string literal 'x'; always and only 'x'; indicates extended operation namespace +- **xid** - string; matching the pattern `/x-[a-z0-9-]+$/`; this is the extended operation's id; it is analogous with `op` in the RFC. NOTE: the required prefix `'x-'` is a convention choice to help visually discern extended from RFC ops +- **path** - string; RFC-6901-_ish_ (more about this below) JSON Pointer path + +##### Example Basic Extended Operation Object (as JSON) + +```json +{ "op": "x", "xid": "x-foo", "path": "/a/2/b" } +``` + +As you can see it's not much different from the RFC ops. + +Extended operation objects may also include the following **OPTIONAL** properties: + +- **value** - any type; (`undefined` is treated as not present); Unlike some RFC ops, this prop is optional as it is up to the operation's implementation whether or not it is used. [**default**: `undefined`] +- **args** - Array<any type>; (`undefined` is treated as not present. Individual `undefined` elements are allowed however - in JS). The args array is a way for additional data to be passed along to the associated operator function(s) that implement the logic of the extended operation. It can be thought of as an extension to the `value` property of RFC ops; for convenience. It's up to the developer to determine if/how `args` are used. [**default**: `undefined`] +- **resolve** - boolean; Flag to allow for non-existent paths to be **CREATED** (or "resolved") before invoking the operator function. [**default**: `false`] (more about this below) + +##### Example Advanced Extended Operation Object (as JSON) + +```json +{ + "op": "x", + "xid": "x-bar", + "path": "/a/b/c", + "value": 42, + "args": [null, 54, "<"], + "resolve": true +} +``` + +That's still fairly compact and similar to the RFC ops. + +## RFC-6901 (JSON Pointer Extensions) + +Path strings are defined by the RFC-6901 specification. Extended operations resolve paths just like their RFC-6902 counterparts, but with a couple of additions of note: + +- **'/'** A single slash path is **NOT** allowed for extended operations since its interpretation is ambiguous without knowing what the corresponding operator(s) are intended to do; e.g. replace top-level document or make changes at the root of the top-level document, etc. + +- **'--'** (double dash) **applies to Arrays only**; This is the _"end element sentinel"_ It is a dynamic placeholder for the last existing (highest) index of an array. This is **NOT** RFC-6901 compliant; It only applies to extended operation paths. It is a cousin to the **'-'** (single dash) "append sentinel" _(which represents the index after the last existing element)_ Note: if used on an object, it will represent a literal double dash key name. + +#### End Element Sentinel Examples + +```js +const arr = [0, 1, 2, 3, 4]; +// this path represents index 4 relative to 'arr' +const path = "/--"; // gets transformed to '/4' + +// may be nested +const arr2 = [0, 1, 2, 3, [4, "hi"]]; +// represents path to value 'hi' +const path2 = "/--/--"; // gets transformed to '/4/1 + +// also works with arrays nested in objects and vice versa +const obj = { + a: { + b: [ + "hi", + { + c: [0, 1, 2], + d: "foo", + }, + ], + }, +}; +// represents nested array element with value 2 +const path3 = "/a/b/--/c/--"; // gets transformed to '/a/b/1/c/2' +``` + +# Extended Operation Configuration - Creating Your Own Custom Extended Operations + +In a nutshell, creating an extended operation involves authoring a JavaScript `Object` with exactly `3` specific properties: + +- **'arr'** +- **'obj'** +- **'validator'** + +The TypeScript signature for this object is: + +```ts +interface ExtendedMutationOperationConfig { + readonly arr: ArrayOperator | undefined>; + readonly obj: ObjectOperator | undefined>; + readonly validator: Validator; +} +``` + +ALL values for these properties are **required** to be `functions` (shown types will be discussed later) + +### Operators + +Properties `arr` and `obj` are called "**operators**", or "operator functions". These functions define the (potential) mutation behavior of the **Extended Operation** with respect to the type of object at the given '_`path`_'; `Array` or `Object` ('arr' or 'obj', respectively) + +Operators **must** return an `Object` with the following signature: + +```ts +// return type +type ExtendedOperationResult = OperationResult | undefined; + +// where OperationResult === +interface OperationResult { + removed?: any; + newDocument: T; +} +``` + +#### Operator: Function vs. Arrow Function + +**IMPORTANT!!!** +When defining your operator functions, you should use [traditional function expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function) +**NOT** [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) + +Operator functions are invoked via `Function.prototype.call` in order to provide the context of the `extended operation object` as `this` inside of the operator function. Arrow functions are not `Function Objects` proper, and do not allow `call`-ing with context other than any closures that may have been created at the scope on which they were defined. + +Arrow functions **WILL NOT** have access to the operation object by using '`this.`' + +#### Array Operator + +The array operator (identified by property '`arr`') is a JavaScript `function` with the following signature: + +```ts +interface ArrayOperator { + (arr: Array, i: number, document: T): ExtendedOperationResult; +} +``` + +- **arr** - is a reference to the current parent object (Array) at the given `path`. This is the array that may be mutated by the operator at index `i`. (this represents the 2nd to last path component in the source document) +- **i** - the numeric index (or key) to be mutated/inspected +- **document** - this is the root-level source document to which `arr` belongs. It should be treated as _read only_ (though there's nothing enforcing this) Modifications should only be made to `arr` at index `i`. + +##### example arr operator + +```js +// assume extended operator object is +const xop = {op: 'x', xid: 'x-foo', path: '/0', value: 'hi'}; + +// source document is +const source = ['first']; + +const config = { + /** arr OPERATOR + * this operator adds/replaces value at path + */ + arr: function(arr, i, document) { + arr[i] = this.value; // this.value === 'hi'; 'this' is the 'xop' object above + return { newDocument: document }; + }; + /* ...rest of config... */ +}; + +// the result of this operation when applied to source array is +['hi'] +``` + +**Note**: `document` is mutated by the assignment of `arr[i]` since `arr` is a reference to an array object nested somewhere within the document (in this case they are the same reference). The 'root' ref of the modified document must be returned. By convention, you should always return the `document` as `newDocument`. + +#### Object Operator + +The object operator (identified by property '`obj`') is a JavaScript `function` with the following signature: + +```ts +interface ObjectOperator { + (obj: T, key: string, document: T): ExtendedOperationResult; +} +``` + +- **obj** - is a reference to the current parent object (Object) at the given `path`. This is the object that may be mutated/inspected by the operator at key `key`. (this represents the 2nd to last path component in the source document) +- **key** - the string property name (or key) to be mutated/inspected +- **document** - this is the root-level source document to which `obj` belongs. It should be treated as _read only_ (though there's nothing enforcing this) Modifications should only be made to `obj` at `key`. + +##### example obj operator + +```js +// assume extended operator object is +const xop = {op: 'x', xid: 'x-foo', path: '/a/b/c', value: 'hi'}; + +// source document is +const source = { + a: { + b: {}, + }, +}; + +const config = { + /** obj OPERATOR + * this operator adds/replaces value at path + */ + obj: function(obj, key, document) { + obj[key] = this.value; // this.value === 'hi'; 'this' is the 'xop' object above + return { newDocument: document }; + }; + /* ...rest of config... */ +}; + +// the result of this operation when applied to source array is +{ + a: { + b: { + c: 'hi', + }, + }, +} +``` + +**Note**: `document` is mutated by the assignment of `obj[key]` since `obj` is a reference to an object nested somewhere within the document. The 'root' ref of the modified document must be returned. By convention, you should always return the `document` as `newDocument`. + +### Signaling a "No Op" - Making No changes + +A distinguishing feature of extended operations is that they allow their operators to determine whether or not to perform a mutation at all without having to terminate the patch with an `Error` (exception). + +An operator may perform some conditional check first and then decide that no modifications are necessary by explicitly returning `undefined` _instead_ of an `Object` with a 'newDocument' property. + +**NOTE**: missing path components created via "**forced path resolution**" (see below) will not persist when the operator returns `undefined`; ensuring that it is a true "No Op" with no side effects. + +### Item Removal + +It is possible for an operator to remove a value at `i` or `key`. However, nothing prevents your operator implementation from removing other things **"off-path"**. But the chances of these removals actually propagating to the final result document are not great. + +It's best to stick with **ONLY** removing the item at `arr[i]` or `obj[key]` should you need to do so. And even then, the RFC spec already provides operations to do just that. Extended operations with removals can be useful in cases where you may want to perform removal based upon some condition that can only be determined when patching is executing. + +If you do remove something at the given path, **ALWAYS INCLUDE** a non-`undefined` value for the property `removed` in your operator's return value (Object) In most cases this should be the value of the thing that was removed. The extended operation processor itself does not care what the actual value is (but your callers might). The processor will look for any non-`undefined` value in order to perform some extra housekeeping in order to preserve the removal in the final result of the patch. + +```js +// e.g. operator return value includes the removed value +return { newDocument: document, removed: 42 }; +``` + +#### Nota Bene + +In most cases you should be using the standard RFC-6902 `remove` and `move` operations for their associated purposes. The extended operation processor is **NOT** able to determine the intention behind your operator implementations, and as such it cannot always determine which parts of the document tree have been removed and will output an erroneous patched result document when "off-path" removals have occured during processing. + +Also, removal on a non-existent path (via "forced path resolution" or not) results in an `Error`. You can't remove something that doesn't exist. + +### Operator Errors + +Should you encounter a condition in which you'd like your operator to signal a **termination** of the entire patching process (as opposed to skipping the operation with a No Op), simply throw an `Error` from your operator function. + +It is recommended to throw `JsonPatchError` and use the error name '`OPERATION_X_OPERATOR_EXCEPTION`' (defined in [src/helpers.ts](src/helpers.ts)) to better identify the source of problems. + +### Validator Function + +The validator function is an extension to JSON-Patch's (the library) default operation validation. This function is not required to be implemented, but is required to exist in the extended operation's configuration object. + +The signature of the validator is: + +```ts +interface Validator { + ( + operation: Operation, + index: number, + document: T, + existingPathFragment: string + ): void; +} +``` + +The parameters are: + +- **'operation'** - the `extended operation object` +- **'index'** - not always present - when bulk validating a patch, the current index of the operation object in the patch array +- **'document'** - the source document tree +- **'existingPathFragment'** - not always present - current path during path resolution phase of current patch operation + +Note that the validator function does not return a value. This is because a validator's purpose is to ensure the `operation object` is valid against its intended purpose as well as the state of the source document. In other words, an invalid state indicates that there is an error and the patching process should be terminated. + +Therefore, the implementation of a validator must throw an `Error` when an invalid state is detected. Return values are ignored. + +In most cases the validator should be used to check the values in the `extended operation object` against the operational semantics of the operation. In other words, "Do the values of props `value` and/or `args` make sense in the context of this operation?" + +#### Example Validator Function + +```js +validator: (op, i, d, ep) => { + // validate operation inputs + if (typeof op.value !== "number") { + throw new jsonpatch.JsonPatchError( + "x operation `value` must be a number", + "OPERATION_VALUE_REQUIRED", + i, + op, + d + ); + } +}; + +// the validator above would reject the following operation object: +{op: 'x', xid: 'x-bad', path: '/a/b/c', value: 'hi there'} +``` + +**NOTE**: unlike operator functions, a validator function **MAY** be implemented as an `arrow function` (and probably should be) + +#### Example "bypass" Validator + +You might not need/want to implement any additional validation for your extended operation. In this case, just supply an empty function (reminder: a validator function must always be included in a config object, even if it doesn't do anything): + +```js +const cfg = { + /* ... operator functions ... */ + + // No Op validator - a function value is always required + validator: () => {}, +}; +``` + +# Resolving Non-Existent Path Components - Forced Path Resolution + +Extended operations allow for actions to be performed at paths that do not exist on the current source document being patched (if desired). This is called "forced path resolution", and is flagged by passing the KVP '`resolve: true`' along with the extended operation in question: + +```js +{resolve: true, /*other op properties...*/} +``` + +**By DEFAULT, extended operations set `resolve = false` when not present in the operation object**. + +When `resolve === true`, if any `path` component (below root) is not present in the source document, the corresponding object structure will be created in order to resolve the path down to the final "leaf" component. + +#### Example of Forced Path Resolution + +```js +// starting source document +const d = { + a: { + b: "hi", + }, +}; + +// non-existent path @ 'c' +const path = "/a/c/d"; + +// when resolve === true, the missing structure will be created +const out = { + a: { + b: "hi", + c: {}, // object now exists at /a/c as a potential parent for key 'd' + }, +}; +``` + +**NOTE**: in order for a path component to be "force resolved", it **MUST NOT** already exist in the source document. Forced path resolution will **NOT** replace existing document structure. The operaton will be rejected with a `JsonPathError`. + +**NOTE2**: Strictly speaking, when dealing with JavaScript objects (beyond what is representable by JSON) forced path resolution may add properties and object trees to the target; so any JavaScript `Object`-like thing, as long as it is not sealed or frozen, etc., can be mutated, and things may wind up in places you do not expect (via custom getters/setters, indexers, etc.). Be mindful of object references when dealing with source JS objects that are not just "freshly parsed JSON documents" + +## Resolving when top-level doc is Array vs. Object + +The behavior of `resolve` differs with the type of the **root** (source) document. When the source doc is an `Object` proper (not an Array), the missing path components are created as empty Objects `{}`. + +**NOTE**: numeric keys (path components) ex: '`/a/2/1/c`' will be treated as `string properties` when resolving `Object` paths; like so: + +```js +// start with source document (Object) {a: {b: 'foo'}} +// a resolved path of '/a/2/1/c' results in the following structure: +const result = { + a: { + b: "foo", + // Object @ key "2" + 2: { + // Object @ key "1" + 1: {}, + }, + }, +}; +``` + +### When Source Document is Array + +Resolving missing path components when the source document is an `Array` results in additional nested `Arrays` being created at the corresponding path components. + +For example: + +```js +// source Array is ['hi'] ; length 1 +// path to resolve is '/4/2/1' +// resolved result is: +const result = ["hi", , , , [, , []]]; +``` + +The missing elements in the example aren't typos. Those are "empty" elements as defined by JavaScript - see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Array + +The rule is that a sparse array is created for each missing path component. The algorithm uses '`new Array(idx + 1)`' to ensure capacity of at least a length required to accommodate an insertion at the given path (index). The sparse nature of the array will not occupy additional space (memory) even for large capacities. + +**Note**: Be careful when serializing to JSON from JS after resolving Array paths. Empty elements are translated into `null` values in JSON. It is possible to cause the size of the resulting JSON file to increase by a significant amount. + +## When in doubt... + +Don't try to "force resolve" paths into complex structures in cases where instead using the RFC `add` operation will produce the desired result. Forced path resolution is useful in cases when diffrent operation behavior is necessary depending upon the existence of structure in the source document (e.g. create/replace vs. update); which isn't possible with the standard RFC ops. Use the resolve feature only when necessary; YMMV. + +# Extended Operations API + +As stated above, extended operation functionality is 100% opt-in. This is done by "registering" your custom operation configs with the JSON-Patch library. + +### Once Extended Ops are registered, you use the standard API as you would with regular RFC operations. + +**reminder**: Only the following JSON-Patch functions are compatible with _registered_ extended operations: + +```js +jsonpatch.applyOperation(/* ... */); + +jsonpatch.applyPatch(/* ... */); + +jsonpatch.applyReducer(/* ... */); + +jsonpatch.validator(/* ... */); + +jsonpatch.validate(/* ... */); +``` + +The extended operation API adds a single function that registers an extended operation configuration. You call this once per extended operation you wish to register. + +The extended API also includes a couple of convenience functions to help manage your extensions. + +#### `function useExtendedOperation(xid: string, config: ExtendedMutationOperationConfig): void` + +Registers an extended operation (configuration object) `config` with the library with operation id `xid`. + +**This is the only function required to "opt-in" to using extended operations**; Called once per extended operation you wish to use. i.e. once an extended operation is registered, you do not need to call this function again at any point for the same operation. (unless, of course extended operations have been unregistered - see below) + +(re)Registering a previously-registered (same `xid`) **replaces** the existing configuration with the new `config`. + +Will throw an error if `xid` or `config` are invalid. + +#### `function hasExtendedOperation(xid: string): boolean` + +A convenience function to inspect the extended operation registry for the existence of a user-defined operation with id `xid` + +#### `function unregisterAllExtendedOperations(): void` + +A convenience function to remove all registered extended operations from the library. + +This is mostly useful during unit testing. + +## Extended Operation Error Names + +In addition to the core library exceptions, Extended Operations introduce a few more possibilities to help when debugging: (note: Error messages vary) + +| Error name | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| OPERATION_X_ARGS_NOT_ARRAY | `args` was provided a value (in an extended operation object) and is not an array | +| OPERATION_X_OP_INVALID | `xid` was not found in extended operations registry | +| OPERATION_X_CONFIG_INVALID | Extended operation configuration object has an invalid/missing `arr`, `obj`, or `validator` property | +| OPERATION_X_ID_INVALID | Extended operation `xid` is missing or does not follow the '`x-`' prefix convention | +| OPERATION_X_AMBIGUOUS_REMOVAL | `arr` operator reported a removal (returned a `removed` value) when the operation object specified `resolve: true` | +| OPERATION_X_OPERATOR_EXCEPTION | Intended to be used by developers when throwing an error from their custom extended operator functions | + +# Core Lib Interop Note + +Extended Operations are NOT compatible with (will not be considered) when performing the following core JSON-Patch API calls: + +```js +/* these core API functions will NOT include ANY registered/unregistered extended operations*/ + +jsonpatch.observe(/* ... */); + +jsonpatch.unobserve(/* ... */); + +jsonpatch.generate(/* ... */); + +jsonpatch.compare(/* ... */); +``` + +Unlike the well-known RFC-6902 operations, there is no way to determine what an extended operation will do without executing it. Technically, the user-defined extended operator functions are possibly non-invertable anyway with no way to derive a suitable "undo". They are a black-box to the library with their semantics only exposed through developer documentation. (you did document your operations, didn't you?) + +# Limitations + +The intent behind extended operations is to **AUGMENT** the functionality provided by the core JSON Patch spec (RFC-6902). Therefore, extended operations are expected to respect the _spirit_ of the RFC. + +Extended operations should only mutate at the given path (if they mutate at all) not perform multiple mutations at parts of the document tree unrelated to the provided `path`. Extended operations should also not duplicate the exact functionality of an existing RFC operation. Use standard existing ops when possible. + +### TL;DR + +#### What it IS: + +> A simple way to have patch operations that change their behavior based on values in the source document; a way to do "dynamic" patching along side of the standard RFC operations. + +#### What it ISN'T: + +> A replacement for complex JSON/JS Object parsing/traversal/manipulation tools. Keep it simple. Use the right tools for the job. + +### Best Practices + +- **It is best to keep things simple when implementing your own extended operations** as complex computations and manipulations will hinder performance and be difficult to test/validate. + +- **Always prefer a standard RFC-6902 operation over a custom extended implementation if it will do what you need**; no need to reinvent the wheel + +- **Stick with operations that update/add/replace instead of remove/move** - in most cases you should be using the RFC `remove` and `move` operations for their associated purposes. The extended operation processor is **NOT** able to determine the intention behind your operator implementations, and as such it cannot always determine which parts of the document tree have been removed and will output an erroneous patched result document when "off-path" removals have occured during processing. + +- **Do NOT mix addition of properties/values with removal of properties** - per above, it is impossible to determine what has been removed/added if outside the given `path` target. Stick with RFC-compliant removal ops when needed. + +- **Beware JS-to-JSON and JSON-to-JS serialization** - Keep in mind how you are using the results of an extended operation. Only a subset of JS values properly serialize into JSON; can be parsed back to JS. Values for the properties `value` and `args` may include any valid JS value; But not all of these values will be representable in JSON when serializing the operation objects themselves, or the resulting mutated document tree. + +- **Extended Operations are Black Boxes** - If you are creating extended operations for later use, or planned distribution to other developers, at a minimum, you must document their functionality including inputs, output, intention/usage, pre-conditions, etc. **Distribute a unit test suite with your extended operation configurations** to provide working examples of your operations in action along with documentation of what is and isn't supported by their use. + +- **Always Enable Validation** - It is recommended to always set `applyOperation` to use validation (3rd argument `true`) + +# Example Extended Operation Implementations + +These examples represent the kind of mutations that are best suited for implementation as extended operations. + +## Basic Summation Operation + +Suppose you are patching some documents and instead of just replacing some numbers, you'd like to update the numbers according to their current values while patching (maybe you've got a simple database of prices that is represented as a JSON document, and you need to increase/decrease certain values by a specific amount; in bulk). This extended operation configuration allows for summing existing values with a provided value. + +```js +/* + This function applies to both arr and obj and is defined separately. + + The operation it represents adds an existing number with a number + provided in the operation object. + + If the value at the target path is not of type number, then No Op + + - value may be +/- +*/ +function xsum(o, k, d) { + if (typeof o[k] !== "number") { + // No Op + return undefined; + } + + // update + o[k] = o[k] + this.value; + return { newDocument: d }; +} + +/* Create the operation config object */ +const xcfg = { + // arr and obj share same logic + arr: xsum, + obj: xsum, + // custom validator ensures that the operation object has a numeric `value` property + validator: (op, i, d) => { + if (typeof op.value !== "number") { + throw new jsonpatch.JsonPatchError( + "x-sum operation `value` must be a number", + "OPERATION_VALUE_REQUIRED", + i, + op, + d + ); + } + }, +}; + +/* Register the configuration object with the library with the id 'sum' */ +jsonpatch.useExtendedOperation("x-sum", xcfg); +``` + +```js +/* ---EXPECTED BEHAVIOR--- */ + +/* EXAMPLE #1 +given a source document of: */ +{ + a: 5 +} + +// and an operation object: +{op: 'x', xid: 'x-sum', path: '/a', value: 12} + +// the result of the extended operation would yield: +{ + a: 17 +} + +/* EXAMPLE #2 +this operation would result in an error because the value property is not a number: */ +{op: 'x', xid: 'x-sum', path: '/a', value: 'i am not a numeric type'} + +/* EXAMPLE #3 +given this source document: */ +[1, 'hello'] + +// and an operation object: +{op: 'x', xid: 'x-sum', path: '/1', value: 4} + +// the result of the extended operation would yield: +[1, 'hello'] + +// No changes were made because the value at the specified path was not numeric +``` + +## Advanced Summation Operation + +To demonstrate that more sophisticated behavior is possible with extended operations this example extends the summation task from above by adding more control over the numeric results by passing additional values in the operation object as `args`. + +Here we specify `args` as follows: + +- **args[0]** numeric - a "default/starting value" to use when value at `path` is not present +- **args[1]** numeric - a minimum (inclusive) value that the result must not fall below after the summation +- **args[2]** numeric - a maximum (inclusive) value that the result must not rise above after the summation + +**Note**: all args are implemented as _optional_ with the following defaults: + +- default 'missing' value 0 +- no minimum enforced +- no maximum enforced + +```js +/* + This function applies to both arr and obj and is defined separately. + + The operation it represents adds an existing number with a number + provided in the operation object. + + - If the value at the target path is not present (undefined), use default value before performing the summation + + - If the value at the target path is not of type number, throw an error + + - allow for user-specified min and/or max values of the final result +*/ +function xsum2(o, k, d) { + // start with a default offset of 0 or user-provided in args + let sum = + Array.isArray(this.args) && typeof this.args[0] === "number" + ? this.args[0] + : 0; + + // target is not present + if (o[k] === undefined) { + sum += this.value; + } + // target exists, but is not numeric + else if (typeof o[k] !== "number") { + throw new jsonpatch.JsonPatchError( + "x-sum op target value is not a number", + "OPERATION_X_OPERATOR_EXCEPTION", + undefined, + this, + d + ); + } + // add existing with provided value + else { + sum = o[k] + this.value; + } + + // check min max + if (Array.isArray(this.args)) { + // min + if (this.args[1] !== undefined) { + sum = Math.max(sum, this.args[1]); + } + // max + if (this.args[2] !== undefined) { + sum = Math.min(sum, this.args[2]); + } + } + + // update + o[k] = sum; + return { newDocument: d }; +} + +/* Create the operation config object */ +const xcfg2 = { + // arr and obj share same logic + arr: xsum2, + obj: xsum2, + // custom validator ensures valid inputs in the operation object + validator: (op, i, d, ep) => { + // validate operation input value + if (typeof op.value !== "number") { + throw new jsonpatch.JsonPatchError( + "x-sum2 operation `value` must be a number", + "OPERATION_VALUE_REQUIRED", + i, + op, + d + ); + } + + // validate args if present + if (Array.isArray(op.args)) { + if ( + op.args.find((v) => v !== undefined && typeof v !== "number") !== + undefined + ) { + throw new jsonpatch.JsonPatchError( + "x-sum2 operation all provided `args` must be a explicitly undefined or a number", + "OPERATION_X_OP_INVALID", + i, + op, + d + ); + } + } + }, +}; + +/* Register the configuration object with the library with the id 'sum2' */ +jsonpatch.useExtendedOperation("x-sum2", xcfg2); +``` + +```js +/* ---EXPECTED BEHAVIOR--- */ + +/* EXAMPLE #1 +Given a source document of */ +{} + +// And a patch consisting of several operations +[ + { op: "add", path: "/a", value: {} }, + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 150 + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 250 + { op: "add", path: "/a/c", value: "hi" }, // a.c === 'hi' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 350 + { op: "replace", path: "/a/c", value: "hello" }, // a.c === 'hello' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 400 +]; + +// The expected results of `jsonpatch.applyPatch` are +{ + a: { + b: 400, + c: "hello", + }, +}; + +/* EXAMPLE #2 */ + +// The same results could be achieved by "force resolving" the path '/a/b' instead of first creating it with an RFC 'add' operation: +[ + // { op: "add", path: "/a", value: {} }, + { + resolve: true, op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400], + }, // a.b === 150 + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 250 + { op: "add", path: "/a/c", value: "hi" }, // a.c === 'hi' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 350 + { op: "replace", path: "/a/c", value: "hello" }, // a.c === 'hello' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 400 +]; + +``` diff --git a/README.md b/README.md index 59f4394..3bf1144 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,26 @@ -JSON-Patch -=============== +# JSON-Patch > A leaner and meaner implementation of JSON-Patch. Small footprint. High performance. [![Build Status](https://travis-ci.org/Starcounter-Jack/JSON-Patch.svg?branch=master)](https://travis-ci.org/Starcounter-Jack/JSON-Patch) With JSON-Patch, you can: + - **apply** patches (arrays) and single operations on JS object - **validate** a sequence of patches - **observe** for changes and **generate** patches when a change is detected - **compare** two objects to obtain the difference +- **"user defined"** (non RFC-6902) patch operations (v4.x+ only) [SEE HERE](EXTENDED.md) Tested in Firefox, Chrome, Edge, Safari, IE11, Deno and Node.js - ## Why you should use JSON-Patch JSON-Patch [(RFC6902)](http://tools.ietf.org/html/rfc6902) is a standard format that allows you to update a JSON document by sending the changes rather than the whole document. JSON Patch plays well with the HTTP PATCH verb (method) and REST style programming. -Mark Nottingham has a [nice blog]( http://www.mnot.net/blog/2012/09/05/patch) about it. - +Mark Nottingham has a [nice blog](http://www.mnot.net/blog/2012/09/05/patch) about it. ## Install @@ -32,7 +31,6 @@ Mark Nottingham has a [nice blog]( http://www.mnot.net/blog/2012/09/05/patch) ab npm install fast-json-patch --save ``` - ## Adding to your project ### In a web browser @@ -47,8 +45,8 @@ In [browsers that support ECMAScript modules](https://caniuse.com/#feat=es6-modu ```html ``` @@ -57,22 +55,22 @@ In [browsers that support ECMAScript modules](https://caniuse.com/#feat=es6-modu In Node 12+ with `--experimental-modules` flag, the below code uses this library as an ECMAScript module: ```js -import * as jsonpatch from 'fast-json-patch/index.mjs'; -import { applyOperation } from 'fast-json-patch/index.mjs'; +import * as jsonpatch from "fast-json-patch/index.mjs"; +import { applyOperation } from "fast-json-patch/index.mjs"; ``` In Webpack (and most surely other bundlers based on Babel), the below code uses this library as an ECMAScript module: ```js -import * as jsonpatch from 'fast-json-patch'; -import { applyOperation } from 'fast-json-patch'; +import * as jsonpatch from "fast-json-patch"; +import { applyOperation } from "fast-json-patch"; ``` In standard Node, the below code uses this library as a CommonJS module: ```js -const { applyOperation } = require('fast-json-patch'); -const applyOperation = require('fast-json-patch').applyOperation; +const { applyOperation } = require("fast-json-patch"); +const applyOperation = require("fast-json-patch").applyOperation; ``` ## Directories @@ -86,6 +84,8 @@ Directories used in this package: ## API +### _See [**here**](EXTENDED.md) for Documentation on using extended (non RFC-6902) operations_ + #### `function applyPatch(document: T, patch: Operation[], validateOperation?: boolean | Validator, mutateDocument: boolean = true, banPrototypeModifications: boolean = true): PatchResult` Applies `patch` array on `obj`. @@ -94,7 +94,7 @@ Applies `patch` array on `obj`. - `patch` a JSON-Patch array of operations to apply - `validateOperation` Boolean for whether to validate each operation with our default validator, or to pass a validator callback - `mutateDocument` Whether to mutate the original document or clone it before applying -- `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. +- `banPrototypeModifications` Whether to ban modifications to `__proto__`, defaults to `true`. An invalid patch results in throwing an error (see `jsonpatch.validate` for more information about the error object). @@ -103,15 +103,15 @@ If you would like to avoid touching your `patch` array values, clone them: `json 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}`. -* `test` - boolean result of the test -* `remove`, `replace` and `move` - original object that has been removed -* `add` (only when adding to an array) - index at which item has been inserted (useful when using `-` alias) +- `test` - boolean result of the test +- `remove`, `replace` and `move` - original object that has been removed +- `add` (only when adding to an array) - index at which item has been inserted (useful when using `-` alias) -- ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** -- ** Note II: the returned array has `newDocument` property that you can use as the final state of the patched document **. -- ** Note III: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. +* ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** +* ** Note II: the returned array has `newDocument` property that you can use as the final state of the patched document **. +* \*\* Note III: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. -- See [Validation notes](#validation-notes). +* See [Validation notes](#validation-notes). Example: @@ -120,7 +120,11 @@ var document = { firstName: "Albert", contactDetails: { phoneNumbers: [] } }; var patch = [ { op: "replace", path: "/firstName", value: "Joachim" }, { op: "add", path: "/lastName", value: "Wester" }, - { op: "add", path: "/contactDetails/phoneNumbers/0", value: { number: "555-123" } } + { + op: "add", + path: "/contactDetails/phoneNumbers/0", + value: { number: "555-123" }, + }, ]; document = jsonpatch.applyPatch(document, patch).newDocument; // document == { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [{number:"555-123"}] } }; @@ -143,7 +147,7 @@ If you would like to avoid touching your values, clone them: `jsonpatch.applyOpe Returns an [`OperationResult`](#operationresult-type) object `{newDocument: any, test?: boolean, removed?: any}`. - ** Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. ** -- ** Note II: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. +- \*\* Note II: By default, when `banPrototypeModifications` is `true`, this method throws a `TypeError` when you attempt to modify an object's prototype. - See [Validation notes](#validation-notes). @@ -169,11 +173,15 @@ Note: It throws `TEST_OPERATION_FAILED` error if `test` operation fails. Example: ```js -var document = { firstName: "Albert", contactDetails: { phoneNumbers: [ ] } }; +var document = { firstName: "Albert", contactDetails: { phoneNumbers: [] } }; var patch = [ - { op:"replace", path: "/firstName", value: "Joachim" }, - { op:"add", path: "/lastName", value: "Wester" }, - { op:"add", path: "/contactDetails/phoneNumbers/0", value: { number: "555-123" } } + { op: "replace", path: "/firstName", value: "Joachim" }, + { op: "add", path: "/lastName", value: "Wester" }, + { + op: "add", + path: "/contactDetails/phoneNumbers/0", + value: { number: "555-123" }, + }, ]; var updatedDocument = patch.reduce(applyReducer, document); // updatedDocument == { firstName:"Joachim", lastName:"Wester", contactDetails:{ phoneNumbers[ {number:"555-123"} ] } }; @@ -214,11 +222,15 @@ If there are no pending changes in `obj`, returns an empty array (length 0). Example: ```js -var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; +var document = { + firstName: "Joachim", + lastName: "Wester", + contactDetails: { phoneNumbers: [{ number: "555-123" }] }, +}; var observer = jsonpatch.observe(document); document.firstName = "Albert"; document.contactDetails.phoneNumbers[0].number = "123"; -document.contactDetails.phoneNumbers.push({ number:"456" }); +document.contactDetails.phoneNumbers.push({ number: "456" }); var patch = jsonpatch.generate(observer); // patch == [ // { op: "replace", path: "/firstName", value: "Albert"}, @@ -230,11 +242,15 @@ var patch = jsonpatch.generate(observer); Example of generating patches with test operations for values in the first object: ```js -var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; +var document = { + firstName: "Joachim", + lastName: "Wester", + contactDetails: { phoneNumbers: [{ number: "555-123" }] }, +}; var observer = jsonpatch.observe(document); document.firstName = "Albert"; document.contactDetails.phoneNumbers[0].number = "123"; -document.contactDetails.phoneNumbers.push({ number:"456" }); +document.contactDetails.phoneNumbers.push({ number: "456" }); var patch = jsonpatch.generate(observer, true); // patch == [ // { op: "test", path: "/firstName", value: "Joachim"}, @@ -253,15 +269,15 @@ Any remaining changes are delivered synchronously (as in `jsonpatch.generate`). #### `jsonpatch.compare(document1: any, document2: any, invertible = false): Operation[]` -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`. +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`. If there are no differences, returns an empty array (length 0). Example: ```js -var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; -var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; +var documentA = { user: { firstName: "Albert", lastName: "Einstein" } }; +var documentB = { user: { firstName: "Albert", lastName: "Collins" } }; var diff = jsonpatch.compare(documentA, documentB); //diff == [{op: "replace", path: "/user/lastName", value: "Collins"}] ``` @@ -269,8 +285,8 @@ var diff = jsonpatch.compare(documentA, documentB); Example of comparing two object trees with test operations for values in the first object: ```js -var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; -var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; +var documentA = { user: { firstName: "Albert", lastName: "Einstein" } }; +var documentB = { user: { firstName: "Albert", lastName: "Collins" } }; var diff = jsonpatch.compare(documentA, documentB, true); //diff == [ // {op: "test", path: "/user/lastName", value: "Einstein"}, @@ -294,37 +310,38 @@ If there are no errors, returns undefined. If there is an errors, returns a Json Possible errors: -Error name | Error message -------------------------------|------------ -SEQUENCE_NOT_AN_ARRAY | Patch sequence must be an array -OPERATION_NOT_AN_OBJECT | Operation is not an object -OPERATION_OP_INVALID | Operation `op` property is not one of operations defined in RFC-6902 -OPERATION_PATH_INVALID | Operation `path` property is not a valid string -OPERATION_FROM_REQUIRED | Operation `from` property is not present (applicable in `move` and `copy` operations) -OPERATION_VALUE_REQUIRED | Operation `value` property is not present, or `undefined` (applicable in `add`, `replace` and `test` operations) -OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED | Operation `value` property object has at least one `undefined` value (applicable in `add`, `replace` and `test` operations) -OPERATION_PATH_CANNOT_ADD | Cannot perform an `add` operation at the desired path -OPERATION_PATH_UNRESOLVABLE | Cannot perform the operation at a path that does not exist -OPERATION_FROM_UNRESOLVABLE | Cannot perform the operation from a path that does not exist -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 -OPERATION_VALUE_OUT_OF_BOUNDS | The specified index MUST NOT be greater than the number of elements in the array -TEST_OPERATION_FAILED | When operation is `test` and the test fails, applies to `applyReducer`. +| Error name | Error message | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| SEQUENCE_NOT_AN_ARRAY | Patch sequence must be an array | +| OPERATION_NOT_AN_OBJECT | Operation is not an object | +| OPERATION_OP_INVALID | Operation `op` property is not one of operations defined in RFC-6902 | +| OPERATION_PATH_INVALID | Operation `path` property is not a valid string | +| OPERATION_FROM_REQUIRED | Operation `from` property is not present (applicable in `move` and `copy` operations) | +| OPERATION_VALUE_REQUIRED | Operation `value` property is not present, or `undefined` (applicable in `add`, `replace` and `test` operations) | +| OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED | Operation `value` property object has at least one `undefined` value (applicable in `add`, `replace` and `test` operations) | +| OPERATION_PATH_CANNOT_ADD | Cannot perform an `add` operation at the desired path | +| OPERATION_PATH_UNRESOLVABLE | Cannot perform the operation at a path that does not exist | +| OPERATION_FROM_UNRESOLVABLE | Cannot perform the operation from a path that does not exist | +| 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 | +| OPERATION_VALUE_OUT_OF_BOUNDS | The specified index MUST NOT be greater than the number of elements in the array | +| TEST_OPERATION_FAILED | When operation is `test` and the test fails, applies to `applyReducer`. | Example: ```js -var obj = {user: {firstName: "Albert"}}; -var patches = [{op: "replace", path: "/user/firstName", value: "Albert"}, {op: "replace", path: "/user/lastName", value: "Einstein"}]; +var obj = { user: { firstName: "Albert" } }; +var patches = [ + { op: "replace", path: "/user/firstName", value: "Albert" }, + { op: "replace", path: "/user/lastName", value: "Einstein" }, +]; var errors = jsonpatch.validate(patches, obj); if (errors.length == 0) { - //there are no errors! -} -else { - for (var i=0; i < errors.length; i++) { + //there are no errors! +} else { + for (var i = 0; i < errors.length; i++) { if (!errors[i]) { console.log("Valid patch at index", i, patches[i]); - } - else { + } else { console.error("Invalid patch at index", i, errors[i], patches[i]); } } @@ -345,7 +362,6 @@ Where: - `test`: if the operation was a `test` operation. This will be its result. - `removed`: contains the removed, moved, or replaced values from the document after a `remove`, `move` or `replace` operation. - ## Validation Notes Functions `applyPatch`, `applyOperation`, and `validate` accept a `validate`/ `validator` parameter: @@ -377,7 +393,7 @@ See the [ECMAScript spec](http://www.ecma-international.org/ecma-262/6.0/index.h ## Specs/tests - - [Run in browser](http://starcounter-jack.github.io/JSON-Patch/test/) +- [Run in browser](http://starcounter-jack.github.io/JSON-Patch/test/) ## [Contributing](CONTRIBUTING.md) @@ -386,8 +402,11 @@ See the [ECMAScript spec](http://www.ecma-international.org/ecma-262/6.0/index.h To see the list of recent changes, see [Releases](https://github.com/Starcounter-Jack/JSON-Patch/releases). ## Footprint + 4 KB minified and gzipped (12 KB minified) +(v4.x+) 5 KB minified and gzipped (17 KB minified) + ## Performance ##### [`add` benchmark](https://run.perf.zone/view/JSON-Patch-Add-Operation-1535541298893) @@ -406,7 +425,7 @@ Tested on 29.08.2018. Compared libraries: - [jiff](https://www.npmjs.com/package/jiff) 0.7.3 - [RFC6902](https://www.npmjs.com/package/rfc6902) 2.4.0 -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. +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. ## License diff --git a/commonjs/core.js b/commonjs/core.js index 16ee5bb..0a1c6c8 100644 --- a/commonjs/core.js +++ b/commonjs/core.js @@ -2,6 +2,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); var helpers_js_1 = require("./helpers.js"); exports.JsonPatchError = helpers_js_1.PatchError; exports.deepClone = helpers_js_1._deepClone; +; +/* Registry of Extended operations */ +var xOpRegistry = new Map(); /* We use a Javascript hash to store each function. Each hash entry (property) uses the operation identifiers specified in rfc6902. @@ -53,8 +56,8 @@ var objOps = { /* The operations applicable to an array. Many are the same as for the object */ var arrOps = { add: function (arr, i, document) { - if (helpers_js_1.isInteger(i)) { - arr.splice(i, 0, this.value); + if (helpers_js_1.isInteger(String(i))) { + arr.splice(~~i, 0, this.value); } else { // array props arr[i] = this.value; @@ -63,7 +66,7 @@ var arrOps = { return { newDocument: document, index: i }; }, remove: function (arr, i, document) { - var removedList = arr.splice(i, 1); + var removedList = arr.splice(~~i, 1); return { newDocument: document, removed: removedList[0] }; }, replace: function (arr, i, document) { @@ -76,6 +79,55 @@ var arrOps = { test: objOps.test, _get: objOps._get }; +/** + * Registers an extended (non-RFC 6902) operation for processing. + * Will overwrite configs that already exist for given xid string + * + * @param xid The operation id (must follow the convention /^x-[a-z]+$/ + * to avoid visual confusion with the RFC's ops) + * @param config the operation configuration object containing + * the array, and object operators (functions), and a validator function for + * the extended operation + */ +function useExtendedOperation(xid, config) { + if (!helpers_js_1.isValidExtendedOpId(xid)) { + throw new exports.JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + // basic checks for all props + if (typeof config.arr !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `arr` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.obj !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `obj` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.validator !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `validator` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + // register config as immutable obj + xOpRegistry.set(xid, Object.freeze(config)); +} +exports.useExtendedOperation = useExtendedOperation; +/** + * Performs check for a registered extended (non-RFC 6902) operation. + * + * @param xid the qualified ("x-") extended operation name + * @return boolean true if xop is registered as an extended operation + */ +function hasExtendedOperation(xid) { + if (!helpers_js_1.isValidExtendedOpId(xid)) { + throw new exports.JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + return xOpRegistry.has(xid); +} +exports.hasExtendedOperation = hasExtendedOperation; +/** + * Removes all previously registered extended operation configurations. + * (primarily used during unit testing) + */ +function unregisterAllExtendedOperations() { + xOpRegistry.clear(); +} +exports.unregisterAllExtendedOperations = unregisterAllExtendedOperations; /** * Retrieves a value from a JSON document by a JSON pointer. * Returns the value. @@ -156,6 +208,31 @@ function applyOperation(document, operation, validateOperation, mutateDocument, operation.value = document; return returnValue; } + else if (operation.op === 'x') { + // get extended config + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // at empty (root) path default to obj operator + var workingDocument = document; + var obj = document; + if (!mutateDocument) { + obj = workingDocument = helpers_js_1._deepClone(document); + } + var result = { newDocument: operation.value }; + // if resolve is true, allow extended operator to run against supplied + // document object/clone + if (operation.resolve === true) { + result = xConfig.obj.call(operation, obj, '', workingDocument); + // in resolve mode, allow operator result of undefined to revert back to + // original document + if (result === undefined) { + return { newDocument: document }; + } + } + return result; + } else { /* bad operation */ if (validateOperation) { throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); @@ -176,6 +253,18 @@ function applyOperation(document, operation, validateOperation, mutateDocument, var len = keys.length; var existingPathFragment = undefined; var key = void 0; + var xConfig = void 0; + var workingDocument = document; + var graftPath = undefined; + if (operation.op === 'x') { + xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // make another clone to allow configured operator to 'abort' or 'no op' + // by returning a strict undefined + workingDocument = obj = helpers_js_1._deepClone(obj); + } var validateFunction = void 0; if (typeof validateOperation == 'function') { validateFunction = validateOperation; @@ -191,7 +280,7 @@ function applyOperation(document, operation, validateOperation, mutateDocument, if (banPrototypeModifications && (key == '__proto__' || (key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) { - 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'); + throw new TypeError(helpers_js_1.PROTO_ERROR_MSG); } if (validateOperation) { if (existingPathFragment === undefined) { @@ -208,8 +297,32 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } t++; if (Array.isArray(obj)) { + // don't coerce an empty key string into an integer (below w/~~) + // if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key === '') { + throw new exports.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); + } if (key === '-') { key = obj.length; + // make some adjustments for grafting with extended ops + if (operation.op === 'x') { + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + } + } + // extended operations can use a special 'end element' sentinel in array paths + else if (operation.op === 'x' && key === '--') { + key = obj.length - 1; + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } } else { if (validateOperation && !helpers_js_1.isInteger(key)) { @@ -217,9 +330,50 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } // only parse key when it's an integer for `arr.prop` to work else if (helpers_js_1.isInteger(key)) { key = ~~key; + // don't allow arbitrary idx creation if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key >= obj.length) { + throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); + } } } if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (typeof key === 'string' && key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended array patch + var result = xConfig.arr.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + // check for ambiguous results + if (result.removed !== undefined && operation.resolve === true) { + // can't use resolve with a removal; ambiguous removal path + throw new exports.JsonPatchError('Extended operation should not remove items while resolving undefined paths', 'OPERATION_X_AMBIGUOUS_REMOVAL', index, operation, document); + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + helpers_js_1._graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } if (validateOperation && operation.op === "add" && key > obj.length) { throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); } @@ -232,6 +386,38 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } else { if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended obj patch + var result = xConfig.obj.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + helpers_js_1._graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch if (returnValue.test === false) { throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); @@ -239,6 +425,25 @@ function applyOperation(document, operation, validateOperation, mutateDocument, return returnValue; } } + // extended operation forced path resolution + if (operation.op === 'x' && xConfig && obj[key] === undefined) { + // get first path where something isn't defined + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + if (operation.resolve === true) { + // add resolvable nodes + // check next key to determine object creation strategy + if (Array.isArray(document) && helpers_js_1.isInteger(String(keys[t]))) { // goofy cast + // if original document is an array, numeric path elements + // should add new arrays with minimum capacity + obj[key] = new Array(~~keys[t] + 1); + } + else { + obj[key] = {}; + } + } + } obj = obj[key]; // If we have more keys in the path, but the next value isn't a non-null object, // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. @@ -312,6 +517,32 @@ function validator(operation, index, document, existingPathFragment) { if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { throw new exports.JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); } + // check for extended ops + if (operation.op === 'x') { + // default validations + if (!helpers_js_1.isValidExtendedOpId(operation.xid)) { + throw new exports.JsonPatchError('Operation `xid` property is not present or invalid string', 'OPERATION_X_ID_INVALID', index, operation, document); + } + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + if (typeof operation.path !== 'string') { + throw new exports.JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { + // paths that aren't empty string should start with "/" + throw new exports.JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path === '/') { + throw new exports.JsonPatchError('Operation `path` slash-only is ambiguous', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); + } + if (operation.args !== undefined && !Array.isArray(operation.args)) { + throw new exports.JsonPatchError('Operation `args` property is not an array', 'OPERATION_X_ARGS_NOT_ARRAY', index, operation, document); + } + // we made it this far, now run the operation's configured validator + xConfig.validator.call(operation, operation, index, document, existingPathFragment); + } else if (!objOps[operation.op]) { throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); } diff --git a/commonjs/duplex.js b/commonjs/duplex.js index 1603bac..88975c6 100644 --- a/commonjs/duplex.js +++ b/commonjs/duplex.js @@ -1,7 +1,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); /*! * https://github.com/Starcounter-Jack/JSON-Patch - * (c) 2017 Joachim Wester + * (c) 2017-2021 Joachim Wester * MIT license */ var helpers_js_1 = require("./helpers.js"); diff --git a/commonjs/helpers.js b/commonjs/helpers.js index 0ac28b4..1473ee3 100644 --- a/commonjs/helpers.js +++ b/commonjs/helpers.js @@ -179,3 +179,77 @@ var PatchError = /** @class */ (function (_super) { return PatchError; }(Error)); exports.PatchError = PatchError; +// exported for use in jasmine test +exports.PROTO_ERROR_MSG = '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'; +function isValidExtendedOpId(xid) { + return typeof xid === 'string' && xid.length >= 3 && xid.indexOf('x-') === 0; +} +exports.isValidExtendedOpId = isValidExtendedOpId; +; +; +/** + * attach/remove source tree to/from target tree at appropriate path. + * modifies targetObj (in place) by reference. + * + * This is necessary to deal with JS "by value" semantics + */ +function _graftTree(sourceObj, targetObj, pathComponents) { + if (pathComponents.comps.length === 0) { + // no changes + return; + } + // traverse document trees until at the appropriate parent level + var graftTgt = targetObj; + var graft = sourceObj; + var graftKey = ''; + // single component is top-level key + if (pathComponents.comps.length === 1) { + graftKey = pathComponents.comps[0]; + if (pathComponents.modType === 'graft') { + graftTgt[graftKey] = graft[graftKey]; + } + else { + // top-level prune is a "best guess" that the provided key was removed from + // the top-level object (we have no visibility into what the extended + // operator has actually done since it comes from "user-space"; + // external to the extension api) + // NOTE: pruning is here only to allow the extension api to + // emulate the RFC api; user-defined operations may perform complex + // removals, but they may not be visible during the prune process. + // It is recommended that the user stick to the RFC 'remove' operation + // and not implemennt their own removal operations in extended-space; + // or at least limit removal to the provided path, and not perform other mutations + // combined with removals + if (Array.isArray(targetObj)) { + graftTgt.splice(~~graftKey, 1); + } + else { + delete graftTgt[graftKey]; + } + } + return; + } + for (var i = 0; i < pathComponents.comps.length; i++) { + graftKey = pathComponents.comps[i]; + graft = graft[graftKey]; + // if there is no value in the target obj at the current key, + // than this is a graft point + if (pathComponents.modType === 'graft' && graftTgt[graftKey] === undefined) { + // if both target and source are undefined - No Op + if (graft === undefined) { + return; + } + break; + } + // there was a removal; the graft point needs to be the 2nd to last path comp + // a.k.a. the parent obj of the pruned key + // in order to preserve additional structure that was not pruned + if (i === pathComponents.comps.length - 2) { + break; + } + graftTgt = graftTgt[graftKey]; + } + // graft + graftTgt[graftKey] = graft; +} +exports._graftTree = _graftTree; diff --git a/dist/fast-json-patch.js b/dist/fast-json-patch.js index 7f046dd..07f458b 100644 --- a/dist/fast-json-patch.js +++ b/dist/fast-json-patch.js @@ -1,4 +1,4 @@ -/*! fast-json-patch, version: 3.1.0 */ +/*! fast-json-patch, version: 4.0.0-rc.1 */ var jsonpatch = /******/ (function(modules) { // webpackBootstrap /******/ // The module cache @@ -271,6 +271,80 @@ var PatchError = /** @class */ (function (_super) { return PatchError; }(Error)); exports.PatchError = PatchError; +// exported for use in jasmine test +exports.PROTO_ERROR_MSG = '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'; +function isValidExtendedOpId(xid) { + return typeof xid === 'string' && xid.length >= 3 && xid.indexOf('x-') === 0; +} +exports.isValidExtendedOpId = isValidExtendedOpId; +; +; +/** + * attach/remove source tree to/from target tree at appropriate path. + * modifies targetObj (in place) by reference. + * + * This is necessary to deal with JS "by value" semantics + */ +function _graftTree(sourceObj, targetObj, pathComponents) { + if (pathComponents.comps.length === 0) { + // no changes + return; + } + // traverse document trees until at the appropriate parent level + var graftTgt = targetObj; + var graft = sourceObj; + var graftKey = ''; + // single component is top-level key + if (pathComponents.comps.length === 1) { + graftKey = pathComponents.comps[0]; + if (pathComponents.modType === 'graft') { + graftTgt[graftKey] = graft[graftKey]; + } + else { + // top-level prune is a "best guess" that the provided key was removed from + // the top-level object (we have no visibility into what the extended + // operator has actually done since it comes from "user-space"; + // external to the extension api) + // NOTE: pruning is here only to allow the extension api to + // emulate the RFC api; user-defined operations may perform complex + // removals, but they may not be visible during the prune process. + // It is recommended that the user stick to the RFC 'remove' operation + // and not implemennt their own removal operations in extended-space; + // or at least limit removal to the provided path, and not perform other mutations + // combined with removals + if (Array.isArray(targetObj)) { + graftTgt.splice(~~graftKey, 1); + } + else { + delete graftTgt[graftKey]; + } + } + return; + } + for (var i = 0; i < pathComponents.comps.length; i++) { + graftKey = pathComponents.comps[i]; + graft = graft[graftKey]; + // if there is no value in the target obj at the current key, + // than this is a graft point + if (pathComponents.modType === 'graft' && graftTgt[graftKey] === undefined) { + // if both target and source are undefined - No Op + if (graft === undefined) { + return; + } + break; + } + // there was a removal; the graft point needs to be the 2nd to last path comp + // a.k.a. the parent obj of the pruned key + // in order to preserve additional structure that was not pruned + if (i === pathComponents.comps.length - 2) { + break; + } + graftTgt = graftTgt[graftKey]; + } + // graft + graftTgt[graftKey] = graft; +} +exports._graftTree = _graftTree; /***/ }), @@ -281,6 +355,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); var helpers_js_1 = __webpack_require__(0); exports.JsonPatchError = helpers_js_1.PatchError; exports.deepClone = helpers_js_1._deepClone; +; +/* Registry of Extended operations */ +var xOpRegistry = new Map(); /* We use a Javascript hash to store each function. Each hash entry (property) uses the operation identifiers specified in rfc6902. @@ -332,8 +409,8 @@ var objOps = { /* The operations applicable to an array. Many are the same as for the object */ var arrOps = { add: function (arr, i, document) { - if (helpers_js_1.isInteger(i)) { - arr.splice(i, 0, this.value); + if (helpers_js_1.isInteger(String(i))) { + arr.splice(~~i, 0, this.value); } else { // array props arr[i] = this.value; @@ -342,7 +419,7 @@ var arrOps = { return { newDocument: document, index: i }; }, remove: function (arr, i, document) { - var removedList = arr.splice(i, 1); + var removedList = arr.splice(~~i, 1); return { newDocument: document, removed: removedList[0] }; }, replace: function (arr, i, document) { @@ -355,6 +432,55 @@ var arrOps = { test: objOps.test, _get: objOps._get }; +/** + * Registers an extended (non-RFC 6902) operation for processing. + * Will overwrite configs that already exist for given xid string + * + * @param xid The operation id (must follow the convention /^x-[a-z]+$/ + * to avoid visual confusion with the RFC's ops) + * @param config the operation configuration object containing + * the array, and object operators (functions), and a validator function for + * the extended operation + */ +function useExtendedOperation(xid, config) { + if (!helpers_js_1.isValidExtendedOpId(xid)) { + throw new exports.JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + // basic checks for all props + if (typeof config.arr !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `arr` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.obj !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `obj` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.validator !== 'function') { + throw new exports.JsonPatchError('Extended operation config has invalid `validator` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + // register config as immutable obj + xOpRegistry.set(xid, Object.freeze(config)); +} +exports.useExtendedOperation = useExtendedOperation; +/** + * Performs check for a registered extended (non-RFC 6902) operation. + * + * @param xid the qualified ("x-") extended operation name + * @return boolean true if xop is registered as an extended operation + */ +function hasExtendedOperation(xid) { + if (!helpers_js_1.isValidExtendedOpId(xid)) { + throw new exports.JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + return xOpRegistry.has(xid); +} +exports.hasExtendedOperation = hasExtendedOperation; +/** + * Removes all previously registered extended operation configurations. + * (primarily used during unit testing) + */ +function unregisterAllExtendedOperations() { + xOpRegistry.clear(); +} +exports.unregisterAllExtendedOperations = unregisterAllExtendedOperations; /** * Retrieves a value from a JSON document by a JSON pointer. * Returns the value. @@ -435,6 +561,31 @@ function applyOperation(document, operation, validateOperation, mutateDocument, operation.value = document; return returnValue; } + else if (operation.op === 'x') { + // get extended config + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // at empty (root) path default to obj operator + var workingDocument = document; + var obj = document; + if (!mutateDocument) { + obj = workingDocument = helpers_js_1._deepClone(document); + } + var result = { newDocument: operation.value }; + // if resolve is true, allow extended operator to run against supplied + // document object/clone + if (operation.resolve === true) { + result = xConfig.obj.call(operation, obj, '', workingDocument); + // in resolve mode, allow operator result of undefined to revert back to + // original document + if (result === undefined) { + return { newDocument: document }; + } + } + return result; + } else { /* bad operation */ if (validateOperation) { throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); @@ -455,6 +606,18 @@ function applyOperation(document, operation, validateOperation, mutateDocument, var len = keys.length; var existingPathFragment = undefined; var key = void 0; + var xConfig = void 0; + var workingDocument = document; + var graftPath = undefined; + if (operation.op === 'x') { + xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // make another clone to allow configured operator to 'abort' or 'no op' + // by returning a strict undefined + workingDocument = obj = helpers_js_1._deepClone(obj); + } var validateFunction = void 0; if (typeof validateOperation == 'function') { validateFunction = validateOperation; @@ -467,8 +630,10 @@ function applyOperation(document, operation, validateOperation, mutateDocument, if (key && key.indexOf('~') != -1) { key = helpers_js_1.unescapePathComponent(key); } - if (banPrototypeModifications && key == '__proto__') { - throw new TypeError('JSON-Patch: modifying `__proto__` 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 (banPrototypeModifications && + (key == '__proto__' || + (key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) { + throw new TypeError(helpers_js_1.PROTO_ERROR_MSG); } if (validateOperation) { if (existingPathFragment === undefined) { @@ -485,8 +650,32 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } t++; if (Array.isArray(obj)) { + // don't coerce an empty key string into an integer (below w/~~) + // if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key === '') { + throw new exports.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); + } if (key === '-') { key = obj.length; + // make some adjustments for grafting with extended ops + if (operation.op === 'x') { + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + } + } + // extended operations can use a special 'end element' sentinel in array paths + else if (operation.op === 'x' && key === '--') { + key = obj.length - 1; + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } } else { if (validateOperation && !helpers_js_1.isInteger(key)) { @@ -494,9 +683,50 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } // only parse key when it's an integer for `arr.prop` to work else if (helpers_js_1.isInteger(key)) { key = ~~key; + // don't allow arbitrary idx creation if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key >= obj.length) { + throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); + } } } if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (typeof key === 'string' && key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended array patch + var result = xConfig.arr.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + // check for ambiguous results + if (result.removed !== undefined && operation.resolve === true) { + // can't use resolve with a removal; ambiguous removal path + throw new exports.JsonPatchError('Extended operation should not remove items while resolving undefined paths', 'OPERATION_X_AMBIGUOUS_REMOVAL', index, operation, document); + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + helpers_js_1._graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } if (validateOperation && operation.op === "add" && key > obj.length) { throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); } @@ -509,6 +739,38 @@ function applyOperation(document, operation, validateOperation, mutateDocument, } else { if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended obj patch + var result = xConfig.obj.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + helpers_js_1._graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch if (returnValue.test === false) { throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); @@ -516,6 +778,25 @@ function applyOperation(document, operation, validateOperation, mutateDocument, return returnValue; } } + // extended operation forced path resolution + if (operation.op === 'x' && xConfig && obj[key] === undefined) { + // get first path where something isn't defined + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + if (operation.resolve === true) { + // add resolvable nodes + // check next key to determine object creation strategy + if (Array.isArray(document) && helpers_js_1.isInteger(String(keys[t]))) { // goofy cast + // if original document is an array, numeric path elements + // should add new arrays with minimum capacity + obj[key] = new Array(~~keys[t] + 1); + } + else { + obj[key] = {}; + } + } + } obj = obj[key]; // If we have more keys in the path, but the next value isn't a non-null object, // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. @@ -589,6 +870,32 @@ function validator(operation, index, document, existingPathFragment) { if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { throw new exports.JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); } + // check for extended ops + if (operation.op === 'x') { + // default validations + if (!helpers_js_1.isValidExtendedOpId(operation.xid)) { + throw new exports.JsonPatchError('Operation `xid` property is not present or invalid string', 'OPERATION_X_ID_INVALID', index, operation, document); + } + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new exports.JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + if (typeof operation.path !== 'string') { + throw new exports.JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { + // paths that aren't empty string should start with "/" + throw new exports.JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path === '/') { + throw new exports.JsonPatchError('Operation `path` slash-only is ambiguous', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); + } + if (operation.args !== undefined && !Array.isArray(operation.args)) { + throw new exports.JsonPatchError('Operation `args` property is not an array', 'OPERATION_X_ARGS_NOT_ARRAY', index, operation, document); + } + // we made it this far, now run the operation's configured validator + xConfig.validator.call(operation, operation, index, document, existingPathFragment); + } else if (!objOps[operation.op]) { throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); } @@ -742,7 +1049,7 @@ exports.unescapePathComponent = helpers.unescapePathComponent; Object.defineProperty(exports, "__esModule", { value: true }); /*! * https://github.com/Starcounter-Jack/JSON-Patch - * (c) 2017 Joachim Wester + * (c) 2017-2021 Joachim Wester * MIT license */ var helpers_js_1 = __webpack_require__(0); diff --git a/dist/fast-json-patch.min.js b/dist/fast-json-patch.min.js index ee84326..35b59c9 100644 --- a/dist/fast-json-patch.min.js +++ b/dist/fast-json-patch.min.js @@ -1,14 +1,14 @@ -/*! fast-json-patch, version: 3.1.0 */ +/*! fast-json-patch, version: 4.0.0-rc.1 */ 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){ /*! * https://github.com/Starcounter-Jack/JSON-Patch * (c) 2017 Joachim Wester * MIT license */ -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;r=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;a=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=s,t.getPath=function(e,t){if(e===t)return"/";var r=s(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;r=3&&0===e.indexOf("x-")},t._graftTree=function(e,t,r){if(0!==r.comps.length){var n=t,o=e,i="";if(1===r.comps.length)return i=r.comps[0],void("graft"===r.modType?n[i]=o[i]:Array.isArray(t)?n.splice(~~i,1):delete n[i]);for(var a=0;a0&&"constructor"==w[E-1]))throw new TypeError(n.PROTO_ERROR_MSG);if(s&&void 0===m&&(void 0===O[A]?m=w.slice(0,E).join("/"):E==y-1&&(m=r.path),void 0!==m&&I(r,0,e,m)),E++,Array.isArray(O)){if("x"===r.op&&!0!==r.resolve&&""===A)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);if("-"===A)A=O.length,"x"===r.op&&(w[E-1]=A.toString(10),void 0===g&&(g=w.slice(0,E).join("/")));else if("x"===r.op&&"--"===A)A=O.length-1,w[E-1]=A.toString(10),void 0===g&&(g=w.slice(0,E).join("/"));else{if(s&&!n.isInteger(A))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);if(n.isInteger(A)&&(A=~~A,"x"===r.op&&!0!==r.resolve&&A>=O.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(E>=y){if("x"===r.op&&P){if("string"==typeof A&&0===A.length&&!r.resolve)return{newDocument:r.value};if(void 0===(_=P.arr.call(r,O,A,v)))return{newDocument:e};if(void 0!==_.removed&&!0===r.resolve)throw new t.JsonPatchError("Extended operation should not remove items while resolving undefined paths","OPERATION_X_AMBIGUOUS_REMOVAL",h,r,e);if(c){var T={modType:"graft",comps:void 0===g?w.slice(1,E):g.split("/").slice(1)};return void 0!==_.removed&&(T={modType:"prune",comps:w.slice(1,E)}),n._graftTree(_.newDocument,e,T),{newDocument:e}}return _}if(s&&"add"===r.op&&A>O.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,O,A,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",h,r,e);return l}}else if(E>=y){if("x"===r.op&&P){if(0===A.length&&!r.resolve)return{newDocument:r.value};if(void 0===(_=P.obj.call(r,O,A,v)))return{newDocument:e};if(c){T={modType:"graft",comps:void 0===g?w.slice(1,E):g.split("/").slice(1)};return void 0!==_.removed&&(T={modType:"prune",comps:w.slice(1,E)}),n._graftTree(_.newDocument,e,T),{newDocument:e}}return _}if(!1===(l=i[r.op].call(r,O,A,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",h,r,e);return l}if("x"===r.op&&P&&void 0===O[A]&&(void 0===g&&(g=w.slice(0,E).join("/")),!0===r.resolve&&(Array.isArray(e)&&n.isInteger(String(w[E]))?O[A]=new Array(1+~~w[E]):O[A]={})),O=O[A],s&&E0)throw new t.JsonPatchError('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",r,e,a);if("/"===e.path)throw new t.JsonPatchError("Operation `path` slash-only is ambiguous","OPERATION_PATH_UNRESOLVABLE",r,e,a);if(void 0!==e.args&&!Array.isArray(e.args))throw new t.JsonPatchError("Operation `args` property is not an array","OPERATION_X_ARGS_NOT_ARRAY",r,e,a);s.validator.call(e,e,r,a,p)}else{if(!i[e.op])throw new t.JsonPatchError("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",r,e,a);if("string"!=typeof e.path)throw new t.JsonPatchError("Operation `path` property is not a string","OPERATION_PATH_INVALID",r,e,a);if(0!==e.path.indexOf("/")&&e.path.length>0)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 c=e.path.split("/").length,u=p.split("/").length;if(c!==u+1&&c!==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!==p)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 d=f([{op:"_get",path:e.from,value:void 0}],a);if(d&&"OPERATION_PATH_UNRESOLVABLE"===d.name)throw new t.JsonPatchError("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",r,e,a)}}}function f(e,r,o){try{if(!Array.isArray(e))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(r)c(n._deepClone(r),n._deepClone(e),o||!0);else{o=o||u;for(var i=0;i0&&(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;c0&&(e.patches=[],e.callback&&e.callback(n)),n}function c(e,t,r,o,i){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var a=n._objectKeys(t),p=n._objectKeys(e),s=!1,u=p.length-1;u>=0;u--){var f=e[h=p[u]];if(!n.hasOwnProperty(t,h)||void 0===t[h]&&void 0!==f&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(i&&r.push({op:"test",path:o+"/"+n.escapePathComponent(h),value:n._deepClone(f)}),r.push({op:"remove",path:o+"/"+n.escapePathComponent(h)}),s=!0):(i&&r.push({op:"test",path:o,value:e}),r.push({op:"replace",path:o,value:t}),!0);else{var d=t[h];"object"==typeof f&&null!=f&&"object"==typeof d&&null!=d&&Array.isArray(f)===Array.isArray(d)?c(f,d,r,o+"/"+n.escapePathComponent(h),i):f!==d&&(!0,i&&r.push({op:"test",path:o+"/"+n.escapePathComponent(h),value:n._deepClone(f)}),r.push({op:"replace",path:o+"/"+n.escapePathComponent(h),value:n._deepClone(d)}))}}if(s||a.length!=p.length)for(u=0;u | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation; +export declare type Operation = AddOperation | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation | ExtendedMutationOperation; export interface Validator { (operation: Operation, index: number, document: T, existingPathFragment: string): void; } @@ -10,6 +10,12 @@ export interface OperationResult { test?: boolean; newDocument: T; } +export interface ArrayOperator { + (arr: Array, i: number | string, document: T): R; +} +export interface ObjectOperator { + (obj: T, key: string, document: T): R; +} export interface BaseOperation { path: string; } @@ -40,9 +46,44 @@ export interface GetOperation extends BaseOperation { op: '_get'; value: T; } +export interface ExtendedMutationOperation extends BaseOperation { + op: 'x'; + args?: Array; + resolve?: boolean; + xid: string; + value?: any; +} +export interface ExtendedMutationOperationConfig { + readonly arr: ArrayOperator | undefined>; + readonly obj: ObjectOperator | undefined>; + readonly validator: Validator; +} export interface PatchResult extends Array> { newDocument: T; } +/** + * Registers an extended (non-RFC 6902) operation for processing. + * Will overwrite configs that already exist for given xid string + * + * @param xid The operation id (must follow the convention /^x-[a-z]+$/ + * to avoid visual confusion with the RFC's ops) + * @param config the operation configuration object containing + * the array, and object operators (functions), and a validator function for + * the extended operation + */ +export declare function useExtendedOperation(xid: string, config: ExtendedMutationOperationConfig): void; +/** + * Performs check for a registered extended (non-RFC 6902) operation. + * + * @param xid the qualified ("x-") extended operation name + * @return boolean true if xop is registered as an extended operation + */ +export declare function hasExtendedOperation(xid: string): boolean; +/** + * Removes all previously registered extended operation configurations. + * (primarily used during unit testing) + */ +export declare function unregisterAllExtendedOperations(): void; /** * Retrieves a value from a JSON document by a JSON pointer. * Returns the value. diff --git a/module/core.mjs b/module/core.mjs index 2eac75c..91c544c 100644 --- a/module/core.mjs +++ b/module/core.mjs @@ -1,6 +1,9 @@ -import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined } from './helpers.mjs'; +import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined, PROTO_ERROR_MSG, isValidExtendedOpId, _graftTree } from './helpers.mjs'; export var JsonPatchError = PatchError; export var deepClone = _deepClone; +; +/* Registry of Extended operations */ +var xOpRegistry = new Map(); /* We use a Javascript hash to store each function. Each hash entry (property) uses the operation identifiers specified in rfc6902. @@ -52,8 +55,8 @@ var objOps = { /* The operations applicable to an array. Many are the same as for the object */ var arrOps = { add: function (arr, i, document) { - if (isInteger(i)) { - arr.splice(i, 0, this.value); + if (isInteger(String(i))) { + arr.splice(~~i, 0, this.value); } else { // array props arr[i] = this.value; @@ -62,7 +65,7 @@ var arrOps = { return { newDocument: document, index: i }; }, remove: function (arr, i, document) { - var removedList = arr.splice(i, 1); + var removedList = arr.splice(~~i, 1); return { newDocument: document, removed: removedList[0] }; }, replace: function (arr, i, document) { @@ -75,6 +78,52 @@ var arrOps = { test: objOps.test, _get: objOps._get }; +/** + * Registers an extended (non-RFC 6902) operation for processing. + * Will overwrite configs that already exist for given xid string + * + * @param xid The operation id (must follow the convention /^x-[a-z]+$/ + * to avoid visual confusion with the RFC's ops) + * @param config the operation configuration object containing + * the array, and object operators (functions), and a validator function for + * the extended operation + */ +export function useExtendedOperation(xid, config) { + if (!isValidExtendedOpId(xid)) { + throw new JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + // basic checks for all props + if (typeof config.arr !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `arr` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.obj !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `obj` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + if (typeof config.validator !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `validator` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + // register config as immutable obj + xOpRegistry.set(xid, Object.freeze(config)); +} +/** + * Performs check for a registered extended (non-RFC 6902) operation. + * + * @param xid the qualified ("x-") extended operation name + * @return boolean true if xop is registered as an extended operation + */ +export function hasExtendedOperation(xid) { + if (!isValidExtendedOpId(xid)) { + throw new JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + return xOpRegistry.has(xid); +} +/** + * Removes all previously registered extended operation configurations. + * (primarily used during unit testing) + */ +export function unregisterAllExtendedOperations() { + xOpRegistry.clear(); +} /** * Retrieves a value from a JSON document by a JSON pointer. * Returns the value. @@ -154,6 +203,31 @@ export function applyOperation(document, operation, validateOperation, mutateDoc operation.value = document; return returnValue; } + else if (operation.op === 'x') { + // get extended config + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // at empty (root) path default to obj operator + var workingDocument = document; + var obj = document; + if (!mutateDocument) { + obj = workingDocument = _deepClone(document); + } + var result = { newDocument: operation.value }; + // if resolve is true, allow extended operator to run against supplied + // document object/clone + if (operation.resolve === true) { + result = xConfig.obj.call(operation, obj, '', workingDocument); + // in resolve mode, allow operator result of undefined to revert back to + // original document + if (result === undefined) { + return { newDocument: document }; + } + } + return result; + } else { /* bad operation */ if (validateOperation) { throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); @@ -174,6 +248,18 @@ export function applyOperation(document, operation, validateOperation, mutateDoc var len = keys.length; var existingPathFragment = undefined; var key = void 0; + var xConfig = void 0; + var workingDocument = document; + var graftPath = undefined; + if (operation.op === 'x') { + xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // make another clone to allow configured operator to 'abort' or 'no op' + // by returning a strict undefined + workingDocument = obj = _deepClone(obj); + } var validateFunction = void 0; if (typeof validateOperation == 'function') { validateFunction = validateOperation; @@ -189,7 +275,7 @@ export function applyOperation(document, operation, validateOperation, mutateDoc if (banPrototypeModifications && (key == '__proto__' || (key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) { - 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'); + throw new TypeError(PROTO_ERROR_MSG); } if (validateOperation) { if (existingPathFragment === undefined) { @@ -206,8 +292,32 @@ export function applyOperation(document, operation, validateOperation, mutateDoc } t++; if (Array.isArray(obj)) { + // don't coerce an empty key string into an integer (below w/~~) + // if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key === '') { + 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); + } if (key === '-') { key = obj.length; + // make some adjustments for grafting with extended ops + if (operation.op === 'x') { + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + } + } + // extended operations can use a special 'end element' sentinel in array paths + else if (operation.op === 'x' && key === '--') { + key = obj.length - 1; + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } } else { if (validateOperation && !isInteger(key)) { @@ -215,9 +325,50 @@ export function applyOperation(document, operation, validateOperation, mutateDoc } // only parse key when it's an integer for `arr.prop` to work else if (isInteger(key)) { key = ~~key; + // don't allow arbitrary idx creation if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key >= obj.length) { + 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); + } } } if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (typeof key === 'string' && key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended array patch + var result = xConfig.arr.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + // check for ambiguous results + if (result.removed !== undefined && operation.resolve === true) { + // can't use resolve with a removal; ambiguous removal path + throw new JsonPatchError('Extended operation should not remove items while resolving undefined paths', 'OPERATION_X_AMBIGUOUS_REMOVAL', index, operation, document); + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + _graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } if (validateOperation && operation.op === "add" && key > obj.length) { 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); } @@ -230,6 +381,38 @@ export function applyOperation(document, operation, validateOperation, mutateDoc } else { if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended obj patch + var result = xConfig.obj.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + if (mutateDocument) { + // default graft path + var pc = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + _graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch if (returnValue.test === false) { throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); @@ -237,6 +420,25 @@ export function applyOperation(document, operation, validateOperation, mutateDoc return returnValue; } } + // extended operation forced path resolution + if (operation.op === 'x' && xConfig && obj[key] === undefined) { + // get first path where something isn't defined + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + if (operation.resolve === true) { + // add resolvable nodes + // check next key to determine object creation strategy + if (Array.isArray(document) && isInteger(String(keys[t]))) { // goofy cast + // if original document is an array, numeric path elements + // should add new arrays with minimum capacity + obj[key] = new Array(~~keys[t] + 1); + } + else { + obj[key] = {}; + } + } + } obj = obj[key]; // If we have more keys in the path, but the next value isn't a non-null object, // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. @@ -307,6 +509,32 @@ export function validator(operation, index, document, existingPathFragment) { if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); } + // check for extended ops + if (operation.op === 'x') { + // default validations + if (!isValidExtendedOpId(operation.xid)) { + throw new JsonPatchError('Operation `xid` property is not present or invalid string', 'OPERATION_X_ID_INVALID', index, operation, document); + } + var xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + if (typeof operation.path !== 'string') { + throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { + // paths that aren't empty string should start with "/" + throw new JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); + } + if (operation.path === '/') { + throw new JsonPatchError('Operation `path` slash-only is ambiguous', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); + } + if (operation.args !== undefined && !Array.isArray(operation.args)) { + throw new JsonPatchError('Operation `args` property is not an array', 'OPERATION_X_ARGS_NOT_ARRAY', index, operation, document); + } + // we made it this far, now run the operation's configured validator + xConfig.validator.call(operation, operation, index, document, existingPathFragment); + } else if (!objOps[operation.op]) { throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); } diff --git a/module/duplex.mjs b/module/duplex.mjs index 6b7e632..d27618e 100644 --- a/module/duplex.mjs +++ b/module/duplex.mjs @@ -1,6 +1,6 @@ /*! * https://github.com/Starcounter-Jack/JSON-Patch - * (c) 2017 Joachim Wester + * (c) 2017-2021 Joachim Wester * MIT license */ import { _deepClone, _objectKeys, escapePathComponent, hasOwnProperty } from './helpers.mjs'; diff --git a/module/helpers.d.ts b/module/helpers.d.ts index 034b1a6..f0bf84c 100644 --- a/module/helpers.d.ts +++ b/module/helpers.d.ts @@ -31,7 +31,7 @@ export declare function getPath(root: Object, obj: Object): string; * Recursively checks whether an object has any undefined values inside. */ export declare function hasUndefined(obj: any): boolean; -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'; +export declare type JsonPatchErrorName = 'SEQUENCE_NOT_AN_ARRAY' | 'OPERATION_NOT_AN_OBJECT' | 'OPERATION_OP_INVALID' | 'OPERATION_X_ARGS_NOT_ARRAY' | 'OPERATION_X_OP_INVALID' | 'OPERATION_X_CONFIG_INVALID' | 'OPERATION_X_ID_INVALID' | 'OPERATION_X_AMBIGUOUS_REMOVAL' | 'OPERATION_X_OPERATOR_EXCEPTION' | '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'; export declare class PatchError extends Error { name: JsonPatchErrorName; index?: number; @@ -39,3 +39,23 @@ export declare class PatchError extends Error { tree?: any; constructor(message: string, name: JsonPatchErrorName, index?: number, operation?: any, tree?: any); } +export declare const PROTO_ERROR_MSG = "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"; +export declare function isValidExtendedOpId(xid: string): boolean; +declare type KeyType = string | number; +interface PruneComponents { + modType: 'prune'; + comps: Array; +} +interface GraftComponents { + modType: 'graft'; + comps: Array; +} +export declare type PathComponents = PruneComponents | GraftComponents; +/** + * attach/remove source tree to/from target tree at appropriate path. + * modifies targetObj (in place) by reference. + * + * This is necessary to deal with JS "by value" semantics + */ +export declare function _graftTree(sourceObj: any, targetObj: any, pathComponents: PathComponents): void; +export {}; diff --git a/module/helpers.mjs b/module/helpers.mjs index 10844fe..e16c240 100644 --- a/module/helpers.mjs +++ b/module/helpers.mjs @@ -169,3 +169,75 @@ var PatchError = /** @class */ (function (_super) { return PatchError; }(Error)); export { PatchError }; +// exported for use in jasmine test +export var PROTO_ERROR_MSG = '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'; +export function isValidExtendedOpId(xid) { + return typeof xid === 'string' && xid.length >= 3 && xid.indexOf('x-') === 0; +} +; +; +/** + * attach/remove source tree to/from target tree at appropriate path. + * modifies targetObj (in place) by reference. + * + * This is necessary to deal with JS "by value" semantics + */ +export function _graftTree(sourceObj, targetObj, pathComponents) { + if (pathComponents.comps.length === 0) { + // no changes + return; + } + // traverse document trees until at the appropriate parent level + var graftTgt = targetObj; + var graft = sourceObj; + var graftKey = ''; + // single component is top-level key + if (pathComponents.comps.length === 1) { + graftKey = pathComponents.comps[0]; + if (pathComponents.modType === 'graft') { + graftTgt[graftKey] = graft[graftKey]; + } + else { + // top-level prune is a "best guess" that the provided key was removed from + // the top-level object (we have no visibility into what the extended + // operator has actually done since it comes from "user-space"; + // external to the extension api) + // NOTE: pruning is here only to allow the extension api to + // emulate the RFC api; user-defined operations may perform complex + // removals, but they may not be visible during the prune process. + // It is recommended that the user stick to the RFC 'remove' operation + // and not implemennt their own removal operations in extended-space; + // or at least limit removal to the provided path, and not perform other mutations + // combined with removals + if (Array.isArray(targetObj)) { + graftTgt.splice(~~graftKey, 1); + } + else { + delete graftTgt[graftKey]; + } + } + return; + } + for (var i = 0; i < pathComponents.comps.length; i++) { + graftKey = pathComponents.comps[i]; + graft = graft[graftKey]; + // if there is no value in the target obj at the current key, + // than this is a graft point + if (pathComponents.modType === 'graft' && graftTgt[graftKey] === undefined) { + // if both target and source are undefined - No Op + if (graft === undefined) { + return; + } + break; + } + // there was a removal; the graft point needs to be the 2nd to last path comp + // a.k.a. the parent obj of the pruned key + // in order to preserve additional structure that was not pruned + if (i === pathComponents.comps.length - 2) { + break; + } + graftTgt = graftTgt[graftKey]; + } + // graft + graftTgt[graftKey] = graft; +} diff --git a/package-lock.json b/package-lock.json index 34f7698..fdb41c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fast-json-patch", - "version": "3.1.0", + "version": "4.0.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fast-json-patch", - "version": "3.1.0", + "version": "4.0.0-rc.1", "license": "MIT", "devDependencies": { "benchmark": "^2.1.4", @@ -15,6 +15,7 @@ "chalk": "^2.4.2", "event-target-shim": "^5.0.1", "fast-deep-equal": "^2.0.1", + "glob": "^7.1.7", "http-server": "^0.12.3", "jasmine": "^3.4.0", "request": "^2.88.0", @@ -1914,7 +1915,71 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", "bundleDependencies": [ - "node-pre-gyp" + "node-pre-gyp", + "abbrev", + "ansi-regex", + "aproba", + "are-we-there-yet", + "balanced-match", + "brace-expansion", + "chownr", + "code-point-at", + "concat-map", + "console-control-strings", + "core-util-is", + "debug", + "deep-extend", + "delegates", + "detect-libc", + "fs-minipass", + "fs.realpath", + "gauge", + "glob", + "has-unicode", + "iconv-lite", + "ignore-walk", + "inflight", + "inherits", + "ini", + "is-fullwidth-code-point", + "isarray", + "minimatch", + "minimist", + "minipass", + "minizlib", + "mkdirp", + "ms", + "needle", + "nopt", + "npm-bundled", + "npm-packlist", + "npmlog", + "number-is-nan", + "object-assign", + "once", + "os-homedir", + "os-tmpdir", + "osenv", + "path-is-absolute", + "process-nextick-args", + "rc", + "readable-stream", + "rimraf", + "safe-buffer", + "safer-buffer", + "sax", + "semver", + "set-blocking", + "signal-exit", + "string_decoder", + "string-width", + "strip-ansi", + "strip-json-comments", + "tar", + "util-deprecate", + "wide-align", + "wrappy", + "yallist" ], "dev": true, "hasInstallScript": true, @@ -1932,6 +1997,8 @@ }, "node_modules/fsevents/node_modules/abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "inBundle": true, "license": "ISC", @@ -1939,6 +2006,8 @@ }, "node_modules/fsevents/node_modules/ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "inBundle": true, "license": "MIT", @@ -1949,6 +2018,8 @@ }, "node_modules/fsevents/node_modules/aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "inBundle": true, "license": "ISC", @@ -1956,6 +2027,8 @@ }, "node_modules/fsevents/node_modules/are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "inBundle": true, "license": "ISC", @@ -1967,6 +2040,8 @@ }, "node_modules/fsevents/node_modules/balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "inBundle": true, "license": "MIT", @@ -1974,6 +2049,8 @@ }, "node_modules/fsevents/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "inBundle": true, "license": "MIT", @@ -1985,6 +2062,8 @@ }, "node_modules/fsevents/node_modules/chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "dev": true, "inBundle": true, "license": "ISC", @@ -1992,6 +2071,8 @@ }, "node_modules/fsevents/node_modules/code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "inBundle": true, "license": "MIT", @@ -2002,6 +2083,8 @@ }, "node_modules/fsevents/node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "inBundle": true, "license": "MIT", @@ -2009,6 +2092,8 @@ }, "node_modules/fsevents/node_modules/console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "inBundle": true, "license": "ISC", @@ -2016,6 +2101,8 @@ }, "node_modules/fsevents/node_modules/core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "inBundle": true, "license": "MIT", @@ -2023,6 +2110,8 @@ }, "node_modules/fsevents/node_modules/debug": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "inBundle": true, "license": "MIT", @@ -2033,6 +2122,8 @@ }, "node_modules/fsevents/node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "inBundle": true, "license": "MIT", @@ -2043,6 +2134,8 @@ }, "node_modules/fsevents/node_modules/delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "inBundle": true, "license": "MIT", @@ -2050,6 +2143,8 @@ }, "node_modules/fsevents/node_modules/detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -2063,6 +2158,8 @@ }, "node_modules/fsevents/node_modules/fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -2073,6 +2170,8 @@ }, "node_modules/fsevents/node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "inBundle": true, "license": "ISC", @@ -2080,6 +2179,8 @@ }, "node_modules/fsevents/node_modules/gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "inBundle": true, "license": "ISC", @@ -2097,6 +2198,8 @@ }, "node_modules/fsevents/node_modules/glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -2115,6 +2218,8 @@ }, "node_modules/fsevents/node_modules/has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "inBundle": true, "license": "ISC", @@ -2122,6 +2227,8 @@ }, "node_modules/fsevents/node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "inBundle": true, "license": "MIT", @@ -2135,6 +2242,8 @@ }, "node_modules/fsevents/node_modules/ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -2145,6 +2254,8 @@ }, "node_modules/fsevents/node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "inBundle": true, "license": "ISC", @@ -2156,6 +2267,8 @@ }, "node_modules/fsevents/node_modules/inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "inBundle": true, "license": "ISC", @@ -2163,6 +2276,8 @@ }, "node_modules/fsevents/node_modules/ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "inBundle": true, "license": "ISC", @@ -2173,6 +2288,8 @@ }, "node_modules/fsevents/node_modules/is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "inBundle": true, "license": "MIT", @@ -2186,6 +2303,8 @@ }, "node_modules/fsevents/node_modules/isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "inBundle": true, "license": "MIT", @@ -2193,6 +2312,8 @@ }, "node_modules/fsevents/node_modules/minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "inBundle": true, "license": "ISC", @@ -2206,6 +2327,8 @@ }, "node_modules/fsevents/node_modules/minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "inBundle": true, "license": "MIT", @@ -2213,6 +2336,8 @@ }, "node_modules/fsevents/node_modules/minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "dev": true, "inBundle": true, "license": "ISC", @@ -2224,6 +2349,8 @@ }, "node_modules/fsevents/node_modules/minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "dev": true, "inBundle": true, "license": "MIT", @@ -2234,6 +2361,8 @@ }, "node_modules/fsevents/node_modules/mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "inBundle": true, "license": "MIT", @@ -2247,6 +2376,8 @@ }, "node_modules/fsevents/node_modules/ms": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true, "inBundle": true, "license": "MIT", @@ -2254,6 +2385,8 @@ }, "node_modules/fsevents/node_modules/needle": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "dev": true, "inBundle": true, "license": "MIT", @@ -2272,6 +2405,8 @@ }, "node_modules/fsevents/node_modules/node-pre-gyp": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -2294,6 +2429,8 @@ }, "node_modules/fsevents/node_modules/nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "inBundle": true, "license": "ISC", @@ -2308,6 +2445,8 @@ }, "node_modules/fsevents/node_modules/npm-bundled": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "dev": true, "inBundle": true, "license": "ISC", @@ -2315,6 +2454,8 @@ }, "node_modules/fsevents/node_modules/npm-packlist": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "dev": true, "inBundle": true, "license": "ISC", @@ -2326,6 +2467,8 @@ }, "node_modules/fsevents/node_modules/npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "inBundle": true, "license": "ISC", @@ -2339,6 +2482,8 @@ }, "node_modules/fsevents/node_modules/number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "inBundle": true, "license": "MIT", @@ -2349,6 +2494,8 @@ }, "node_modules/fsevents/node_modules/object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "inBundle": true, "license": "MIT", @@ -2359,6 +2506,8 @@ }, "node_modules/fsevents/node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "inBundle": true, "license": "ISC", @@ -2369,6 +2518,8 @@ }, "node_modules/fsevents/node_modules/os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "inBundle": true, "license": "MIT", @@ -2379,6 +2530,8 @@ }, "node_modules/fsevents/node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "inBundle": true, "license": "MIT", @@ -2389,6 +2542,8 @@ }, "node_modules/fsevents/node_modules/osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "inBundle": true, "license": "ISC", @@ -2400,6 +2555,8 @@ }, "node_modules/fsevents/node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "inBundle": true, "license": "MIT", @@ -2410,6 +2567,8 @@ }, "node_modules/fsevents/node_modules/process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "inBundle": true, "license": "MIT", @@ -2417,6 +2576,8 @@ }, "node_modules/fsevents/node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "inBundle": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", @@ -2433,6 +2594,8 @@ }, "node_modules/fsevents/node_modules/rc/node_modules/minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "inBundle": true, "license": "MIT", @@ -2440,6 +2603,8 @@ }, "node_modules/fsevents/node_modules/readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "inBundle": true, "license": "MIT", @@ -2456,6 +2621,8 @@ }, "node_modules/fsevents/node_modules/rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "inBundle": true, "license": "ISC", @@ -2469,6 +2636,8 @@ }, "node_modules/fsevents/node_modules/safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "inBundle": true, "license": "MIT", @@ -2476,6 +2645,8 @@ }, "node_modules/fsevents/node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "inBundle": true, "license": "MIT", @@ -2483,6 +2654,8 @@ }, "node_modules/fsevents/node_modules/sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "inBundle": true, "license": "ISC", @@ -2490,6 +2663,8 @@ }, "node_modules/fsevents/node_modules/semver": { "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true, "inBundle": true, "license": "ISC", @@ -2500,6 +2675,8 @@ }, "node_modules/fsevents/node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "inBundle": true, "license": "ISC", @@ -2507,6 +2684,8 @@ }, "node_modules/fsevents/node_modules/signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "inBundle": true, "license": "ISC", @@ -2514,6 +2693,8 @@ }, "node_modules/fsevents/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "inBundle": true, "license": "MIT", @@ -2524,6 +2705,8 @@ }, "node_modules/fsevents/node_modules/string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "inBundle": true, "license": "MIT", @@ -2539,6 +2722,8 @@ }, "node_modules/fsevents/node_modules/strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "inBundle": true, "license": "MIT", @@ -2552,6 +2737,8 @@ }, "node_modules/fsevents/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "inBundle": true, "license": "MIT", @@ -2562,6 +2749,8 @@ }, "node_modules/fsevents/node_modules/tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "dev": true, "inBundle": true, "license": "ISC", @@ -2581,6 +2770,8 @@ }, "node_modules/fsevents/node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "inBundle": true, "license": "MIT", @@ -2588,6 +2779,8 @@ }, "node_modules/fsevents/node_modules/wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "inBundle": true, "license": "ISC", @@ -2598,6 +2791,8 @@ }, "node_modules/fsevents/node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "inBundle": true, "license": "ISC", @@ -2605,6 +2800,8 @@ }, "node_modules/fsevents/node_modules/yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true, "inBundle": true, "license": "ISC", @@ -2650,9 +2847,9 @@ } }, "node_modules/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -2664,6 +2861,9 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -7739,24 +7939,32 @@ "dependencies": { "abbrev": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "bundled": true, "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "bundled": true, "dev": true, "optional": true }, "aproba": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "bundled": true, "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "bundled": true, "dev": true, "optional": true, @@ -7767,12 +7975,16 @@ }, "balanced-match": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "bundled": true, "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "bundled": true, "dev": true, "optional": true, @@ -7783,36 +7995,48 @@ }, "chownr": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", "bundled": true, "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "bundled": true, "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "bundled": true, "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "bundled": true, "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "bundled": true, "dev": true, "optional": true }, "debug": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "bundled": true, "dev": true, "optional": true, @@ -7822,24 +8046,32 @@ }, "deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "bundled": true, "dev": true, "optional": true }, "delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "bundled": true, "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "bundled": true, "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "bundled": true, "dev": true, "optional": true, @@ -7849,12 +8081,16 @@ }, "fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "bundled": true, "dev": true, "optional": true }, "gauge": { "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "bundled": true, "dev": true, "optional": true, @@ -7871,6 +8107,8 @@ }, "glob": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "bundled": true, "dev": true, "optional": true, @@ -7885,12 +8123,16 @@ }, "has-unicode": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "bundled": true, "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "bundled": true, "dev": true, "optional": true, @@ -7900,6 +8142,8 @@ }, "ignore-walk": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "bundled": true, "dev": true, "optional": true, @@ -7909,6 +8153,8 @@ }, "inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "bundled": true, "dev": true, "optional": true, @@ -7919,18 +8165,24 @@ }, "inherits": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "bundled": true, "dev": true, "optional": true }, "ini": { "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "bundled": true, "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "bundled": true, "dev": true, "optional": true, @@ -7940,12 +8192,16 @@ }, "isarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "bundled": true, "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "bundled": true, "dev": true, "optional": true, @@ -7955,12 +8211,16 @@ }, "minimist": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "bundled": true, "dev": true, "optional": true }, "minipass": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "bundled": true, "dev": true, "optional": true, @@ -7971,6 +8231,8 @@ }, "minizlib": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "bundled": true, "dev": true, "optional": true, @@ -7980,6 +8242,8 @@ }, "mkdirp": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "bundled": true, "dev": true, "optional": true, @@ -7989,12 +8253,16 @@ }, "ms": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "bundled": true, "dev": true, "optional": true }, "needle": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", + "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", "bundled": true, "dev": true, "optional": true, @@ -8006,6 +8274,8 @@ }, "node-pre-gyp": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "bundled": true, "dev": true, "optional": true, @@ -8024,6 +8294,8 @@ }, "nopt": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "bundled": true, "dev": true, "optional": true, @@ -8034,12 +8306,16 @@ }, "npm-bundled": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", + "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", "bundled": true, "dev": true, "optional": true, @@ -8050,6 +8326,8 @@ }, "npmlog": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "bundled": true, "dev": true, "optional": true, @@ -8062,18 +8340,24 @@ }, "number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "bundled": true, "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "bundled": true, "dev": true, "optional": true }, "once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "bundled": true, "dev": true, "optional": true, @@ -8083,18 +8367,24 @@ }, "os-homedir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "bundled": true, "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "bundled": true, "dev": true, "optional": true }, "osenv": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "bundled": true, "dev": true, "optional": true, @@ -8105,18 +8395,24 @@ }, "path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "bundled": true, "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "bundled": true, "dev": true, "optional": true }, "rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "bundled": true, "dev": true, "optional": true, @@ -8129,6 +8425,8 @@ "dependencies": { "minimist": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "bundled": true, "dev": true, "optional": true @@ -8137,6 +8435,8 @@ }, "readable-stream": { "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "bundled": true, "dev": true, "optional": true, @@ -8152,6 +8452,8 @@ }, "rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "bundled": true, "dev": true, "optional": true, @@ -8161,42 +8463,56 @@ }, "safe-buffer": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "bundled": true, "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "bundled": true, "dev": true, "optional": true }, "sax": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "bundled": true, "dev": true, "optional": true }, "semver": { "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "bundled": true, "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "bundled": true, "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "bundled": true, "dev": true, "optional": true }, "string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "bundled": true, "dev": true, "optional": true, @@ -8206,6 +8522,8 @@ }, "string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "bundled": true, "dev": true, "optional": true, @@ -8217,6 +8535,8 @@ }, "strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "bundled": true, "dev": true, "optional": true, @@ -8226,12 +8546,16 @@ }, "strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "bundled": true, "dev": true, "optional": true }, "tar": { "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "bundled": true, "dev": true, "optional": true, @@ -8247,12 +8571,16 @@ }, "util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "bundled": true, "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "bundled": true, "dev": true, "optional": true, @@ -8262,12 +8590,16 @@ }, "wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "bundled": true, "dev": true, "optional": true }, "yallist": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "bundled": true, "dev": true, "optional": true @@ -8305,9 +8637,9 @@ } }, "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", diff --git a/package.json b/package.json index f79b40d..b0cda3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fast-json-patch", - "version": "3.1.0", + "version": "4.0.0-rc.1", "description": "Fast implementation of JSON-Patch (RFC-6902) with duplex (observe changes) capabilities", "homepage": "https://github.com/Starcounter-Jack/JSON-Patch", "keywords": [ @@ -32,6 +32,7 @@ "chalk": "^2.4.2", "event-target-shim": "^5.0.1", "fast-deep-equal": "^2.0.1", + "glob": "^7.1.7", "http-server": "^0.12.3", "jasmine": "^3.4.0", "request": "^2.88.0", @@ -51,13 +52,14 @@ "build": "npm run tsc && webpack", "serve": "http-server -p 5000 --silent", "tsc-watch": "tsc -w", - "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", + "test": "npm run tsc && npm run test-core && npm run test-duplex && npm run test-extended && npm run test-commonjs && npm run test-webpack-import && npm run test-typings", "test-sauce": "npm run build && node test/Sauce/Runner.js", "test-commonjs": "jasmine test/spec/commonjs/requireSpec.js", "test-webpack-import": "webpack --env.NODE_ENV=test && jasmine test/spec/webpack/importSpec.build.js", "test-typings": "tsc test/spec/typings/typingsSpec.ts", "test-duplex": "node --experimental-modules jasmine-run.mjs test/**/*[sS]pec.mjs", "test-core": "node --experimental-modules jasmine-run.mjs 'test/spec/{jsonPatchTestsSpec,coreSpec,validateSpec}.mjs'", + "test-extended": "node --experimental-modules jasmine-run.mjs 'test/spec/extendedSpec.mjs'", "bench": "npm run bench-core && npm run bench-duplex", "bench-core": "node test/spec/coreBenchmark.js", "bench-duplex": "node test/spec/coreBenchmark.js && node test/spec/duplexBenchmark.js" diff --git a/src/core.ts b/src/core.ts index cac6607..128b0cc 100644 --- a/src/core.ts +++ b/src/core.ts @@ -5,23 +5,31 @@ */ declare var require: any; -import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined } from './helpers.js'; +import { PatchError, _deepClone, isInteger, unescapePathComponent, hasUndefined, PROTO_ERROR_MSG, isValidExtendedOpId, _graftTree, PathComponents } from './helpers.js'; export const JsonPatchError = PatchError; export const deepClone = _deepClone; -export type Operation = AddOperation | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation; +export type Operation = AddOperation | RemoveOperation | ReplaceOperation | MoveOperation | CopyOperation | TestOperation | GetOperation | ExtendedMutationOperation; export interface Validator { (operation: Operation, index: number, document: T, existingPathFragment: string): void; } export interface OperationResult { - removed?: any, - test?: boolean, + removed?: any; + test?: boolean; newDocument: T; } +export interface ArrayOperator { + (arr: Array, i: number | string, document: T): R; +} + +export interface ObjectOperator { + (obj: T, key: string, document: T): R; +} + export interface BaseOperation { path: string; } @@ -59,10 +67,53 @@ export interface GetOperation extends BaseOperation { op: '_get'; value: T; } + +/* Extended (non-RFC 6902) operation to perform an arbitrary + mutation/modification between existing value at 'path' and supplied 'value' + placing result in output document. + + Property 'xid' is the string name (in registry) of the extended operation + a.k.a. "the extended operation to be performed" + (analogous to 'op' in the RFC operation interfaces) + + Property 'args' is an optional array of additional arguments to be supplied to + the specified 'xid' extended operator + + Property 'resolve' is optional [default false] and will cause an unresolvable + path to be forced to exist in document to be patched: arrays will be padded + with undefined elements, empty objects will be created at each currently + undefined path component IFF current resolution is strictly equal to undefined + + */ + export interface ExtendedMutationOperation extends BaseOperation { + op: 'x'; + args?: Array; + resolve?: boolean; + xid: string, + value?: any; +} + +/* + A configuration object for extended mutation operations that includes + exactly 3 functions: + + 'arr' - the operation implementation for an array element in document + 'obj' - the operation implementation for an object in document + 'validator' - the validation function for this operation; should throw error if not valid + */ + export interface ExtendedMutationOperationConfig { + readonly arr: ArrayOperator | undefined>; + readonly obj: ObjectOperator | undefined>; + readonly validator: Validator; +}; + export interface PatchResult extends Array> { newDocument: T; } +/* Registry of Extended operations */ +const xOpRegistry = new Map>(); + /* We use a Javascript hash to store each function. Each hash entry (property) uses the operation identifiers specified in rfc6902. @@ -71,7 +122,7 @@ export interface PatchResult extends Array> { */ /* The operations applicable to an object */ -const objOps = { +const objOps: { readonly [index: string]: ObjectOperator> } = { add: function (obj, key, document) { obj[key] = this.value; return { newDocument: document }; @@ -124,10 +175,10 @@ const objOps = { }; /* The operations applicable to an array. Many are the same as for the object */ -var arrOps = { +var arrOps: { readonly [index: string]: ArrayOperator> } = { add: function (arr, i, document) { - if(isInteger(i)) { - arr.splice(i, 0, this.value); + if (isInteger(String(i))) { + arr.splice(~~i, 0, this.value); } else { // array props arr[i] = this.value; } @@ -135,7 +186,7 @@ var arrOps = { return { newDocument: document, index: i } }, remove: function (arr, i, document) { - var removedList = arr.splice(i, 1); + var removedList = arr.splice(~~i, 1); return { newDocument: document, removed: removedList[0] }; }, replace: function (arr, i, document) { @@ -149,6 +200,59 @@ var arrOps = { _get: objOps._get }; +/** + * Registers an extended (non-RFC 6902) operation for processing. + * Will overwrite configs that already exist for given xid string + * + * @param xid The operation id (must follow the convention /^x-[a-z]+$/ + * to avoid visual confusion with the RFC's ops) + * @param config the operation configuration object containing + * the array, and object operators (functions), and a validator function for + * the extended operation + */ + export function useExtendedOperation(xid: string, config: ExtendedMutationOperationConfig): void { + if (!isValidExtendedOpId(xid)) { + throw new JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + // basic checks for all props + if (typeof config.arr !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `arr` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + + if (typeof config.obj !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `obj` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + + if (typeof config.validator !== 'function') { + throw new JsonPatchError('Extended operation config has invalid `validator` function', 'OPERATION_X_CONFIG_INVALID', undefined, xid); + } + + // register config as immutable obj + xOpRegistry.set(xid, Object.freeze(config)); +} + +/** + * Performs check for a registered extended (non-RFC 6902) operation. + * + * @param xid the qualified ("x-") extended operation name + * @return boolean true if xop is registered as an extended operation + */ +export function hasExtendedOperation(xid: string): boolean { + if (!isValidExtendedOpId(xid)) { + throw new JsonPatchError('Extended operation `xid` has malformed id (MUST begin with `x-`)', 'OPERATION_X_ID_INVALID', undefined, xid); + } + return xOpRegistry.has(xid); +} + +/** + * Removes all previously registered extended operation configurations. + * (primarily used during unit testing) + */ +export function unregisterAllExtendedOperations(): void { + xOpRegistry.clear(); +} + + /** * Retrieves a value from a JSON document by a JSON pointer. * Returns the value. @@ -219,6 +323,30 @@ export function applyOperation(document: T, operation: Operation, validateOpe } else if (operation.op === '_get') { operation.value = document; return returnValue; + } else if (operation.op === 'x') { + // get extended config + const xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // at empty (root) path default to obj operator + let workingDocument = document; + let obj = document; + if (!mutateDocument) { + obj = workingDocument = _deepClone(document); + } + let result = { newDocument: operation.value }; + // if resolve is true, allow extended operator to run against supplied + // document object/clone + if (operation.resolve === true) { + result = xConfig.obj.call(operation, obj, '', workingDocument); + // in resolve mode, allow operator result of undefined to revert back to + // original document + if (result === undefined) { + return { newDocument: document }; + } + } + return result; } else { /* bad operation */ if (validateOperation) { throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); @@ -238,6 +366,20 @@ export function applyOperation(document: T, operation: Operation, validateOpe let len = keys.length; let existingPathFragment = undefined; let key: string | number; + + let xConfig; + let workingDocument = document; + let graftPath = undefined; + if (operation.op === 'x') { + xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + // make another clone to allow configured operator to 'abort' or 'no op' + // by returning a strict undefined + workingDocument = obj = _deepClone(obj); + } + let validateFunction; if (typeof validateOperation == 'function') { validateFunction = validateOperation; @@ -255,7 +397,7 @@ export function applyOperation(document: T, operation: Operation, validateOpe (key == '__proto__' || (key == 'prototype' && t>0 && keys[t-1] == 'constructor')) ) { - 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'); + throw new TypeError(PROTO_ERROR_MSG); } if (validateOperation) { @@ -273,8 +415,33 @@ export function applyOperation(document: T, operation: Operation, validateOpe } t++; if (Array.isArray(obj)) { + // don't coerce an empty key string into an integer (below w/~~) + // if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key === '') { + 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); + } + if (key === '-') { key = obj.length; + // make some adjustments for grafting with extended ops + if (operation.op === 'x') { + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + } + } + // extended operations can use a special 'end element' sentinel in array paths + else if (operation.op === 'x' && key === '--') { + key = obj.length - 1; + // update global keys array as well + keys[t - 1] = key.toString(10); + // set this point as the graft path + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } } else { if (validateOperation && !isInteger(key)) { @@ -282,9 +449,52 @@ export function applyOperation(document: T, operation: Operation, validateOpe } // only parse key when it's an integer for `arr.prop` to work else if(isInteger(key)) { key = ~~key; + // don't allow arbitrary idx creation if not in resolve mode (extended ops only) + if (operation.op === 'x' && operation.resolve !== true && key >= obj.length) { + 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); + } } } if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (typeof key === 'string' && key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended array patch + const result = xConfig.arr.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + // check for ambiguous results + if (result.removed !== undefined && operation.resolve === true) { + // can't use resolve with a removal; ambiguous removal path + throw new JsonPatchError('Extended operation should not remove items while resolving undefined paths', 'OPERATION_X_AMBIGUOUS_REMOVAL', index, operation, document); + } + + if (mutateDocument) { + // default graft path + let pc: PathComponents = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + _graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } + if (validateOperation && operation.op === "add" && key > obj.length) { 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); } @@ -297,6 +507,40 @@ export function applyOperation(document: T, operation: Operation, validateOpe } else { if (t >= len) { + if (operation.op === 'x' && xConfig) { + // maintain parity with empty root behavior above + if (key.length === 0 && !operation.resolve) { + return { newDocument: operation.value }; + } + // Apply extended obj patch + const result = xConfig.obj.call(operation, obj, key, workingDocument); + if (result === undefined) { + return { newDocument: document }; + } + + if (mutateDocument) { + // default graft path + let pc: PathComponents = { + modType: 'graft', + comps: graftPath === undefined ? keys.slice(1, t) : graftPath.split('/').slice(1), + }; + // figure out graft or prune + if (result.removed !== undefined) { + // it's a prune + pc = { + modType: 'prune', + // use entire path up to, and including, key + comps: keys.slice(1, t), + }; + } + // modifies document in-place + _graftTree(result.newDocument, document, pc); + // make sure to return original document reference + return { newDocument: document }; + } + return result; + } + const returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch if (returnValue.test === false) { throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); @@ -304,6 +548,28 @@ export function applyOperation(document: T, operation: Operation, validateOpe return returnValue; } } + + // extended operation forced path resolution + if (operation.op === 'x' && xConfig && obj[key] === undefined) { + + // get first path where something isn't defined + if (graftPath === undefined) { + graftPath = keys.slice(0, t).join('/'); + } + + if (operation.resolve === true) { + // add resolvable nodes + // check next key to determine object creation strategy + if (Array.isArray(document) && isInteger(String(keys[t]))) { // goofy cast + // if original document is an array, numeric path elements + // should add new arrays with minimum capacity + obj[key] = new Array(~~keys[t] + 1); + } else { + obj[key] = {}; + } + } + } + obj = obj[key]; // If we have more keys in the path, but the next value isn't a non-null object, // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again. @@ -377,6 +643,39 @@ export function validator(operation: Operation, index: number, document?: any, e throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); } + // check for extended ops + if (operation.op === 'x') { + // default validations + if (!isValidExtendedOpId(operation.xid)) { + throw new JsonPatchError('Operation `xid` property is not present or invalid string', 'OPERATION_X_ID_INVALID', index, operation, document); + } + + const xConfig = xOpRegistry.get(operation.xid); + if (!xConfig) { + throw new JsonPatchError('Extended operation `xid` property is not a registered extended operation', 'OPERATION_X_OP_INVALID', index, operation, document); + } + + if (typeof operation.path !== 'string') { + throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); + } + + if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { + // paths that aren't empty string should start with "/" + throw new JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); + } + + if (operation.path === '/') { + throw new JsonPatchError('Operation `path` slash-only is ambiguous', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); + } + + if (operation.args !== undefined && !Array.isArray(operation.args)) { + throw new JsonPatchError('Operation `args` property is not an array', 'OPERATION_X_ARGS_NOT_ARRAY', index, operation, document); + } + + // we made it this far, now run the operation's configured validator + xConfig.validator.call(operation, operation, index, document, existingPathFragment); + } + else if (!objOps[operation.op]) { throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); } diff --git a/src/helpers.ts b/src/helpers.ts index fad6242..b1d1361 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -135,6 +135,12 @@ export function hasUndefined(obj: any): boolean { export type JsonPatchErrorName = 'SEQUENCE_NOT_AN_ARRAY' | 'OPERATION_NOT_AN_OBJECT' | 'OPERATION_OP_INVALID' | + 'OPERATION_X_ARGS_NOT_ARRAY' | + 'OPERATION_X_OP_INVALID' | + 'OPERATION_X_CONFIG_INVALID' | + 'OPERATION_X_ID_INVALID' | + 'OPERATION_X_AMBIGUOUS_REMOVAL' | + 'OPERATION_X_OPERATOR_EXCEPTION' | 'OPERATION_PATH_INVALID' | 'OPERATION_FROM_REQUIRED' | 'OPERATION_VALUE_REQUIRED' | @@ -162,4 +168,93 @@ export class PatchError extends Error { Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 this.message = patchErrorMessageFormatter(message, { name, index, operation, tree }); } -} \ No newline at end of file +} + +// exported for use in jasmine test +export const PROTO_ERROR_MSG = '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'; + +export function isValidExtendedOpId(xid: string): boolean { + return typeof xid === 'string' && xid.length >= 3 && xid.indexOf('x-') === 0; +} + +type KeyType = string | number; + +interface PruneComponents { + modType: 'prune'; + comps: Array; +}; + +interface GraftComponents { + modType: 'graft'; + comps: Array; +}; + +export type PathComponents = PruneComponents | GraftComponents; + +/** + * attach/remove source tree to/from target tree at appropriate path. + * modifies targetObj (in place) by reference. + * + * This is necessary to deal with JS "by value" semantics + */ +export function _graftTree(sourceObj: any, targetObj: any, pathComponents: PathComponents): void { + if (pathComponents.comps.length === 0) { + // no changes + return; + } + + // traverse document trees until at the appropriate parent level + let graftTgt = targetObj; + let graft = sourceObj; + let graftKey: KeyType = ''; + + // single component is top-level key + if (pathComponents.comps.length === 1) { + graftKey = pathComponents.comps[0]; + if (pathComponents.modType === 'graft') { + graftTgt[graftKey] = graft[graftKey]; + } else { + // top-level prune is a "best guess" that the provided key was removed from + // the top-level object (we have no visibility into what the extended + // operator has actually done since it comes from "user-space"; + // external to the extension api) + // NOTE: pruning is here only to allow the extension api to + // emulate the RFC api; user-defined operations may perform complex + // removals, but they may not be visible during the prune process. + // It is recommended that the user stick to the RFC 'remove' operation + // and not implemennt their own removal operations in extended-space; + // or at least limit removal to the provided path, and not perform other mutations + // combined with removals + if (Array.isArray(targetObj)) { + graftTgt.splice(~~graftKey, 1); + } else { + delete graftTgt[graftKey]; + } + } + return; + } + + for (let i = 0; i < pathComponents.comps.length; i++) { + graftKey = pathComponents.comps[i]; + graft = graft[graftKey]; + // if there is no value in the target obj at the current key, + // than this is a graft point + if (pathComponents.modType === 'graft' && graftTgt[graftKey] === undefined) { + // if both target and source are undefined - No Op + if (graft === undefined) { + return; + } + break; + } + // there was a removal; the graft point needs to be the 2nd to last path comp + // a.k.a. the parent obj of the pruned key + // in order to preserve additional structure that was not pruned + if (i === pathComponents.comps.length - 2) { + break; + } + graftTgt = graftTgt[graftKey]; + } + + // graft + graftTgt[graftKey] = graft; +} diff --git a/test/spec/coreSpec.mjs b/test/spec/coreSpec.mjs index b423e07..d7d4639 100644 --- a/test/spec/coreSpec.mjs +++ b/test/spec/coreSpec.mjs @@ -1929,9 +1929,6 @@ describe('undefined - JS to JSON projection / JSON to JS extension', function() }); it(`should not allow __proto__ modifications without unsetting the banPrototypeModifications flag and should throw an error`, function() { - const expectedErrorMessage = - 'JSON-Patch: modifying `__proto__` 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'; - function SomeClass() { this.foo = 'bar'; } @@ -1943,13 +1940,13 @@ describe('undefined - JS to JSON projection / JSON to JS extension', function() { op: 'replace', path: `/__proto__/x`, value: 'polluted' } ]; - expect(() => jsonpatch.applyPatch(doc, patch)).toThrow(new TypeError(expectedErrorMessage)); + expect(() => jsonpatch.applyPatch(doc, patch)).toThrowError(TypeError, jsonpatch.PROTO_ERROR_MSG); expect(otherDoc.x).toEqual(undefined); expect(doc.x).toEqual(undefined); let arr = []; - expect(() => jsonpatch.applyPatch(arr, patch)).toThrow(new TypeError(expectedErrorMessage)); + expect(() => jsonpatch.applyPatch(arr, patch)).toThrowError(TypeError, jsonpatch.PROTO_ERROR_MSG); expect(arr.x).toEqual(undefined); }); }); diff --git a/test/spec/extendedSpec.mjs b/test/spec/extendedSpec.mjs new file mode 100644 index 0000000..9aeea80 --- /dev/null +++ b/test/spec/extendedSpec.mjs @@ -0,0 +1,2636 @@ +import * as jsonpatch from "../../index.mjs"; + +const unwrapPatchError = (fn) => () => { + try { + fn(); + } catch (X) { + if (typeof X.name === "string") { + throw new Error(X.name); + } + throw X; + } +}; + +// API extensions +describe("additional API functions for non-RFC-6902 operations exposed", function () { + it("has additional functions", function () { + expect(jsonpatch.useExtendedOperation) + .withContext("useExtendedOperation should be a method within the object") + .toBeDefined(); + expect(jsonpatch.hasExtendedOperation) + .withContext("hasExtendedOperation should be a method within the object") + .toBeDefined(); + expect(jsonpatch.unregisterAllExtendedOperations) + .withContext( + "unregisterAllExtendedOperations should be a method within the object" + ) + .toBeDefined(); + }); +}); + +// Register extended operations +describe("jsonpatch.useExtendedOperation", function () { + it("should reject improperly formatted extended operation name", function () { + expect( + unwrapPatchError(() => { + jsonpatch.useExtendedOperation("", {}); + }) + ).toThrowError(Error, "OPERATION_X_ID_INVALID"); + }); + + it("should reject improperly formatted extended operation name", function () { + expect( + unwrapPatchError(() => { + jsonpatch.useExtendedOperation("add", {}); + }) + ).toThrowError(Error, "OPERATION_X_ID_INVALID"); + }); + + it("should reject when missing `arr` function", function () { + expect( + unwrapPatchError(() => { + jsonpatch.useExtendedOperation("x-foo", { + obj: () => {}, + validator: () => {}, + }); + }) + ).toThrowError(Error, "OPERATION_X_CONFIG_INVALID"); + }); + + it("should reject when missing `obj` function", function () { + expect( + unwrapPatchError(() => { + jsonpatch.useExtendedOperation("x-foo", { + arr: () => {}, + validator: () => {}, + }); + }) + ).toThrowError(Error, "OPERATION_X_CONFIG_INVALID"); + }); + + it("should reject when missing `validator` function", function () { + expect( + unwrapPatchError(() => { + jsonpatch.useExtendedOperation("x-foo", { + arr: () => {}, + obj: () => {}, + }); + }) + ).toThrowError(Error, "OPERATION_X_CONFIG_INVALID"); + }); +}); + +// Default validation +describe("default extended operation validation", function () { + beforeEach(function () { + jsonpatch.unregisterAllExtendedOperations(); + }); + + const dummyConfig = { + arr: () => {}, + obj: () => {}, + validator: () => {}, + }; + + it("rejects extended operation w/out `xid` property", function () { + const xop = { op: "x", path: "", value: 5 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_X_ID_INVALID"); + }); + + it("rejects unregistered extended operation", function () { + const xop = { op: "x", xid: "x-foo", path: "", value: 5 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_X_OP_INVALID"); + }); + + it("rejects extended operation w/out path string", function () { + jsonpatch.useExtendedOperation("x-foo", dummyConfig); + const xop = { op: "x", xid: "x-foo", value: 5 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_PATH_INVALID"); + }); + + it("rejects extended operation w/bad path string", function () { + jsonpatch.useExtendedOperation("x-foo", dummyConfig); + const xop = { op: "x", xid: "x-foo", path: "not good", value: 5 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_PATH_INVALID"); + }); + + it("rejects extended operation w/slash path string", function () { + jsonpatch.useExtendedOperation("x-foo", dummyConfig); + const xop = { op: "x", xid: "x-foo", path: "/", value: 5 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + }); + + it("rejects extended operation w/non-array `args` property", function () { + jsonpatch.useExtendedOperation("x-foo", dummyConfig); + const xop = { op: "x", xid: "x-foo", path: "", value: 5, args: 99 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop, true); + }) + ).toThrowError(Error, "OPERATION_X_ARGS_NOT_ARRAY"); + }); + + it("runs extended operation validator", function () { + const cfg = Object.assign({}, dummyConfig, { + validator: function (o, i, d, ep) { + // custom validator rejects specific va;ue + if (this.value === 5) { + throw new jsonpatch.JsonPatchError( + "custom validator", + "OPERATION_VALUE_OUT_OF_BOUNDS" + ); + } + }, + }); + jsonpatch.useExtendedOperation("x-foo", cfg); + const xop_invalid = { op: "x", xid: "x-foo", path: "", value: 5 }; + const xop_valid = { op: "x", xid: "x-foo", path: "", value: 8 }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation({}, xop_invalid, true); + }) + ).toThrowError(Error, "OPERATION_VALUE_OUT_OF_BOUNDS"); + // other values should pass + const result = jsonpatch.applyOperation({}, xop_valid, true); + expect(result.newDocument).toBe(8); + }); +}); + +// Object-based semantics +describe("extended operations - object-based semantics", function () { + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + const xCfg = { + arr: () => { + throw new Error("`arr` operator should NOT be invoked by this spec"); + }, + // This object operator sets key to value UNLESS value === 5, then it forces value of 999. + // If value === 77, operator should 'No Op' + obj: function (o, k, d) { + if (this.value === 5) { + o[k] = 999; + } else { + o[k] = this.value; + } + // do this check AFTER the assignment to ensure + // original object is not mutated when returning undefined + if (this.value === 77) { + return undefined; + } + return { newDocument: d }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-obj", xCfg); + }); + + it("should run configured obj operator", function () { + const base = {}; + const base_no_mutate = {}; + const xop = { op: "x", xid: "x-obj", path: "/a", value: 42 }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.a).toBe(42); + expect(base_no_mutate.a).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.a).toBe(42); + expect(base.a).toBe(42); + }); + + it("should run configured obj operator - value override", function () { + const base = {}; + const base_no_mutate = {}; + const xop = { op: "x", xid: "x-obj", path: "/a", value: 5 }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.a).toBe(999); + expect(base_no_mutate.a).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.a).toBe(999); + expect(base.a).toBe(999); + }); + + it("should run configured obj operator - value override; No Op", function () { + const base = {}; + const base_no_mutate = {}; + const xop = { op: "x", xid: "x-obj", path: "/a", value: 77 }; + let result = undefined; + + // w/out document modifications (4th arg false) + result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.a).toBe(undefined); + expect(base_no_mutate.a).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.a).toBe(undefined); + expect(base.a).toBe(undefined); + + ////// sanity check update leaf + const b = { a: { b: { c: "foo", d: "bar" } } }; + const o = { op: "x", xid: "x-obj", path: "/a/b/d", value: "hey!" }; + result = jsonpatch.applyOperation(b, o, true, false); + expect(result.newDocument.a.b.d).toBe("hey!"); + expect(b.a.b.d).toBe("bar"); + + // with base mod + result = jsonpatch.applyOperation(b, o, true, true); + expect(result.newDocument.a.b.d).toBe("hey!"); + expect(b.a.b.d).toBe("hey!"); + }); + + it("should NOT run configured obj operator @root path when resolve !== true - set root doc to value", function () { + const base_no_mutate = { a: "hi" }; + const xop = { op: "x", xid: "x-obj", path: "", value: 77 }; + + const result = jsonpatch.applyOperation(base_no_mutate, xop, true); + expect(result.newDocument).toBe(77); + // NOTE: 4th arg to 'applyOperation' has no effect at root path + // - (JS function argument pass-by-value semantics) + expect(base_no_mutate.a).toBe("hi"); + }); + + it("should run configured obj operator @root path when resolve === true - does NOT replace existing root doc", function () { + const base = { a: "hi" }; + const base_no_mutate = { a: "hi" }; + const xop = { + op: "x", + xid: "x-obj", + path: "", + value: "special value", + resolve: true, + }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + // an empty string key was created on orig doc + expect(result.newDocument[""]).toBe("special value"); + // previously existing keys still exist in result + expect(result.newDocument.a).toBe("hi"); + // base document was not mutated + expect(base_no_mutate.a).toBe("hi"); + expect(base_no_mutate[""]).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + // an empty string key was created on orig doc + expect(result.newDocument[""]).toBe("special value"); + // previously existing keys still exist in result + expect(result.newDocument.a).toBe("hi"); + // base document was mutated as well + expect(base[""]).toBe("special value"); + // previously existing keys still exist in base + expect(base.a).toBe("hi"); + }); + + it("should reject unresolvable path when operation.resolve !== true", function () { + const base = {}; + const base_no_mutate = { a: "hi" }; + let result = {}; + const xop = { op: "x", xid: "x-obj", path: "/a/b/c", value: 5 }; + + // w/out document modifications (4th arg false) + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(result.newDocument).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate.a).toBe("hi"); + + // w/document modifications (4th arg true) + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation(base, xop, true, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(result.newDocument).toBe(undefined); + // no changes should have been made to base + expect(base.a).toBe(undefined); + }); + + it("should NOT reject unresolvable path when operation.resolve === true - create missing hierarchy", function () { + const base = { + a: { + d: "hi", + }, + }; + const base_no_mutate = { + a: { + d: "hello", + }, + }; + const xop = { + op: "x", + xid: "x-obj", + path: "/a/b/c", + value: 5, + resolve: true, + }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.a.b.c).toBe(999); + // previously existing keys should stil exist + expect(result.newDocument.a.d).toBe("hello"); + // no changes should have been made to base + expect(base_no_mutate.a.b).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.a.b.c).toBe(999); + // previously existing keys should stil exist + expect(result.newDocument.a.d).toBe("hi"); + // base should be modified + expect(base.a.b).toBeDefined(); + expect(base.a.b.c).toBe(999); + // previously existing base keys should stil exist + expect(base.a.d).toBe("hi"); + }); + + it("should treat numeric keys as string props when root document is an Object && operation.resolve === true - create missing hierarchy", function () { + const base = { + a: { + d: "hi", + }, + }; + const base_no_mutate = { + a: { + d: "hello", + }, + }; + const xop = { + op: "x", + xid: "x-obj", + path: "/a/0/c", + value: 5, + resolve: true, + }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.a["0"].c).toBe(999); + // previously existing keys should stil exist + expect(result.newDocument.a.d).toBe("hello"); + // no changes should have been made to base + expect(base_no_mutate.a["0"]).toBe(undefined); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.a["0"].c).toBe(999); + // previously existing keys should stil exist + expect(result.newDocument.a.d).toBe("hi"); + // base should be modified + expect(base.a["0"]).toBeDefined(); + expect(base.a["0"].c).toBe(999); + // previously existing base keys should stil exist + expect(base.a.d).toBe("hi"); + }); + + it("should pass args from extended operations to their configured operators", function () { + // special config that passes operation args through to newDocument + const cfg = { + arr: function (a, i, d) { + return { newDocument: this.args }; + }, + obj: function (o, k, d) { + return { newDocument: this.args }; + }, + validator: () => {}, + }; + jsonpatch.useExtendedOperation("x-args", cfg); + const base_arr = [1, 2, 3]; + const base_obj = { a: 5 }; + // use resolve === true to force invocation of obj operator + const xop_arr = { + op: "x", + xid: "x-args", + path: "", + value: null, + args: ["dinosaur", "hello", 54], + resolve: true, + }; + // NOTE: empty path will always run the obj operator + const xop_obj = { + op: "x", + xid: "x-args", + path: "", + value: null, + args: ["hi", 42], + resolve: true, + }; + + // obj + let result = jsonpatch.applyOperation(base_obj, xop_obj, true); + let v = result.newDocument; + expect(Array.isArray(v) && v[0] === "hi" && v[1] === 42).toBe(true); + + // arr + result = jsonpatch.applyOperation(base_arr, xop_arr, true); + v = result.newDocument; + expect( + Array.isArray(v) && v[0] === "dinosaur" && v[1] === "hello" && v[2] === 54 + ).toBe(true); + }); +}); + +// Array-based semantics +describe("extended operations - array-based semantics", function () { + const EMSG = "`obj` operator should NOT be invoked by this spec"; + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + const xCfg = { + // This array operator sets index to value UNLESS value === 5, then it forces value of 999. + // If value === 77, operator should 'No Op' + arr: function (a, i, d) { + if (this.value === 5) { + a[i] = 999; + } else { + a[i] = this.value; + } + // do this check AFTER the assignment to ensure + // original object is not mutated when returning undefined + if (this.value === 77) { + return undefined; + } + return { newDocument: d }; + }, + obj: () => { + throw new Error(EMSG); + }, + validator: () => {}, + }; + + const xCfg2 = { + arr: function (a, i, d) { + a[i] = this.value; + return { newDocument: d }; + }, + obj: function (o, k, d) { + o[k] = this.value; + return { newDocument: d }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-arr", xCfg); + jsonpatch.useExtendedOperation("x-arr2", xCfg2); + }); + + it("should run configured arr operator", function () { + const base = ["hi"]; + const base_no_mutate = ["hello"]; + const xop = { op: "x", xid: "x-arr", path: "/0", value: 42 }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument[0]).toBe(42); + expect(base_no_mutate[0]).toBe("hello"); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument[0]).toBe(42); + expect(base[0]).toBe(42); + }); + + it("should run configured arr operator - value override", function () { + const base = ["hi", "there"]; + const base_no_mutate = ["hello", "you"]; + const xop = { op: "x", xid: "x-arr", path: "/1", value: 5 }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toBe(999); + // existing indexes unchanged + expect(result.newDocument[0]).toBe("hello"); + // no base modifications + expect(base_no_mutate.length).toBe(2); + expect(base_no_mutate[0]).toBe("hello"); + expect(base_no_mutate[1]).toBe("you"); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toBe(999); + // existing indexes unchanged + expect(result.newDocument[0]).toBe("hi"); + // base modifications + expect(base.length).toBe(2); + expect(base[0]).toBe("hi"); + expect(base[1]).toBe(999); + }); + + it("should run configured arr operator - value override; No Op", function () { + const base = ["hi", "there"]; + const base_no_mutate = ["hello", "you"]; + const xop = { op: "x", xid: "x-arr", path: "/1", value: 77 }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + // ensure no op + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[0]).toBe("hello"); + expect(result.newDocument[1]).toBe("you"); + // no base modifications + expect(base_no_mutate.length).toBe(2); + expect(base_no_mutate[0]).toBe("hello"); + expect(base_no_mutate[1]).toBe("you"); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + // ensure no op + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[0]).toBe("hi"); + expect(result.newDocument[1]).toBe("there"); + // no base modifications + expect(base.length).toBe(2); + expect(base[0]).toBe("hi"); + expect(base[1]).toBe("there"); + }); + + it("should NOT run EITHER `arr` or `obj` configured operators @ empty root path when `resolve` === false; return raw value", function () { + const base_no_mutate = ["hi"]; + const xop = { op: "x", xid: "x-arr", path: "", value: 77 }; + const result = jsonpatch.applyOperation(base_no_mutate, xop, true); + expect(result.newDocument).toBe(77); + // no base modifications + expect(Array.isArray(base_no_mutate)).toBe(true); + expect(base_no_mutate.length).toBe(1); + expect(base_no_mutate[0]).toBe("hi"); + }); + + it("should NOT run configured `arr` operator @empty root path when resolve === true - run `obj` operator", function () { + const base_no_mutate = ["hi"]; + const xop = { + op: "x", + xid: "x-arr", + path: "", + value: "special value", + resolve: true, + }; + expect(() => { + jsonpatch.applyOperation(base_no_mutate, xop, true); + }).toThrowError(Error, EMSG); + // no base modifications + expect(Array.isArray(base_no_mutate)).toBe(true); + expect(base_no_mutate.length).toBe(1); + expect(base_no_mutate[0]).toBe("hi"); + }); + + it("should reject unresolvable path when operation.resolve !== true", function () { + const base = ["hi"]; + const base_no_mutate = ["hi"]; + let result = undefined; + const xop_prop_path = { op: "x", xid: "x-arr", path: "/a", value: 2 }; + const xop_root_slash_path = { op: "x", xid: "x-arr", path: "/", value: 4 }; + const xop_idx_path = { op: "x", xid: "x-arr", path: "/4", value: 6 }; + const xop_nested_idx_path = { + op: "x", + xid: "x-arr", + path: "/a/1", + value: 8, + }; + const xop_deep_nested_idx_path = { + op: "x", + xid: "x-arr", + path: "/a/1/b/c/0", + value: 10, + }; + + /************** w/out document modifications (4th arg false) **************/ + + // prop path + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_prop_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // root slash + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_root_slash_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // non-existent idx + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_idx_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_VALUE_OUT_OF_BOUNDS"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // nested + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_nested_idx_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // deep nested + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_deep_nested_idx_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + /***************** w/document modifications (4th arg true) ****************/ + + // prop path + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_prop_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // root slash + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_root_slash_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // non-existent idx + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_idx_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_VALUE_OUT_OF_BOUNDS"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // nested + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_nested_idx_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // deep nested + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_deep_nested_idx_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + }); + + it('should still reject "Array Object Prop" path when operation.resolve === true', function () { + const base = ["hi"]; + const base_no_mutate = ["hi"]; + let result = undefined; + const xop_prop_path = { + op: "x", + xid: "x-arr", + path: "/a", + value: 2, + resolve: true, + }; + const xop_root_slash_path = { + op: "x", + xid: "x-arr", + path: "/", + value: 4, + resolve: true, + }; + const xop_idx_path = { + op: "x", + xid: "x-arr", + path: "/4", + value: 6, + resolve: true, + }; + const xop_nested_idx_path = { + op: "x", + xid: "x-arr", + path: "/a/1", + value: 8, + resolve: true, + }; + const xop_deep_nested_idx_path = { + op: "x", + xid: "x-arr", + path: "/a/1/b/c/0", + value: 10, + resolve: true, + }; + + /************** w/out document modifications (4th arg false) **************/ + + // prop path - still illegal when resolve === true + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_prop_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // root slash - still illegal when resolve === true + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_root_slash_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // non-existent idx - added when resolve === true + result = jsonpatch.applyOperation( + base_no_mutate, + xop_idx_path, + true, + false + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(result.newDocument[4]).toBe(6); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // nested - first path component is non-numeric - should still reject when resolve === true + result = undefined; + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_nested_idx_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // deep nested + result = undefined; + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_deep_nested_idx_path, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + /***************** w/document modifications (4th arg true) ****************/ + + // prop path - still illegal when resolve === true + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_prop_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // root slash - still illegal when resolve === true + let b = base.concat(); + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation(b, xop_root_slash_path, true, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + // no changes should have been made to base + expect(b[0]).toBe("hi"); + + // non-existent idx - added when resolve === true + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_idx_path, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(result.newDocument[4]).toBe(6); + // appropriate changes should have been made to base + expect(b.length).toBe(5); + expect(b[4]).toBe(6); + // untouched + expect(b[0]).toBe("hi"); + + // nested - first path component is non-numeric - should still reject when resolve === true + result = undefined; + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_nested_idx_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + + // deep nested + result = undefined; + expect( + unwrapPatchError(() => { + result = jsonpatch.applyOperation( + base_no_mutate, + xop_deep_nested_idx_path, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_ILLEGAL_ARRAY_INDEX"); + expect(result).toBe(undefined); + // no changes should have been made to base + expect(base_no_mutate[0]).toBe("hi"); + }); + + it("should create missing arr/object hierarchy when operation.resolve === true", function () { + const base = ["hi"]; + let result = undefined; + const xop_idx_path = { + op: "x", + xid: "x-arr2", + path: "/4", + value: 42, + resolve: true, + }; + const xop_idx_path_2 = { + op: "x", + xid: "x-arr2", + path: "/4/2", + value: "double", + resolve: true, + }; + const xop_idx_path_3 = { + op: "x", + xid: "x-arr2", + path: "/4/2/1", + value: "triple", + resolve: true, + }; + const xop_idx_append = { + op: "x", + xid: "x-arr2", + path: "/-", + value: "appended", + resolve: true, + }; + const xop_nested_prop_path = { + op: "x", + xid: "x-arr2", + path: "/1/a", + value: "hello", + resolve: true, + }; + const xop_nested_prop_path_2 = { + op: "x", + xid: "x-arr2", + path: "/1/a/b", + value: "dog", + resolve: true, + }; + const xop_deep_nested_idx_path = { + op: "x", + xid: "x-arr2", + path: "/1/1/b/c/0", + value: { p: "nested" }, + resolve: true, + }; + + // /************** w/out document modifications (4th arg false) **************/ + + // create non-existent index - 1 level of nesting + result = jsonpatch.applyOperation(base, xop_idx_path, true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(result.newDocument[4]).toBe(42); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // create non-existent index - 2 level of nesting + result = jsonpatch.applyOperation(base, xop_idx_path_2, true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(Array.isArray(result.newDocument[4])).toBe(true); + expect(result.newDocument[4][2]).toBe("double"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // create non-existent index - 3 level of nesting + result = jsonpatch.applyOperation(base, xop_idx_path_3, true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(Array.isArray(result.newDocument[4])).toBe(true); + expect(Array.isArray(result.newDocument[4][2])).toBe(true); + expect(result.newDocument[4][2][1]).toBe("triple"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // create non-existent index - append element sentinel + result = jsonpatch.applyOperation(base, xop_idx_append, true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toBe("appended"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[1]).toBe(undefined); + + // create obj at non existent index + result = jsonpatch.applyOperation(base, xop_nested_prop_path, true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toEqual({ a: "hello" }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // create nested obj at non existent index + result = jsonpatch.applyOperation( + base, + xop_nested_prop_path_2, + true, + false + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toEqual({ a: { b: "dog" } }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // create deep nested array & obj at non existent index /1/1/b/c/0 + result = jsonpatch.applyOperation( + base, + xop_deep_nested_idx_path, + true, + false + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(Array.isArray(result.newDocument[1])).toEqual(true); + expect(result.newDocument[1][1]).toEqual({ b: { c: [{ p: "nested" }] } }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // no base mods + expect(base[0]).toBe("hi"); + expect(base[4]).toBe(undefined); + + // reject bogus path + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation( + base, + { + op: "x", + xid: "x-arr2", + path: "/--/b/--", + value: "wut?", + resolve: true, + }, + true, + false + ); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + + /***************** w/document modifications (4th arg true) ****************/ + + let b = base.concat(); + + // create non-existent index - 1 level of nesting + result = jsonpatch.applyOperation(b, xop_idx_path, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(result.newDocument[4]).toBe(42); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(b[4]).toBe(42); + + // create non-existent index - 2 level of nesting + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_idx_path_2, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(Array.isArray(result.newDocument[4])).toBe(true); + expect(result.newDocument[4][2]).toBe("double"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(Array.isArray(b[4])).toBe(true); + expect(b[4][2]).toBe("double"); + + // create non-existent index - 3 level of nesting + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_idx_path_3, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(5); + expect(Array.isArray(result.newDocument[4])).toBe(true); + expect(Array.isArray(result.newDocument[4][2])).toBe(true); + expect(result.newDocument[4][2][1]).toBe("triple"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(Array.isArray(b[4])).toBe(true); + expect(Array.isArray(b[4][2])).toBe(true); + expect(b[4][2][1]).toBe("triple"); + + // create non-existent index - append element sentinel + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_idx_append, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toBe("appended"); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(b[1]).toBe("appended"); + + // create obj at non existent index + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_nested_prop_path, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toEqual({ a: "hello" }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(b[1]).toEqual({ a: "hello" }); + + // create nested obj at non existent index + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_nested_prop_path_2, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(result.newDocument[1]).toEqual({ a: { b: "dog" } }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(b[1]).toEqual({ a: { b: "dog" } }); + + // create deep nested array & obj at non existent index /1/1/b/c/0 + b = base.concat(); + result = jsonpatch.applyOperation(b, xop_deep_nested_idx_path, true, true); + expect(result).toBeDefined(); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(2); + expect(Array.isArray(result.newDocument[1])).toEqual(true); + expect(result.newDocument[1][1]).toEqual({ b: { c: [{ p: "nested" }] } }); + // existing element still there + expect(result.newDocument[0]).toBe("hi"); + // base mods + expect(b[0]).toBe("hi"); + expect(Array.isArray(b[1])).toBe(true); + expect(Array.isArray(b[1][1].b.c)).toBe(true); + expect(b[1][1]).toEqual({ b: { c: [{ p: "nested" }] } }); + + // reject bogus path + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation( + base, + { + op: "x", + xid: "x-arr2", + path: "/--/b/--", + value: "wut?", + resolve: true, + }, + true, + true + ); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + }); +}); + +describe("extended operations support (existing) property removal", function () { + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + const xCfg = { + arr: function (a, i, d) { + const rem = a.splice(i, 1); + return { newDocument: d, removed: rem[0] }; + }, + obj: function (o, k, d) { + const removed = o[k]; + delete o[k]; + return { newDocument: d, removed }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-rm", xCfg); + }); + + it("should allow removal from objects", function () { + const base = { + a: 42, + b: "hi", + c: { + d: 99, + e: { + f: "deep prop", + }, + }, + }; + const base_no_mutate = JSON.parse(JSON.stringify(base)); + const xop = { op: "x", xid: "x-rm", path: "/c/e" }; + + // w/out document modifications (4th arg false) + let result = jsonpatch.applyOperation(base_no_mutate, xop, true, false); + expect(result.newDocument.c.e).toBe(undefined); + // previously existing keys should stil exist + expect(result.newDocument.c.d).toBe(99); + expect(result.newDocument.a).toBe(42); + expect(result.newDocument.b).toBe("hi"); + // no changes should have been made to base + expect(base_no_mutate.c.e).toBeDefined(); + expect(base_no_mutate.c.e.f).toBe("deep prop"); + // previously existing keys should stil exist + expect(base_no_mutate.c.d).toBe(99); + expect(base_no_mutate.a).toBe(42); + expect(base_no_mutate.b).toBe("hi"); + + // w/document modifications (4th arg true) + result = jsonpatch.applyOperation(base, xop, true, true); + expect(result.newDocument.c.e).toBe(undefined); + // previously existing keys should stil exist + expect(result.newDocument.c.d).toBe(99); + expect(result.newDocument.a).toBe(42); + expect(result.newDocument.b).toBe("hi"); + // changes should have been made to base + expect(base.c.e).toBe(undefined); + // previously existing keys should stil exist + expect(base.c.d).toBe(99); + expect(base.a).toBe(42); + expect(base.b).toBe("hi"); + }); + + it("should avoid removal at unresolvable path - obj; undef leaf", function () { + const base = { + a: 42, + b: "hi", + c: { + d: 99, + e: { + f: "deep prop", + }, + }, + }; + // only prop 'g' is undefined + const b = JSON.parse(JSON.stringify(base)); + const xop = { op: "x", xid: "x-rm", path: "/c/e/g" }; + const r = jsonpatch.applyOperation(b, xop, true, true); + // no changes to base + expect(r.newDocument).toEqual(base); + }); + + it("should reject removal at unresolvable path - obj; undef parent(s)", function () { + const base = { + a: 42, + b: "hi", + c: { + d: 99, + e: { + f: "deep prop", + }, + }, + }; + // all props below 'g' are undefined + const xop = { op: "x", xid: "x-rm", path: "/c/e/g/h/i" }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation(base, xop, true); + }) + ).toThrowError("OPERATION_PATH_UNRESOLVABLE"); + }); + + it("should allow removal from arrays", function () { + const base_arr = [0, 1, 2, 3, 4]; + const base_arr_2 = [0, 1, [2, 3], 4]; + const base_obj = { + a: { + b: [9, 8, 7], + }, + c: "hi", + d: [5, [6, 7], 8], + }; + let b = undefined; + let result = undefined; + const xop_arr = { op: "x", xid: "x-rm", path: "/2" }; + const xop_arr_nested = { op: "x", xid: "x-rm", path: "/2/1" }; + const xop_obj_arr = { op: "x", xid: "x-rm", path: "/a/b/2" }; + const xop_obj_arr_nested = { op: "x", xid: "x-rm", path: "/d/1/0" }; + + /************** w/out document modifications (4th arg false) **************/ + + // simple index + b = base_arr.concat(); + result = jsonpatch.applyOperation(b, xop_arr, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(4); + expect(result.newDocument).toEqual([0, 1, 3, 4]); + // no changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b.length).toBe(5); + expect(b).toEqual(base_arr); + + // nested index + b = base_arr_2.concat(); + result = jsonpatch.applyOperation(b, xop_arr_nested, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(4); + expect(Array.isArray(result.newDocument[2])).toBe(true); + expect(result.newDocument[2].length).toBe(1); + expect(result.newDocument[2]).toEqual([2]); + // no changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b.length).toBe(4); + expect(b).toEqual(base_arr_2); + + // remove double nested array + b = base_arr_2.concat(); + result = jsonpatch.applyOperation(b, xop_arr, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(3); + expect(result.newDocument).toEqual([0, 1, 4]); + // no changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b.length).toBe(4); + expect(b).toEqual(base_arr_2); + + // remove array element nested in object + b = JSON.parse(JSON.stringify(base_obj)); + result = jsonpatch.applyOperation(b, xop_obj_arr, true, false); + expect(Array.isArray(result.newDocument.a.b)).toBe(true); + expect(result.newDocument.a.b.length).toBe(2); + expect(result.newDocument.a.b).toEqual([9, 8]); + // no changes should have been made to base + expect(b).toEqual(base_obj); + + // remove double nested array element nested in an object + b = JSON.parse(JSON.stringify(base_obj)); + result = jsonpatch.applyOperation(b, xop_obj_arr_nested, true, false); + expect(Array.isArray(result.newDocument.d)).toBe(true); + expect(result.newDocument.d.length).toBe(3); + expect(result.newDocument.d[1]).toEqual([7]); + // no changes should have been made to base + expect(b).toEqual(base_obj); + + /***************** w/document modifications (4th arg true) ****************/ + + // simple index + b = base_arr.concat(); + result = jsonpatch.applyOperation(b, xop_arr, true, true); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(4); + expect(result.newDocument).toEqual([0, 1, 3, 4]); + // changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b.length).toBe(4); + expect(b).toEqual([0, 1, 3, 4]); + + // nested index + b = base_arr_2.concat(); + result = jsonpatch.applyOperation(b, xop_arr_nested, true, true); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(4); + expect(Array.isArray(result.newDocument[2])).toBe(true); + expect(result.newDocument[2].length).toBe(1); + expect(result.newDocument[2]).toEqual([2]); + // changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b[2].length).toBe(1); + expect(b[2]).toEqual([2]); + + // remove double nested array + b = base_arr_2.concat(); + result = jsonpatch.applyOperation(b, xop_arr, true, true); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument.length).toBe(3); + expect(result.newDocument).toEqual([0, 1, 4]); + // changes should have been made to base + expect(Array.isArray(b)).toBe(true); + expect(b.length).toBe(3); + expect(b).toEqual([0, 1, 4]); + + // remove array element nested in object + b = JSON.parse(JSON.stringify(base_obj)); + result = jsonpatch.applyOperation(b, xop_obj_arr, true, true); + expect(Array.isArray(result.newDocument.a.b)).toBe(true); + expect(result.newDocument.a.b.length).toBe(2); + expect(result.newDocument.a.b).toEqual([9, 8]); + // no changes should have been made to base + expect(b.a.b.length).toBe(2); + expect(b.a.b).toEqual([9, 8]); + + // remove double nested array element nested in an object + b = JSON.parse(JSON.stringify(base_obj)); + result = jsonpatch.applyOperation(b, xop_obj_arr_nested, true, true); + expect(Array.isArray(result.newDocument.d)).toBe(true); + expect(result.newDocument.d.length).toBe(3); + expect(result.newDocument.d[1]).toEqual([7]); + // changes should have been made to base + expect(b.d[1]).toEqual([7]); + }); + + it("should reject removal at unresolvable path - arr", function () { + const base = { + a: 42, + b: "hi", + c: { + d: [99, 100, "abc"], + e: { + f: "deep prop", + }, + }, + }; + // + const xop = { op: "x", xid: "x-rm", path: "/c/d/4" }; + expect( + unwrapPatchError(() => { + jsonpatch.applyOperation(base, xop, true, true); + }) + ).toThrowError("OPERATION_VALUE_OUT_OF_BOUNDS"); + // no modifications to base + expect(base).toEqual(base); + }); +}); + +describe("extended operations support end element sentinels `--`", function () { + let base_arr; + let base_obj; + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + base_arr = [ + "hi", + { + // "/--" + a: "A", + b: [ + 0, + 1, + 2, + { + // "/--/b/--" + c: [ + 9, + 8, + 7, + [ + 6, + 5, + 4, // "/--/b/--/c/3/--" + ], + 3, + 2, + 1, // "/--/b/--/c/--" + ], + }, + ], + }, + ]; + + base_obj = { + a: "hi", + b: [ + [ + // "/b/0" + 1, + 2, + 3, // "/b/0/--" + ], + { + // "/b/--" + c: [ + 1, + [ + // "/b/--/c/--" + { d: "deep" }, + [ + 1, + 2, + 3, // "/b/--/c/--/--/--" + ], + ], + ], + }, + ], + }; + + const xCfg = { + arr: function (a, i, d) { + const rem = a.splice(i, 1); + return { newDocument: d, removed: rem[0] }; + }, + obj: function (o, k, d) { + const removed = o[k]; + delete o[k]; + return { newDocument: d, removed }; + }, + validator: () => {}, + }; + + const xCfg2 = { + arr: function (a, i, d) { + a[i] = this.value; + return { newDocument: d }; + }, + obj: function (o, k, d) { + o[k] = this.value; + return { newDocument: d }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-rm", xCfg); + jsonpatch.useExtendedOperation("x-set", xCfg2); + }); + + it("should find and replace appropriate end elements", function () { + const xop = { op: "x", xid: "x-set", value: 42 }; + let result; + + /************** w/out document modifications (4th arg false) **************/ + + /**** root obj is Array ****/ + + let b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1]).toBe(42); + // no base mods + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3]).toBe(42); + // no base mods + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/3/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3].c[3][2]).toBe(42); + // no base mods + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3].c[6]).toBe(42); + // no base mods + expect(b).toEqual(base_arr); + + /**** root obj is Object ****/ + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/0/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[0][2]).toBe(42); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1]).toBe(42); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1]).toBe(42); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/0/d"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1][0].d).toBe(42); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/--/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1][1][2]).toBe(42); + // no base mods + expect(b).toEqual(base_obj); + + /***************** w/document modifications (4th arg true) ****************/ + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1]).toBe(42); + // base mods + expect(b[1]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3]).toBe(42); + // base mods + expect(b[1].b[3]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/3/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3].c[3][2]).toBe(42); + // base mods + expect(b[1].b[3].c[3][2]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3].c[6]).toBe(42); + // base mods + expect(b[1].b[3].c[6]).toBe(42); + + /**** root obj is Object ****/ + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/0/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[0][2]).toBe(42); + // base mods + expect(b.b[0][2]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1]).toBe(42); + // base mods + expect(b.b[1]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1]).toBe(42); + // base mods + expect(b.b[1].c[1]).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/0/d"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1][0].d).toBe(42); + // base mods + expect(b.b[1].c[1][0].d).toBe(42); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/--/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1][1][2]).toBe(42); + // base mods + expect(b.b[1].c[1][1][2]).toBe(42); + }); + + it("should preform appropriate removals based on end sentinels", function () { + const xop = { op: "x", xid: "x-rm" }; + + /************** w/out document modifications (4th arg false) **************/ + + /**** root obj is Array ****/ + + // + let b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--"; + let result = jsonpatch.applyOperation(b, xop, true, false); + expect(Array.isArray(result.newDocument)).toBe(true); + expect(result.newDocument[1]).toBe(undefined); + // no changes should have been made to base + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3]).toBe(undefined); + // no base mods + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/3/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3].c[3][2]).toBe(undefined); + // no base mods + expect(b).toEqual(base_arr); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument[1].b[3].c[6]).toBe(undefined); + // no base mods + expect(b).toEqual(base_arr); + + /**** root obj is Object ****/ + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/0/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[0][2]).toBe(undefined); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1]).toBe(undefined); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1]).toBe(undefined); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/0/d"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1][0].d).toBe(undefined); + // no base mods + expect(b).toEqual(base_obj); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/--/--"; + result = jsonpatch.applyOperation(b, xop, true, false); + expect(result.newDocument.b[1].c[1][1][2]).toBe(undefined); + // no base mods + expect(b).toEqual(base_obj); + + /***************** w/document modifications (4th arg true) ****************/ + + /**** root obj is Array ****/ + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1]).toBe(undefined); + // base mods + expect(b[1]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3]).toBe(undefined); + // base mods + expect(b[1].b[3]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/3/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3].c[3][2]).toBe(undefined); + // base mods + expect(b[1].b[3].c[3][2]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_arr)); + xop.path = "/--/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument[1].b[3].c[6]).toBe(undefined); + // base mods + expect(b[1].b[3].c[6]).toBe(undefined); + + /**** root obj is Object ****/ + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/0/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[0][2]).toBe(undefined); + // base mods + expect(b.b[0][2]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1]).toBe(undefined); + // base mods + expect(b.b[1]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1]).toBe(undefined); + // base mods + expect(b.b[1].c[1]).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/0/d"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1][0].d).toBe(undefined); + // base mods + expect(b.b[1].c[1][0].d).toBe(undefined); + + // + b = JSON.parse(JSON.stringify(base_obj)); + xop.path = "/b/--/c/--/--/--"; + result = jsonpatch.applyOperation(b, xop, true, true); + expect(result.newDocument.b[1].c[1][1][2]).toBe(undefined); + // base mods + expect(b.b[1].c[1][1][2]).toBe(undefined); + }); +}); + +// Mixed w/RFC-6902 ops +describe("extended operations applied with RFC-6902 ops", function () { + let base_arr; + let base_obj; + let results; + + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + const xCfg = { + arr: function (a, i, d) { + a[i] = this.value; + // do this check AFTER the assignment to ensure + // original object is not mutated when returning undefined + if (this.value === 77) { + return undefined; + } + return { newDocument: d }; + }, + // This object operator sets key to value UNLESS value === 5, then it forces value of 999. + // If value === 77, operator should 'No Op' + obj: function (o, k, d) { + if (this.value === 5) { + o[k] = 999; + } else { + o[k] = this.value; + } + // do this check AFTER the assignment to ensure + // original object is not mutated when returning undefined + if (this.value === 77) { + return undefined; + } + return { newDocument: d }; + }, + validator: () => {}, + }; + + const xCfg2 = { + arr: function (a, i, d) { + const rem = a.splice(i, 1); + return { newDocument: d, removed: rem[0] }; + }, + obj: function (o, k, d) { + const removed = o[k]; + delete o[k]; + return { newDocument: d, removed }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-foo", xCfg); + jsonpatch.useExtendedOperation("x-rm", xCfg2); + + base_arr = [ + "hi", + { + // "/--" + a: "A", + b: [ + 0, + 1, + 2, + { + // "/--/b/--" + c: [ + 9, + 8, + 7, + [ + 6, + 5, + 4, // "/--/b/--/c/3/--" + ], + 3, + 2, + 1, // "/--/b/--/c/--" + ], + }, + ], + }, + ]; + + base_obj = { + a: "hi", + b: [ + [0, 1, 2], + { + c: [1, [{ d: "deep" }, [0, 1, 2]]], + }, + ], + }; + }); + + it("should add and replace with RFC ops while also modifying with extended ops", function () { + const ops = [ + { op: "add", path: "/1/b/-", value: 3 }, + { op: "x", xid: "x-foo", path: "/1/b/--", value: "X" }, + { op: "replace", path: "/1/b/4", value: "replaced" }, + ]; + + /************** w/out document modifications (4th arg false) **************/ + + let b = JSON.parse(JSON.stringify(base_arr)); + results = jsonpatch.applyPatch(b, ops, true, false); + expect(results.newDocument).toEqual([ + "hi", + { + a: "A", + b: [ + 0, + 1, + 2, + { + c: [9, 8, 7, [6, 5, 4], 3, 2, 1], + }, + "replaced", + ], + }, + ]); + }); + + it("should respect extended No-Op with RFC ops - root Array", function () { + const ops = [ + { op: "add", path: "/1/b/-", value: 4 }, + { op: "x", xid: "x-foo", path: "/1/b/--", value: "X" }, + { op: "replace", path: "/1/b/4", value: "replaced" }, + { op: "replace", path: "/1/b/4", value: "replaced again" }, + { op: "x", xid: "x-foo", path: "/1/b/--", value: 77 }, // NoOp + ]; + + const expected = [ + "hi", + { + a: "A", + b: [ + 0, + 1, + 2, + { + c: [9, 8, 7, [6, 5, 4], 3, 2, 1], + }, + "replaced again", + ], + }, + ]; + + let b = JSON.parse(JSON.stringify(base_arr)); + results = jsonpatch.applyPatch(b, ops, true, false); + expect(results.newDocument).toEqual(expected); + expect(b).toEqual(base_arr); + + b = [0, 1, 2]; + results = jsonpatch.applyPatch( + b, + [{ op: "x", xid: "x-rm", path: "/1" }], + true, + false + ); + expect(results.newDocument).toEqual([0, 2]); + expect(b).toEqual([0, 1, 2]); + + b = [0, 1, 2]; + results = jsonpatch.applyPatch( + b, + [{ op: "x", xid: "x-rm", path: "/--" }], + true, + false + ); + expect(results.newDocument).toEqual([0, 1]); + expect(b).toEqual([0, 1, 2]); + + /***************** w/document modifications (4th arg true) ****************/ + + b = JSON.parse(JSON.stringify(base_arr)); + results = jsonpatch.applyPatch(b, ops, true, true); + expect(results.newDocument).toEqual(expected); + expect(b).toEqual(results.newDocument); + + b = [0, 1, 2]; + results = jsonpatch.applyPatch( + b, + [{ op: "x", xid: "x-rm", path: "/1" }], + true, + true + ); + expect(results.newDocument).toEqual([0, 2]); + expect(b).toEqual([0, 2]); + + b = [0, 1, 2]; + results = jsonpatch.applyPatch( + b, + [{ op: "x", xid: "x-rm", path: "/--" }], + true, + true + ); + expect(results.newDocument).toEqual([0, 1]); + expect(b).toEqual([0, 1]); + }); + + it("should respect extended No-Op with RFC ops - root Object", function () { + const ops = [ + { op: "add", path: "/b/0/-", value: 3 }, + { op: "x", xid: "x-foo", path: "/b/0/--", value: "X" }, + { op: "replace", path: "/b/0/3", value: "replaced" }, + { op: "x", xid: "x-foo", path: "/b/--", value: 77 }, // NoOp + { op: "replace", path: "/b/0/3", value: "replaced again" }, + { op: "add", path: "/b/1/c/1/0/e", value: { f: "deep nested" } }, + { op: "x", xid: "x-rm", path: "/a" }, + ]; + + const expected = { + b: [ + [0, 1, 2, "replaced again"], + { + c: [ + 1, + [ + { + d: "deep", + e: { f: "deep nested" }, + }, + [0, 1, 2], + ], + ], + }, + ], + }; + + // top-level removal + const o = [{ op: "x", xid: "x-rm", path: "/a" }]; + + /************** w/out document modifications (4th arg false) **************/ + + let b = JSON.parse(JSON.stringify(base_obj)); + results = jsonpatch.applyPatch(b, ops, true, false); + expect(results.newDocument).toEqual(expected); + expect(b).toEqual(base_obj); + + // top + b = { a: "hi", b: 42 }; + results = jsonpatch.applyPatch(b, o, true, false); + expect(results.newDocument).toEqual({ b: 42 }); + expect(b).toEqual({ a: "hi", b: 42 }); + + /***************** w/document modifications (4th arg true) ****************/ + + b = JSON.parse(JSON.stringify(base_obj)); + results = jsonpatch.applyPatch(b, ops, true, true); + expect(results.newDocument).toEqual(expected); + expect(b).toEqual(results.newDocument); + + // top + b = { a: "hi", b: 42 }; + results = jsonpatch.applyPatch(b, o, true, true); + expect(results.newDocument).toEqual({ b: 42 }); + expect(b).toEqual(results.newDocument); + }); +}); + +describe("misc extended operations", function () { + let base_arr; + let base_obj; + let results; + + beforeAll(function () { + jsonpatch.unregisterAllExtendedOperations(); + + /* + operation that sums existing values with provided value + + rejects non-numeric type mismatch + + value must be numeric + + operation takes 3 optional additional args: + args[0] - number, default "accumulator" value when target path is undefined + default 0 + args[1] - number, minimum final value (inclusive) (default none) + args[2] - number, maximum final value (inclusive) (default none) + */ + + // this function can apply to both arr and obj + function xsum(o, k, d) { + let sum = + Array.isArray(this.args) && typeof this.args[0] === "number" + ? this.args[0] + : 0; + + if (o[k] === undefined) { + sum += this.value; + } else if (typeof o[k] !== "number") { + throw new jsonpatch.JsonPatchError( + "x-sum op target value is not a number", + "OPERATION_X_OPERATOR_EXCEPTION", + undefined, + this, + d + ); + } else { + sum = o[k] + this.value; + } + + // check min max + if (Array.isArray(this.args)) { + // min + if (this.args[1] !== undefined) { + sum = Math.max(sum, this.args[1]); + } + // max + if (this.args[2] !== undefined) { + sum = Math.min(sum, this.args[2]); + } + } + + // update + o[k] = sum; + return { newDocument: d }; + } + + const xCfg_sum = { + arr: xsum, + obj: xsum, + validator: (op, i, d, ep) => { + // validate operation inputs + if (typeof op.value !== "number") { + throw new jsonpatch.JsonPatchError( + "x-sum operation `value` must be a number", + "OPERATION_VALUE_REQUIRED", + i, + op, + d + ); + } + + if (Array.isArray(op.args)) { + if ( + op.args.find((v) => v !== undefined && typeof v !== "number") !== + undefined + ) { + throw new jsonpatch.JsonPatchError( + "x-sum operation all provided `args` must be a explicitly undefined or a number", + "OPERATION_X_OP_INVALID", + i, + op, + d + ); + } + } + }, + }; + + const xCfg_rm = { + arr: function (a, i, d) { + const rem = a.splice(i, 1); + return { newDocument: d, removed: rem[0] }; + }, + obj: function (o, k, d) { + const removed = o[k]; + delete o[k]; + return { newDocument: d, removed }; + }, + validator: () => {}, + }; + + jsonpatch.useExtendedOperation("x-sum", xCfg_sum); + jsonpatch.useExtendedOperation("x-rm", xCfg_rm); + + base_arr = [ + "hi", + { + // "/--" + a: "A", + b: [ + 0, + 1, + 2, + { + // "/--/b/--" + c: [ + 9, + 8, + 7, + [ + 6, + 5, + 4, // "/--/b/--/c/3/--" + ], + 3, + 2, + 42, // "/--/b/--/c/--" + ], + }, + ], + }, + ]; + + base_obj = { + a: "hi", + b: [ + [0, 1, 2], + { + c: [1, [{ d: "deep" }, [0, 1, 2]]], + }, + ], + }; + }); + + it("should run custom sum operator - obj basic", function () { + let b = JSON.parse(JSON.stringify(base_obj)); + const o = { op: "x", xid: "x-sum", path: "/b/0/1", value: 100 }; + const o2 = { op: "x", xid: "x-sum", path: "/d", value: 200 }; + const o3 = { op: "x", xid: "x-sum", path: "/b/1/e/f", value: 500 }; + let r = undefined; + + /************** w/out document modifications (4th arg false) **************/ + + r = jsonpatch.applyOperation(b, o, true, false); + expect(r.newDocument.b[0][1]).toBe(101); + expect(b.b[0][1]).toBe(1); + + r = jsonpatch.applyOperation(b, o2, true, false); + expect(r.newDocument.d).toBe(200); + expect(b.d).toBe(undefined); + + // reject when resolve == false + r = undefined; + b = JSON.parse(JSON.stringify(base_obj)); + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o3, true, false); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b.b[1].e).toBe(undefined); + + // resolve === true + r = undefined; + o3.resolve = true; + r = jsonpatch.applyOperation(b, o3, true, false); + expect(r.newDocument.b[1].e.f).toBe(500); + expect(b.b[1].e).toBe(undefined); + + /***************** w/document modifications (4th arg true) ****************/ + + b = JSON.parse(JSON.stringify(base_obj)); + r = jsonpatch.applyOperation(b, o, true, true); + expect(r.newDocument.b[0][1]).toBe(101); + expect(b.b[0][1]).toBe(101); + + b = JSON.parse(JSON.stringify(base_obj)); + r = jsonpatch.applyOperation(b, o2, true, true); + expect(r.newDocument.d).toBe(200); + expect(b.d).toBe(200); + + // reject when resolve == false + r = undefined; + o3.resolve = false; + b = JSON.parse(JSON.stringify(base_obj)); + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o3, true, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b.b[1].e).toBe(undefined); + + // resolve === true + r = undefined; + o3.resolve = true; + b = JSON.parse(JSON.stringify(base_obj)); + r = jsonpatch.applyOperation(b, o3, true, true); + expect(r.newDocument.b[1].e.f).toBe(500); + expect(b.b[1].e.f).toBe(500); + }); + + it("should run custom sum operator - arr basic", function () { + let b = JSON.parse(JSON.stringify(base_arr)); + // existing leaf + const o = { op: "x", xid: "x-sum", path: "/1/b/2", value: 100 }; + // root existing leaf - bad type + const o2 = { op: "x", xid: "x-sum", path: "/0", value: 200 }; + // deep nesting + const o3 = { op: "x", xid: "x-sum", path: "/1/b/3/c/3/1", value: 300 }; + // end element sentinel + const o4 = { op: "x", xid: "x-sum", path: "/1/b/3/c/--", value: 400 }; + // non-existent + const o5 = { op: "x", xid: "x-sum", path: "/1/d/1/e/f", value: 500 }; + let r = undefined; + + /************** w/out document modifications (4th arg false) **************/ + + r = jsonpatch.applyOperation(b, o, true, false); + expect(r.newDocument[1].b[2]).toBe(102); + expect(b[1].b[2]).toBe(2); + + // should reject - target value is not a number + r = undefined; + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o2, true, false); + }) + ).toThrowError(Error, "OPERATION_X_OPERATOR_EXCEPTION"); + expect(r).toBe(undefined); + expect(b[0]).toBe("hi"); + + r = jsonpatch.applyOperation(b, o3, true, false); + expect(r.newDocument[1].b[3].c[3][1]).toBe(305); + expect(b[1].b[3].c[3][1]).toBe(5); + + r = jsonpatch.applyOperation(b, o4, true, false); + expect(r.newDocument[1].b[3].c[6]).toBe(442); + expect(b[1].b[3].c[6]).toBe(42); + + // should reject - bad path - resolve == false + r = undefined; + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o5, true, false); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b[0]).toBe("hi"); + + // resolve === true + o5.resolve = true; + r = jsonpatch.applyOperation(b, o5, true, false); + expect(r.newDocument[1].d[1].e.f).toBe(500); + expect(b[1].d).toBe(undefined); + + /***************** w/document modifications (4th arg true) ****************/ + + b = JSON.parse(JSON.stringify(base_arr)); + r = jsonpatch.applyOperation(b, o, true, true); + expect(r.newDocument[1].b[2]).toBe(102); + expect(b[1].b[2]).toBe(102); + + // should reject - target value is not a number + b = JSON.parse(JSON.stringify(base_arr)); + r = undefined; + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o2, true, true); + }) + ).toThrowError(Error, "OPERATION_X_OPERATOR_EXCEPTION"); + expect(r).toBe(undefined); + expect(b[0]).toBe("hi"); + + b = JSON.parse(JSON.stringify(base_arr)); + r = jsonpatch.applyOperation(b, o3, true, true); + expect(r.newDocument[1].b[3].c[3][1]).toBe(305); + expect(b[1].b[3].c[3][1]).toBe(305); + + b = JSON.parse(JSON.stringify(base_arr)); + r = jsonpatch.applyOperation(b, o4, true, true); + expect(r.newDocument[1].b[3].c[6]).toBe(442); + expect(b[1].b[3].c[6]).toBe(442); + + // should reject - bad path - resolve == false + o5.resolve = false; + b = JSON.parse(JSON.stringify(base_arr)); + r = undefined; + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o5, true, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b[0]).toBe("hi"); + + // resolve === true + o5.resolve = true; + b = JSON.parse(JSON.stringify(base_arr)); + o5.resolve = true; + r = jsonpatch.applyOperation(b, o5, true, true); + expect(r.newDocument[1].d[1].e.f).toBe(500); + expect(b[1].d[1].e.f).toBe(500); + }); + + it("should run custom sum operator - obj w/args", function () { + let b = JSON.parse(JSON.stringify(base_obj)); + // existing path + const o = { op: "x", xid: "x-sum", path: "/b/0/1", value: 100 }; + // non-existent path + const o2 = { op: "x", xid: "x-sum", path: "/b/1/e/f", value: 500 }; + let r = undefined; + + /************** w/out document modifications (4th arg false) **************/ + + // test minimum + o.args = [undefined, 125]; + r = jsonpatch.applyOperation(b, o, true, false); + expect(r.newDocument.b[0][1]).toBe(125); + expect(b.b[0][1]).toBe(1); + + // test maximum + o.args = [undefined, undefined, 50]; + r = jsonpatch.applyOperation(b, o, true, false); + expect(r.newDocument.b[0][1]).toBe(50); + expect(b.b[0][1]).toBe(1); + + // reject when resolve == false + r = undefined; + o2.resolve = false; + o2.args = undefined; + b = JSON.parse(JSON.stringify(base_obj)); + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o2, true, false); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b.b[1].e).toBe(undefined); + + // resolve === true + r = undefined; + o2.resolve = true; + // new starting accumulator + o2.args = [-200]; + r = jsonpatch.applyOperation(b, o2, true, false); + expect(r.newDocument.b[1].e.f).toBe(300); + expect(b.b[1].e).toBe(undefined); + + /***************** w/document modifications (4th arg true) ****************/ + + // test minimum + b = JSON.parse(JSON.stringify(base_obj)); + o.args = [undefined, 125]; + r = jsonpatch.applyOperation(b, o, true, true); + expect(r.newDocument.b[0][1]).toBe(125); + expect(b.b[0][1]).toBe(125); + + // test maximum + b = JSON.parse(JSON.stringify(base_obj)); + o.args = [undefined, undefined, 50]; + r = jsonpatch.applyOperation(b, o, true, true); + expect(r.newDocument.b[0][1]).toBe(50); + expect(b.b[0][1]).toBe(50); + + // reject when resolve == false + r = undefined; + o2.resolve = false; + o2.args = undefined; + b = JSON.parse(JSON.stringify(base_obj)); + expect( + unwrapPatchError(() => { + r = jsonpatch.applyOperation(b, o2, true, true); + }) + ).toThrowError(Error, "OPERATION_PATH_UNRESOLVABLE"); + expect(r).toBe(undefined); + expect(b.b[1].e).toBe(undefined); + + // resolve === true + b = JSON.parse(JSON.stringify(base_obj)); + r = undefined; + o2.resolve = true; + // new starting accumulator + o2.args = [-200]; + r = jsonpatch.applyOperation(b, o2, true, true); + expect(r.newDocument.b[1].e.f).toBe(300); + expect(b.b[1].e.f).toBe(300); + }); + + it("should apply patch of repeated custom sum operations - obj w/args", function () { + const base = {}; + const patch = [ + { op: "add", path: "/a", value: {} }, + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 150 + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 250 + { op: "add", path: "/a/c", value: "hi" }, // a.c === 'hi' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 350 + { op: "replace", path: "/a/c", value: "hello" }, // a.c === 'hello' + { op: "x", xid: "x-sum", path: "/a/b", value: 100, args: [50, 100, 400] }, // a.b === 400 + ]; + + const expected = { + a: { + b: 400, + c: "hello", + }, + }; + + let r = undefined; + + /************** w/out document modifications (4th arg false) **************/ + let b = JSON.parse(JSON.stringify(base)); + r = jsonpatch.applyPatch(b, patch, true, false); + expect(r.newDocument.a.b).toBe(400); + expect(r.newDocument.a.c).toBe("hello"); + expect(b).toEqual({}); + + /***************** w/document modifications (4th arg true) ****************/ + b = JSON.parse(JSON.stringify(base)); + r = jsonpatch.applyPatch(b, patch, true, true); + expect(r.newDocument.a.b).toBe(400); + expect(r.newDocument.a.c).toBe("hello"); + expect(b).toEqual(expected); + }); +}); diff --git a/test/spec/webpack/importSpec.build.js b/test/spec/webpack/importSpec.build.js index 55e8d38..53af1af 100644 --- a/test/spec/webpack/importSpec.build.js +++ b/test/spec/webpack/importSpec.build.js @@ -1,13 +1,13 @@ -!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 m}),n.d(o,"deepClone",function(){return y}),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}); +!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 A}),n.d(o,"deepClone",function(){return _}),n.d(o,"useExtendedOperation",function(){return x}),n.d(o,"hasExtendedOperation",function(){return T}),n.d(o,"unregisterAllExtendedOperations",function(){return N}),n.d(o,"getValueByPointer",function(){return P}),n.d(o,"applyOperation",function(){return I}),n.d(o,"applyPatch",function(){return D}),n.d(o,"applyReducer",function(){return R}),n.d(o,"validator",function(){return j}),n.d(o,"validate",function(){return L}),n.d(o,"_areEquals",function(){return C});var r={};n.r(r),n.d(r,"unobserve",function(){return U}),n.d(r,"observe",function(){return k}),n.d(r,"generate",function(){return M}),n.d(r,"compare",function(){return X});var i={};n.r(i),n.d(i,"JsonPatchError",function(){return w}),n.d(i,"deepClone",function(){return f}),n.d(i,"escapePathComponent",function(){return h}),n.d(i,"unescapePathComponent",function(){return l}),n.d(i,"default",function(){return J}),n.d(i,"useExtendedOperation",function(){return x}),n.d(i,"hasExtendedOperation",function(){return T}),n.d(i,"unregisterAllExtendedOperations",function(){return N}),n.d(i,"getValueByPointer",function(){return P}),n.d(i,"applyOperation",function(){return I}),n.d(i,"applyPatch",function(){return D}),n.d(i,"applyReducer",function(){return R}),n.d(i,"validator",function(){return j}),n.d(i,"validate",function(){return L}),n.d(i,"_areEquals",function(){return C}),n.d(i,"unobserve",function(){return U}),n.d(i,"observe",function(){return k}),n.d(i,"generate",function(){return M}),n.d(i,"compare",function(){return X}); /*! * https://github.com/Starcounter-Jack/JSON-Patch * (c) 2017 Joachim Wester * MIT license */ -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),m=w,y=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 m("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 m("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)throw new TypeError("JSON-Patch: modifying `__proto__` 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 m("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 m("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 m("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 m("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a}if(p=p[v],n&&c0)throw new m('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 m("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 m("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;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),m="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";function O(e){return"string"==typeof e&&e.length>=3&&0===e.indexOf("x-")}function y(e,t,n){if(0!==n.comps.length){var o=t,r=e,i="";if(1===n.comps.length)return i=n.comps[0],void("graft"===n.modType?o[i]=r[i]:Array.isArray(t)?o.splice(~~i,1):delete o[i]);for(var a=0;a0&&"constructor"==c[h-1]))throw new TypeError(m);if(n&&void 0===w&&(void 0===p[O]?w=c.slice(0,h).join("/"):h==v-1&&(w=t.path),void 0!==w&&T(t,0,e,w)),h++,Array.isArray(p)){if("x"===t.op&&!0!==t.resolve&&""===O)throw new A("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);if("-"===O)O=p.length,"x"===t.op&&(c[h-1]=O.toString(10),void 0===x&&(x=c.slice(0,h).join("/")));else if("x"===t.op&&"--"===O)O=p.length-1,c[h-1]=O.toString(10),void 0===x&&(x=c.slice(0,h).join("/"));else{if(n&&!s(O))throw new A("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);if(s(O)&&(O=~~O,"x"===t.op&&!0!==t.resolve&&O>=p.length))throw new A("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",i,t,e)}if(h>=v){if("x"===t.op&&_){if("string"==typeof O&&0===O.length&&!t.resolve)return{newDocument:t.value};if(void 0===(d=_.arr.call(t,p,O,u)))return{newDocument:e};if(void 0!==d.removed&&!0===t.resolve)throw new A("Extended operation should not remove items while resolving undefined paths","OPERATION_X_AMBIGUOUS_REMOVAL",i,t,e);if(o){var N={modType:"graft",comps:void 0===x?c.slice(1,h):x.split("/").slice(1)};return void 0!==d.removed&&(N={modType:"prune",comps:c.slice(1,h)}),y(d.newDocument,e,N),{newDocument:e}}return d}if(n&&"add"===t.op&&O>p.length)throw new A("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=g[t.op].call(t,p,O,e)).test)throw new A("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a}}else if(h>=v){if("x"===t.op&&_){if(0===O.length&&!t.resolve)return{newDocument:t.value};if(void 0===(d=_.obj.call(t,p,O,u)))return{newDocument:e};if(o){N={modType:"graft",comps:void 0===x?c.slice(1,h):x.split("/").slice(1)};return void 0!==d.removed&&(N={modType:"prune",comps:c.slice(1,h)}),y(d.newDocument,e,N),{newDocument:e}}return d}if(!1===(a=E[t.op].call(t,p,O,e)).test)throw new A("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return a}if("x"===t.op&&_&&void 0===p[O]&&(void 0===x&&(x=c.slice(0,h).join("/")),!0===t.resolve&&(Array.isArray(e)&&s(String(c[h]))?p[O]=new Array(1+~~c[h]):p[O]={})),p=p[O],n&&h0)throw new A('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,n);if("/"===e.path)throw new A("Operation `path` slash-only is ambiguous","OPERATION_PATH_UNRESOLVABLE",t,e,n);if(void 0!==e.args&&!Array.isArray(e.args))throw new A("Operation `args` property is not an array","OPERATION_X_ARGS_NOT_ARRAY",t,e,n);r.validator.call(e,e,t,n,o)}else{if(!E[e.op])throw new A("Operation `op` property is not one of operations defined in RFC-6902","OPERATION_OP_INVALID",t,e,n);if("string"!=typeof e.path)throw new A("Operation `path` property is not a string","OPERATION_PATH_INVALID",t,e,n);if(0!==e.path.indexOf("/")&&e.path.length>0)throw new A('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 A("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 A("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;p0&&(e.patches=[],e.callback&&e.callback(o)),o}function F(e,t,n,o,r){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=c(t),a=c(e),u=!1,p=a.length-1;p>=0;p--){var s=e[v=a[p]];if(!d(t,v)||void 0===t[v]&&void 0!==s&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(r&&n.push({op:"test",path:o+"/"+h(v),value:f(s)}),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 s&&null!=s&&"object"==typeof l&&null!=l&&Array.isArray(s)===Array.isArray(l)?F(s,l,n,o+"/"+h(v),r):s!==l&&(!0,r&&n.push({op:"test",path:o+"/"+h(v),value:f(s)}),n.push({op:"replace",path:o+"/"+h(v),value:f(l)}))}}if(u||i.length!=a.length)for(p=0;p