immutable-json-patch
Version:
Immutable JSON patch with support for reverting operations
187 lines (174 loc) • 6.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.add = add;
exports.copy = copy;
exports.immutableJSONPatch = immutableJSONPatch;
exports.isArrayItem = isArrayItem;
exports.move = move;
exports.parseFrom = parseFrom;
exports.parsePath = parsePath;
exports.remove = remove;
exports.replace = replace;
exports.resolvePathIndex = resolvePathIndex;
exports.test = test;
exports.validateJSONPatchOperation = validateJSONPatchOperation;
var _immutabilityHelpers = require("./immutabilityHelpers.js");
var _jsonPointer = require("./jsonPointer.js");
var _utils = require("./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
*/
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
*/
function replace(document, path, value) {
return (0, _immutabilityHelpers.existsIn)(document, path) ? (0, _immutabilityHelpers.setIn)(document, path, value) : document;
}
/**
* Remove an item or property
*/
function remove(document, path) {
return (0, _immutabilityHelpers.deleteIn)(document, path);
}
/**
* Add an item or property
*/
function add(document, path, value) {
if (isArrayItem(document, path)) {
return (0, _immutabilityHelpers.insertAt)(document, path, value);
}
return (0, _immutabilityHelpers.setIn)(document, path, value);
}
/**
* Copy a value
*/
function copy(document, path, from) {
const value = (0, _immutabilityHelpers.getIn)(document, from);
if (isArrayItem(document, path)) {
return (0, _immutabilityHelpers.insertAt)(document, path, value);
}
return (0, _immutabilityHelpers.setIn)(document, path, value);
}
/**
* Move a value
*/
function move(document, path, from) {
const value = (0, _immutabilityHelpers.getIn)(document, from);
const removedJson = (0, _immutabilityHelpers.deleteIn)(document, from);
return isArrayItem(removedJson, path) ? (0, _immutabilityHelpers.insertAt)(removedJson, path, value) : (0, _immutabilityHelpers.setIn)(removedJson, path, value);
}
/**
* Test whether the data contains the provided value at the specified path.
* Throws an error when the test fails
*/
function test(document, path, value) {
if (value === undefined) {
throw new Error(`Test failed: no value provided (path: "${(0, _jsonPointer.compileJSONPointer)(path)}")`);
}
if (!(0, _immutabilityHelpers.existsIn)(document, path)) {
throw new Error(`Test failed: path not found (path: "${(0, _jsonPointer.compileJSONPointer)(path)}")`);
}
const actualValue = (0, _immutabilityHelpers.getIn)(document, path);
if (!(0, _utils.isEqual)(actualValue, value)) {
throw new Error(`Test failed, value differs (path: "${(0, _jsonPointer.compileJSONPointer)(path)}")`);
}
}
function isArrayItem(document, path) {
if (path.length === 0) {
return false;
}
const parent = (0, _immutabilityHelpers.getIn)(document, (0, _utils.initial)(path));
return Array.isArray(parent);
}
/**
* Resolve the path index of an array, resolves indexes '-'
* @returns Returns the resolved path
*/
function resolvePathIndex(document, path) {
if ((0, _utils.last)(path) !== '-') {
return path;
}
const parentPath = (0, _utils.initial)(path);
const parent = (0, _immutabilityHelpers.getIn)(document, parentPath);
// @ts-ignore
return parentPath.concat(parent.length);
}
/**
* Validate a JSONPatch operation.
* Throws an error when there is an issue
*/
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)}`);
}
}
}
function parsePath(document, pointer) {
return resolvePathIndex(document, (0, _jsonPointer.parseJSONPointer)(pointer));
}
function parseFrom(fromPointer) {
return (0, _jsonPointer.parseJSONPointer)(fromPointer);
}
//# sourceMappingURL=immutableJSONPatch.js.map