Files
dspace-angular/src/app/core/json-patch/json-patch-operations.reducer.ts
2024-01-26 16:01:01 -06:00

381 lines
12 KiB
TypeScript

import { hasValue, isNotEmpty, isNotUndefined, isNull } from '../../shared/empty.util';
import {
FlushPatchOperationsAction,
PatchOperationsActions,
JsonPatchOperationsActionTypes,
NewPatchAddOperationAction,
NewPatchCopyOperationAction,
NewPatchMoveOperationAction,
NewPatchRemoveOperationAction,
NewPatchReplaceOperationAction,
CommitPatchOperationsAction,
StartTransactionPatchOperationsAction,
RollbacktPatchOperationsAction,
DeletePendingJsonPatchOperationsAction
} from './json-patch-operations.actions';
import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model';
/**
* An interface to represent JSON-PATCH Operation objects to execute
*/
export interface JsonPatchOperationObject {
operation: JsonPatchOperationModel;
timeCompleted: number;
}
/**
* An interface to represent the body containing a list of JsonPatchOperationObject
*/
export interface JsonPatchOperationsEntry {
body: JsonPatchOperationObject[];
}
/**
* Interface used to represent a JSON-PATCH path member
* in JsonPatchOperationsState
*/
export interface JsonPatchOperationsResourceEntry {
children: { [resourceId: string]: JsonPatchOperationsEntry };
transactionStartTime: number;
commitPending: boolean;
}
/**
* The JSON patch operations State
*
* Consists of a map with a namespace as key,
* and an array of JsonPatchOperationModel as values
*/
export interface JsonPatchOperationsState {
[resourceType: string]: JsonPatchOperationsResourceEntry;
}
const initialState: JsonPatchOperationsState = Object.create(null);
/**
* The JSON-PATCH operations Reducer
*
* @param state
* the current state
* @param action
* the action to perform on the state
* @return JsonPatchOperationsState
* the new state
*/
export function jsonPatchOperationsReducer(state = initialState, action: PatchOperationsActions): JsonPatchOperationsState {
switch (action.type) {
case JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS: {
return commitOperations(state, action as CommitPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS: {
return flushOperation(state, action as FlushPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: {
return newOperation(state, action as NewPatchAddOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION: {
return newOperation(state, action as NewPatchCopyOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION: {
return newOperation(state, action as NewPatchMoveOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: {
return newOperation(state, action as NewPatchRemoveOperationAction);
}
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION: {
return newOperation(state, action as NewPatchReplaceOperationAction);
}
case JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS: {
return rollbackOperations(state, action as RollbacktPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS: {
return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction);
}
case JsonPatchOperationsActionTypes.DELETE_PENDING_JSON_PATCH_OPERATIONS: {
return deletePendingOperations(state, action as DeletePendingJsonPatchOperationsAction);
}
default: {
return state;
}
}
}
/**
* Set the transaction start time.
*
* @param state
* the current state
* @param action
* an StartTransactionPatchOperationsAction
* @return JsonPatchOperationsState
* the new state.
*/
function startTransactionPatchOperations(state: JsonPatchOperationsState, action: StartTransactionPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& isNull(state[ action.payload.resourceType ].transactionStartTime)) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
transactionStartTime: action.payload.startTime,
commitPending: true
})
});
} else {
return state;
}
}
/**
* Set commit pending state.
*
* @param state
* the current state
* @param action
* an CommitPatchOperationsAction
* @return JsonPatchOperationsState
* the new state, with the section new validity status.
*/
function commitOperations(state: JsonPatchOperationsState, action: CommitPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& state[ action.payload.resourceType ].commitPending) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
commitPending: false
})
});
} else {
return state;
}
}
/**
* Set commit pending state.
*
* @param state
* the current state
* @param action
* an RollbacktPatchOperationsAction
* @return JsonPatchOperationsState
* the new state.
*/
function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])
&& state[ action.payload.resourceType ].commitPending) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
transactionStartTime: null,
commitPending: false
})
});
} else {
return state;
}
}
/**
* Set the JsonPatchOperationsState to its initial value.
*
* @param state
* the current state
* @param action
* an DeletePendingJsonPatchOperationsAction
* @return JsonPatchOperationsState
* the new state.
*/
function deletePendingOperations(state: JsonPatchOperationsState, action: DeletePendingJsonPatchOperationsAction): JsonPatchOperationsState {
return null;
}
/**
* Add new JSON patch operation list.
*
* @param state
* the current state
* @param action
* an NewPatchAddOperationAction
* @return JsonPatchOperationsState
* the new state, with the section new validity status.
*/
function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperationsState {
const newState = Object.assign({}, state);
const body: any[] = hasValidBody(newState, action.payload.resourceType, action.payload.resourceId)
? newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body : Array.of();
const newBody = addOperationToList(
body,
action.type,
action.payload.path,
hasValue(action.payload.value) ? action.payload.value : null,
hasValue(action.payload.from) ? action.payload.from : null);
if (hasValue(newState[ action.payload.resourceType ])
&& hasValue(newState[ action.payload.resourceType ].children)) {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: Object.assign({}, state[ action.payload.resourceType ].children, {
[action.payload.resourceId]: {
body: newBody,
}
}),
commitPending: isNotUndefined(state[ action.payload.resourceType ].commitPending) ? state[ action.payload.resourceType ].commitPending : false
})
});
} else {
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, {
children: {
[action.payload.resourceId]: {
body: newBody,
}
},
transactionStartTime: null,
commitPending: false
})
});
}
}
/**
* Check if state has a valid body.
*
* @param state
* the current state
* @param resourceType
* an resource type
* @param resourceId
* an resource ID
* @return boolean
*/
function hasValidBody(state: JsonPatchOperationsState, resourceType: any, resourceId: any): boolean {
return (hasValue(state[ resourceType ])
&& hasValue(state[ resourceType ].children)
&& hasValue(state[ resourceType ].children[ resourceId ])
&& isNotEmpty(state[ resourceType ].children[ resourceId ].body));
}
/**
* Set the section validity.
*
* @param state
* the current state
* @param action
* an FlushPatchOperationsAction
* @return SubmissionObjectState
* the new state, with the section new validity status.
*/
function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOperationsAction): JsonPatchOperationsState {
if (hasValue(state[ action.payload.resourceType ])) {
let newChildren;
if (isNotUndefined(action.payload.resourceId)) {
// flush only specified child's operations
if (hasValue(state[ action.payload.resourceType ].children)
&& hasValue(state[ action.payload.resourceType ].children[ action.payload.resourceId ])) {
newChildren = Object.assign({}, state[ action.payload.resourceType ].children, {
[action.payload.resourceId]: {
body: state[ action.payload.resourceType ].children[ action.payload.resourceId ].body
.filter((entry) => entry.timeCompleted > state[ action.payload.resourceType ].transactionStartTime)
}
});
} else {
newChildren = state[ action.payload.resourceType ].children;
}
} else {
// flush all children's operations
newChildren = state[ action.payload.resourceType ].children;
Object.keys(newChildren)
.forEach((resourceId) => {
newChildren = Object.assign({}, newChildren, {
[resourceId]: {
body: newChildren[ resourceId ].body
.filter((entry) => entry.timeCompleted > state[ action.payload.resourceType ].transactionStartTime)
}
});
});
}
return Object.assign({}, state, {
[action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], {
children: newChildren,
transactionStartTime: null,
})
});
} else {
return state;
}
}
/**
* Add a new operation to a patch
*
* @param body
* The current patch
* @param actionType
* The type of operation to add
* @param targetPath
* The path for the operation
* @param value
* The new value
* @param fromPath
* The previous path (in case of a move operation)
*/
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?, fromPath?) {
const newBody = Array.from(body);
switch (actionType) {
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
newBody.push(makeOperationEntry({
op: JsonPatchOperationType.add,
path: targetPath,
value: value
}));
break;
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION:
newBody.push(makeOperationEntry({
op: JsonPatchOperationType.replace,
path: targetPath,
value: value
}));
break;
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
break;
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION:
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath }));
break;
}
return dedupeOperationEntries(newBody);
}
/**
* Dedupe operation entries by op and path. This prevents processing unnecessary patches in a single PATCH request.
*
* @param body JSON patch operation object entries
* @returns deduped JSON patch operation object entries
*/
function dedupeOperationEntries(body: JsonPatchOperationObject[]): JsonPatchOperationObject[] {
const ops = new Map<string, any>();
for (let i = body.length - 1; i >= 0; i--) {
const patch = body[i].operation;
const key = `${patch.op}-${patch.path}`;
if (!ops.has(key)) {
ops.set(key, patch);
} else {
body.splice(i, 1);
}
}
return body;
}
function makeOperationEntry(operation) {
return { operation: operation, timeCompleted: new Date().getTime() };
}