0.2.19 • Published 5 months ago

@api-client/json v0.2.19

Weekly downloads
-
License
CC-BY-2.0
Repository
github
Last release
5 months ago

JSON Patch Library for API Clients

This library provides a robust implementation of JSON Patch (as defined by RFC 6902) for JavaScript applications. It's designed to be used primarily with API clients, enabling efficient and precise data updates. Think of it as a way to send "diffs" or "changes" to a JSON document, rather than sending the entire document every time.

This library is built as an ES module (ESM) and includes advanced features like calculating diffs, reverting changes, and patch rebasing for handling concurrent edits. It builds upon the JSON8 library.

Key Concepts

Before diving into the code, let's understand the core ideas:

  • JSON Patch: A standardized way to describe changes to a JSON document. Instead of sending a whole new document, you send a "patch" that describes the changes (add, remove, replace, etc.).
  • Operation: A single change within a JSON Patch (e.g., "add a new value at this path").
  • Path: A "location" within the JSON document where an operation happens, using JSON Pointer notation. (e.g. /foo/bar/0 will point to the first element of bar array, inside foo object).
  • Diff: The difference between two JSON documents, expressed as a JSON Patch.
  • Revert: Undoing a patch, restoring the document to its previous state.
  • Rebase: Resolves conflicts between the server's and the client's changes to the document.

Installation

npm install @api-client/json

Basic Usage: Applying a Patch

The most common use case is applying a patch to a document.

import { Patch, Json } from '@api-client/json';

// Our starting document
let myDocument = {
  "foo": "bar",
  "baz": [1, 2, 3],
};

// A patch that describes how to modify the document
const myPatch = [
  { "op": "replace", "path": "/foo", "value": "new value" },
  { "op": "add", "path": "/baz/-", "value": 4 } // Adds 4 to the end of the array
];

// Apply the patch
const result = Patch.apply(myDocument, myPatch);

// Update the document with new values
myDocument = result.doc;

// myDocument is now: { "foo": "new value", "baz": [1, 2, 3, 4] }
console.log(myDocument);

Important Note on Mutability:

For efficiency, Patch.apply may modify the original document you pass in. If you need to preserve the original, make a copy:

const myOriginalDocument = { foo: 'bar' };
const docCopy = Json.clone(myOriginalDocument); // Create a deep copy

const result = Patch.apply(docCopy, myPatch); // Operate on the copy
// myOriginalDocument is unchanged, docCopy has been updated

Core API Functions

Here are the essential functions you'll use most often:

Patch.apply(document, patch, [options])

Applies a patch (an array of operations) to a document.

  • document: The target document (may be mutated).
  • patch: The JSON Patch to apply (an array of operation objects).
  • options: Optional. If { reversible: true } is provided, the return value will include a revert property.

Returns an object with a doc property containing the patched document. Due to the JSON Patch specification, this may be a new object, even if the patch didn't modify the document.

The operation is atomic: if any patch operation fails, the document is restored to its original state, and an error is thrown.

import { Patch } from '@api-client/json'

const result = Patch.apply(doc, patch)
doc = result.doc

// With reversible option:
const reversibleResult = Patch.apply(doc, patch, { reversible: true })
doc = reversibleResult.doc
const revertPatch = reversibleResult.revert // Object that can be used to revert the patch

Patch.patch(document, patch, [options])

Alias for Patch.apply().

Patch.revert(document, revert)

Reverts a patch using a revert object (obtained from Patch.apply with reversible: true).

  • document: The document to revert.
  • revert: The revert object.

Returns an object with a doc property containing the reverted document.

import { Patch } from '@api-client/json';

const myDocument = { a: 1 };
const myPatch = [{ op: 'replace', path: '/a', value: 2 }];

// Apply the patch with revert info
const result = Patch.apply(myDocument, myPatch, { reversible: true });
const patchedDocument = result.doc; // { a: 2 }
const revertInfo = result.revert;

// Revert the change
const revertedResult = Patch.revert(patchedDocument, revertInfo);
const revertedDocument = revertedResult.doc; // { a: 1 }

Patch.buildRevertPatch(revert)

Builds a standard JSON Patch from a revert object. This allows for more flexibility, such as storing the revert patch, applying it later, or combining it with other patches.

  • revert: The revert object.

Returns a valid JSON Patch (an array of operation objects).

import { Patch } from '@api-client/json';

// ... previous example ...
const revertPatch = Patch.buildRevertPatch(revertInfo);
// Now revertPatch is a regular JSON Patch you can store or apply later
const revertedResult = Patch.apply(patchedDocument, revertPatch);
const revertedDocument = revertedResult.doc; // { a: 1 }

Patch.diff(value1, value2)

Computes the difference between two JSON values and returns it as a JSON Patch.

  • value1: The original value.
  • value2: The new value.

Returns a JSON Patch (an array of operation objects).

import { Patch } from '@api-client/json';

const original = { a: 1, b: 2 };
const updated = { a: 1, b: 3 };
const diffPatch = Patch.diff(original, updated);
console.log(diffPatch) // it will print: [{ op: 'replace', path: '/b', value: 3 }];

Patch.valid(patch)

Checks if a patch is semantically valid according to the JSON Patch specification. It does not check if the patch is valid JSON. Use JSON.parse or a similar method for JSON validation.

  • patch: The patch to validate.

Returns true if the patch is valid, false otherwise.

Operation Methods (Patch.add, Patch.remove, Patch.replace, Patch.move, Patch.copy, Patch.test)

These methods provide direct access to individual JSON Patch operations. They all have the same signature:

