immutable-json-patch
Version:
Immutable JSON patch with support for reverting operations
171 lines (158 loc) • 5.45 kB
JavaScript
import { deleteIn, existsIn, getIn, insertAt, setIn } from './immutabilityHelpers.js';
import { compileJSONPointer, parseJSONPointer } from './jsonPointer.js';
import { initial, isEqual, last } from './utils.js';
/**
* Apply a patch to a JSON object
* The original JSON object will not be changed,
* instead, the patch is applied in an immutable way
*/
export function immutableJSONPatch(document, operations, options) {
let updatedDocument = document;
for (let i = 0; i < operations.length; i++) {
validateJSONPatchOperation(operations[i]);
let operation = operations[i];
// TODO: test before
if (options?.before) {
const result = options.before(updatedDocument, operation);
if (result !== undefined) {
if (result.document !== undefined) {
updatedDocument = result.document;
}
// @ts-ignore
if (result.json !== undefined) {
// TODO: deprecated since v5.0.0. Cleanup this warning some day
throw new Error('Deprecation warning: returned object property ".json" has been renamed to ".document"');
}
if (result.operation !== undefined) {
operation = result.operation;
}
}
}
const previousDocument = updatedDocument;
const path = parsePath(updatedDocument, operation.path);
if (operation.op === 'add') {
updatedDocument = add(updatedDocument, path, operation.value);
} else if (operation.op === 'remove') {
updatedDocument = remove(updatedDocument, path);
} else if (operation.op === 'replace') {
updatedDocument = replace(updatedDocument, path, operation.value);
} else if (operation.op === 'copy') {
updatedDocument = copy(updatedDocument, path, parseFrom(operation.from));
} else if (operation.op === 'move') {
updatedDocument = move(updatedDocument, path, parseFrom(operation.from));
} else if (operation.op === 'test') {
test(updatedDocument, path, operation.value);
} else {
throw new Error(`Unknown JSONPatch operation ${JSON.stringify(operation)}`);
}
// TODO: test after
if (options?.after) {
const result = options.after(updatedDocument, operation, previousDocument);
if (result !== undefined) {
updatedDocument = result;
}
}
}
return updatedDocument;
}
/**
* Replace an existing item
*/
export function replace(document, path, value) {
return existsIn(document, path) ? setIn(document, path, value) : document;
}
/**
* Remove an item or property
*/
export function remove(document, path) {
return deleteIn(document, path);
}
/**
* Add an item or property
*/
export function add(document, path, value) {
if (isArrayItem(document, path)) {
return insertAt(document, path, value);
}
return setIn(document, path, value);
}
/**
* Copy a value
*/
export function copy(document, path, from) {
const value = getIn(document, from);
if (isArrayItem(document, path)) {
return insertAt(document, path, value);
}
return setIn(document, path, value);
}
/**
* Move a value
*/
export function move(document, path, from) {
const value = getIn(document, from);
const removedJson = deleteIn(document, from);
return isArrayItem(removedJson, path) ? insertAt(removedJson, path, value) : setIn(removedJson, path, value);
}
/**
* Test whether the data contains the provided value at the specified path.
* Throws an error when the test fails
*/
export function test(document, path, value) {
if (value === undefined) {
throw new Error(`Test failed: no value provided (path: "${compileJSONPointer(path)}")`);
}
if (!existsIn(document, path)) {
throw new Error(`Test failed: path not found (path: "${compileJSONPointer(path)}")`);
}
const actualValue = getIn(document, path);
if (!isEqual(actualValue, value)) {
throw new Error(`Test failed, value differs (path: "${compileJSONPointer(path)}")`);
}
}
export function isArrayItem(document, path) {
if (path.length === 0) {
return false;
}
const parent = getIn(document, initial(path));
return Array.isArray(parent);
}
/**
* Resolve the path index of an array, resolves indexes '-'
* @returns Returns the resolved path
*/
export function resolvePathIndex(document, path) {
if (last(path) !== '-') {
return path;
}
const parentPath = initial(path);
const parent = getIn(document, parentPath);
// @ts-ignore
return parentPath.concat(parent.length);
}
/**
* Validate a JSONPatch operation.
* Throws an error when there is an issue
*/
export function validateJSONPatchOperation(operation) {
// TODO: write unit tests
const ops = ['add', 'remove', 'replace', 'copy', 'move', 'test'];
if (!ops.includes(operation.op)) {
throw new Error(`Unknown JSONPatch op ${JSON.stringify(operation.op)}`);
}
if (typeof operation.path !== 'string') {
throw new Error(`Required property "path" missing or not a string in operation ${JSON.stringify(operation)}`);
}
if (operation.op === 'copy' || operation.op === 'move') {
if (typeof operation.from !== 'string') {
throw new Error(`Required property "from" missing or not a string in operation ${JSON.stringify(operation)}`);
}
}
}
export function parsePath(document, pointer) {
return resolvePathIndex(document, parseJSONPointer(pointer));
}
export function parseFrom(fromPointer) {
return parseJSONPointer(fromPointer);
}
//# sourceMappingURL=immutableJSONPatch.js.map