UNPKG

immutable-json-patch

Version:

Immutable JSON patch with support for reverting operations

171 lines (158 loc) 5.45 kB
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