Patch.operation(document, path, value / from) // value for add, replace, test; from for move, copy
  • document: The JSON document.
  • path: The target path (JSON Pointer).
  • value: Value for add, replace, test.
  • from: Path to the original value for move, copy.

They return an object with doc (the modified document) and previous (the previous value at the path, if applicable) properties.

import { Patch } from '@api-client/json';

let myDocument = { foo: 'bar', list: [1,2,3] };
myDocument = Patch.add(myDocument, '/newKey', 'newValue').doc; // Adds newKey:newValue
myDocument = Patch.remove(myDocument, '/foo').doc; // Removes 'foo'
myDocument = Patch.replace(myDocument, '/list/0', 10).doc; //Replaces list[0] with 10
myDocument = Patch.move(myDocument, '/list/1', '/newPath').doc; // Moves list[1] to newPath
myDocument = Patch.copy(myDocument, '/list/0', '/copiedPath').doc; // Copies list[0] to copiedPath
const testResult = Patch.test(myDocument, '/list/0', 10) //returns {doc:myDocument, success: true} // Tests if list[0] equals 10. Doesn't modify the document

Extra Methods (Patch.get, Patch.has)

Utility functions for interacting with JSON documents.

  • Patch.get(document, path): Retrieves the value at the specified path.
  • Patch.has(document, path): Checks if a value exists at the specified path.

Patch.pack(patch) and Patch.unpack(packedPatch)

These methods compress and decompress JSON Patches to reduce their size, which can be useful for storage or transmission.

  • Patch.pack(patch): Compresses a patch.
  • Patch.unpack(packedPatch): Decompresses a packed patch.
const packed = Patch.pack(patch)
const unpacked = Patch.unpack(packed)

Rebasing Documents and Patches

Rebasing is a process of adjusting a local patch (client changes) to account for changes made by the server. It's crucial in collaborative scenarios where multiple clients may be editing the same document concurrently. This library provides two main functions for rebasing: Patch.rebase and Patch.rebaseDocument.

Patch.rebase(base, client, server)

The Patch.rebase function is a utility for transforming a client's patch against a server's patch, given a common base document. The base should be a common document for both client and server.

  • base: The original document.
  • client: The patch created by the client.
  • server: The patch created by the server.

Returns an object containing the client (the rebased client patch) and diff (the patch representing the changes that happened during the rebase process).

Usage scenario:

  1. A client retrieves a document from the server (the base).
  2. The client makes changes to the document, generating a client patch.
  3. The server also makes changes to the document, generating a server patch.
  4. Before sending the client patch to the server, the client performs a rebase operation.
  5. The client will receive a rebased patch and the changes that happened during the rebase.
  6. The client applies the diff to its document, and then sends the rebased patch to the server.
import { Patch } from '@api-client/json';

// base document
let doc = { "foo": "bar" };
// server patch
const serverPatch = [{ op: 'replace', path: '/foo', value: 'server' }];
// client patch
const clientPatch = [{ op: 'replace', path: '/foo', value: 'client' }];
const result = Patch.rebase(doc, clientPatch, serverPatch);

console.log(result.client) // [{ op: 'replace', path: '/foo', value: 'client' }]
console.log(result.diff) // [{ op: 'replace', path: '/foo', value: 'server' }]

Patch.rebaseDocument(document, client, server)

The Patch.rebaseDocument function is a helper that does the whole rebase process.

  • document: The document to rebase.
  • client: The patch created by the client.
  • server: The patch created by the server.

Returns an object with:

  • doc: The modified document.
  • diff: The diff (local patch) generated during the rebase process.
  • client: The rebased client patches.

Usage scenario:

  1. A client retrieves a document from the server.
  2. The client makes changes to the document, generating a client patch.
  3. The server also makes changes to the document, generating a server patch.
  4. The client then calls Patch.rebaseDocument to rebase its changes.
  5. Client will receive modified document, diff patch and the rebased client patch.
  6. Client then sends the rebased client patch to the server.
import { Patch } from '@api-client/json';

// base document
let doc = { "foo": "bar" };
// server patch
const serverPatch = [{ op: 'replace', path: '/foo', value: 'server' }];
// client patch
const clientPatch = [{ op: 'replace', path: '/foo', value: 'client' }];
const result = Patch.rebaseDocument(doc, clientPatch, serverPatch);

console.log(result.doc) // { foo: 'server' }
console.log(result.client) // [{ op: 'replace', path: '/foo', value: 'client' }]
console.log(result.diff) // [{ op: 'replace', path: '/foo', value: 'server' }]

In this example result.doc will be { foo: 'server' } because in this use case the server made a change, and the client will receive it, so it can update its document. The result.diff will be the server's changes, in this case { op: 'replace', path: '/foo', value: 'server' }. The result.client will be the client changes, in this case { op: 'replace', path: '/foo', value: 'client' }.

This process ensures that both the client and server remain in sync with the latest changes.

0.2.19

5 months ago

0.2.18

5 months ago

0.2.17

5 months ago

0.2.16

5 months ago

0.2.15

6 months ago

0.2.14

7 months ago

0.2.13

7 months ago

0.2.12

7 months ago

0.2.11

7 months ago

0.2.10

7 months ago

0.2.1

8 months ago

0.2.0

8 months ago

0.2.7

8 months ago

0.2.6

8 months ago

0.2.9

7 months ago

0.2.8

8 months ago

0.2.3

8 months ago

0.2.2

8 months ago

0.2.5

8 months ago

0.1.6

9 months ago

0.2.4

8 months ago

0.1.5

9 months ago

0.1.4

3 years ago

0.1.3

4 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago