73014: MetadataPatchOperationService create patch in two steps to fix remove operation issue

This commit is contained in:
Kristof De Langhe
2020-09-14 13:34:30 +02:00
parent 1647c95600
commit eee57f5b41
8 changed files with 169 additions and 55 deletions

View File

@@ -11,7 +11,6 @@ import { NgModel } from '@angular/forms';
import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model';
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
import { METADATA_PATCH_OPERATION_SERVICE_TOKEN } from '../../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service';
@Component({ @Component({
// tslint:disable-next-line:component-selector // tslint:disable-next-line:component-selector
@@ -76,7 +75,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* Sends a new change update for this field to the object updates service * Sends a new change update for this field to the object updates service
*/ */
update(ngModel?: NgModel) { update(ngModel?: NgModel) {
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN); this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata));
if (hasValue(ngModel)) { if (hasValue(ngModel)) {
this.checkValidity(ngModel); this.checkValidity(ngModel);
} }
@@ -104,7 +103,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
* Sends a new remove update for this field to the object updates service * Sends a new remove update for this field to the object updates service
*/ */
remove() { remove() {
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN); this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata));
} }
/** /**

View File

@@ -94,14 +94,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
*/ */
add(metadata: MetadatumViewModel = new MetadatumViewModel()) { add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata, METADATA_PATCH_OPERATION_SERVICE_TOKEN); this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
} }
/** /**
* Sends all initial values of this item to the object updates service * Sends all initial values of this item to the object updates service
*/ */
public initializeOriginalFields() { public initializeOriginalFields() {
this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, METADATA_PATCH_OPERATION_SERVICE_TOKEN);
} }
/** /**

View File

@@ -40,7 +40,8 @@ export class InitializeFieldsAction implements Action {
payload: { payload: {
url: string, url: string,
fields: Identifiable[], fields: Identifiable[],
lastModified: Date lastModified: Date,
patchOperationServiceToken?: InjectionToken<PatchOperationService>
}; };
/** /**
@@ -50,16 +51,15 @@ export class InitializeFieldsAction implements Action {
* the unique url of the page for which the fields are being initialized * the unique url of the page for which the fields are being initialized
* @param fields The identifiable fields of which the updates are kept track of * @param fields The identifiable fields of which the updates are kept track of
* @param lastModified The last modified date of the object that belongs to the page * @param lastModified The last modified date of the object that belongs to the page
* @param order A custom order to keep track of objects moving around * @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch
* @param pageSize The page size used to fill empty pages for the custom order
* @param page The first page to populate in the custom order
*/ */
constructor( constructor(
url: string, url: string,
fields: Identifiable[], fields: Identifiable[],
lastModified: Date lastModified: Date,
patchOperationServiceToken?: InjectionToken<PatchOperationService>
) { ) {
this.payload = { url, fields, lastModified }; this.payload = { url, fields, lastModified, patchOperationServiceToken };
} }
} }
@@ -72,7 +72,6 @@ export class AddFieldUpdateAction implements Action {
url: string, url: string,
field: Identifiable, field: Identifiable,
changeType: FieldChangeType, changeType: FieldChangeType,
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>
}; };
/** /**
@@ -86,9 +85,8 @@ export class AddFieldUpdateAction implements Action {
constructor( constructor(
url: string, url: string,
field: Identifiable, field: Identifiable,
changeType: FieldChangeType, changeType: FieldChangeType) {
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) { this.payload = { url, field, changeType };
this.payload = { url, field, changeType, patchOperationServiceToken };
} }
} }

View File

@@ -51,7 +51,6 @@ export interface Identifiable {
export interface FieldUpdate { export interface FieldUpdate {
field: Identifiable, field: Identifiable,
changeType: FieldChangeType, changeType: FieldChangeType,
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>
} }
/** /**
@@ -92,6 +91,7 @@ export interface ObjectUpdatesEntry {
fieldUpdates: FieldUpdates; fieldUpdates: FieldUpdates;
virtualMetadataSources: VirtualMetadataSources; virtualMetadataSources: VirtualMetadataSources;
lastModified: Date; lastModified: Date;
patchOperationServiceToken?: InjectionToken<PatchOperationService>;
} }
/** /**
@@ -166,6 +166,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
const url: string = action.payload.url; const url: string = action.payload.url;
const fields: Identifiable[] = action.payload.fields; const fields: Identifiable[] = action.payload.fields;
const lastModifiedServer: Date = action.payload.lastModified; const lastModifiedServer: Date = action.payload.lastModified;
const patchOperationServiceToken: InjectionToken<PatchOperationService> = action.payload.patchOperationServiceToken;
const fieldStates = createInitialFieldStates(fields); const fieldStates = createInitialFieldStates(fields);
const newPageState = Object.assign( const newPageState = Object.assign(
{}, {},
@@ -173,7 +174,8 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
{ fieldStates: fieldStates }, { fieldStates: fieldStates },
{ fieldUpdates: {} }, { fieldUpdates: {} },
{ virtualMetadataSources: {} }, { virtualMetadataSources: {} },
{ lastModified: lastModifiedServer } { lastModified: lastModifiedServer },
{ patchOperationServiceToken }
); );
return Object.assign({}, state, { [url]: newPageState }); return Object.assign({}, state, { [url]: newPageState });
} }
@@ -187,7 +189,6 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
const url: string = action.payload.url; const url: string = action.payload.url;
const field: Identifiable = action.payload.field; const field: Identifiable = action.payload.field;
const changeType: FieldChangeType = action.payload.changeType; const changeType: FieldChangeType = action.payload.changeType;
const patchOperationServiceToken: InjectionToken<PatchOperationService<Identifiable>> = action.payload.patchOperationServiceToken;
const pageState: ObjectUpdatesEntry = state[url] || {}; const pageState: ObjectUpdatesEntry = state[url] || {};
let states = pageState.fieldStates; let states = pageState.fieldStates;
@@ -198,7 +199,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {}; let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {};
const newChangeType = determineChangeType(fieldUpdate.changeType, changeType); const newChangeType = determineChangeType(fieldUpdate.changeType, changeType);
fieldUpdate = Object.assign({}, { field, changeType: newChangeType, patchOperationServiceToken }); fieldUpdate = Object.assign({}, { field, changeType: newChangeType });
const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate }); const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate });

View File

@@ -59,9 +59,10 @@ export class ObjectUpdatesService {
* @param url The page's URL for which the changes are being mapped * @param url The page's URL for which the changes are being mapped
* @param fields The initial fields for the page's object * @param fields The initial fields for the page's object
* @param lastModified The date the object was last modified * @param lastModified The date the object was last modified
* @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch
*/ */
initialize(url, fields: Identifiable[], lastModified: Date): void { initialize(url, fields: Identifiable[], lastModified: Date, patchOperationServiceToken?: InjectionToken<PatchOperationService>): void {
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified)); this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, patchOperationServiceToken));
} }
/** /**
@@ -70,8 +71,8 @@ export class ObjectUpdatesService {
* @param field An updated field for the page's object * @param field An updated field for the page's object
* @param changeType The last type of change applied to this field * @param changeType The last type of change applied to this field
*/ */
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) { private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) {
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType, patchOperationServiceToken)) this.store.dispatch(new AddFieldUpdateAction(url, field, changeType))
} }
/** /**
@@ -188,8 +189,8 @@ export class ObjectUpdatesService {
* @param url The page's URL for which the changes are saved * @param url The page's URL for which the changes are saved
* @param field An updated field for the page's object * @param field An updated field for the page's object
*/ */
saveAddFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) { saveAddFieldUpdate(url: string, field: Identifiable) {
this.saveFieldUpdate(url, field, FieldChangeType.ADD, patchOperationServiceToken); this.saveFieldUpdate(url, field, FieldChangeType.ADD);
} }
/** /**
@@ -197,8 +198,8 @@ export class ObjectUpdatesService {
* @param url The page's URL for which the changes are saved * @param url The page's URL for which the changes are saved
* @param field An updated field for the page's object * @param field An updated field for the page's object
*/ */
saveRemoveFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) { saveRemoveFieldUpdate(url: string, field: Identifiable) {
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE, patchOperationServiceToken); this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
} }
/** /**
@@ -206,8 +207,8 @@ export class ObjectUpdatesService {
* @param url The page's URL for which the changes are saved * @param url The page's URL for which the changes are saved
* @param field An updated field for the page's object * @param field An updated field for the page's object
*/ */
saveChangeFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) { saveChangeFieldUpdate(url: string, field: Identifiable) {
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE, patchOperationServiceToken); this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
} }
/** /**
@@ -345,18 +346,17 @@ export class ObjectUpdatesService {
/** /**
* Create a patch from the current object-updates state * Create a patch from the current object-updates state
* The {@link ObjectUpdatesEntry} should contain a patchOperationServiceToken, in order to define how a patch should
* be created. If it doesn't, an empty patch will be returned.
* @param url The URL of the page for which the patch should be created * @param url The URL of the page for which the patch should be created
*/ */
createPatch(url: string): Observable<Operation[]> { createPatch(url: string): Observable<Operation[]> {
return this.getObjectEntry(url).pipe( return this.getObjectEntry(url).pipe(
map((entry) => { map((entry) => {
const patch = []; let patch = [];
Object.keys(entry.fieldUpdates).forEach((uuid) => { if (hasValue(entry.patchOperationServiceToken)) {
const update = entry.fieldUpdates[uuid]; patch = this.injector.get(entry.patchOperationServiceToken).fieldUpdatesToPatchOperations(entry.fieldUpdates);
if (hasValue(update.patchOperationServiceToken)) { }
patch.push(this.injector.get(update.patchOperationServiceToken).fieldUpdateToPatchOperation(update));
}
});
return patch; return patch;
}) })
); );

View File

@@ -0,0 +1,33 @@
import { Operation } from 'fast-json-patch';
import { hasValue } from '../../../../shared/empty.util';
/**
* Wrapper object for metadata patch Operations
* It contains the operation type, metadata field, metadata place and patch value, as well as a method to transform it
* into a fast-json-patch Operation.
*/
export class MetadataPatchOperation {
op: string;
field: string;
place: number;
value: any;
constructor(op: string, field: string, place?: number, value?: any) {
this.op = op;
this.field = field;
this.place = place;
this.value = value;
}
/**
* Transform the MetadataPatchOperation into a fast-json-patch Operation by constructing its path and other properties
* using the information provided.
*/
toOperation(): Operation {
let path = `/metadata/${this.field}`;
if (hasValue(this.place)) {
path += `/${this.place}`;
}
return { op: this.op as any, path, value: this.value };
}
}

View File

@@ -1,29 +1,103 @@
import { PatchOperationService } from './patch-operation.service'; import { PatchOperationService } from './patch-operation.service';
import { MetadataValue, MetadatumViewModel } from '../../../shared/metadata.models'; import { MetadatumViewModel } from '../../../shared/metadata.models';
import { FieldUpdate } from '../object-updates.reducer'; import { FieldUpdates } from '../object-updates.reducer';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { FieldChangeType } from '../object-updates.actions'; import { FieldChangeType } from '../object-updates.actions';
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { MetadataPatchOperation } from './metadata-patch-operation.model';
import { hasValue } from '../../../../shared/empty.util';
/**
* Token to use for injecting this service anywhere you want
* This token can used to store in the object-updates store
*/
export const METADATA_PATCH_OPERATION_SERVICE_TOKEN = new InjectionToken<MetadataPatchOperationService>('MetadataPatchOperationService', { export const METADATA_PATCH_OPERATION_SERVICE_TOKEN = new InjectionToken<MetadataPatchOperationService>('MetadataPatchOperationService', {
providedIn: 'root', providedIn: 'root',
factory: () => new MetadataPatchOperationService(), factory: () => new MetadataPatchOperationService(),
}); });
export class MetadataPatchOperationService implements PatchOperationService<MetadatumViewModel> { /**
fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation { * Service transforming {@link FieldUpdates} into {@link Operation}s for metadata values
const metadatum = fieldUpdate.field as MetadatumViewModel; * This expects the fields within every {@link FieldUpdate} to be {@link MetadatumViewModel}s
const path = `/metadata/${metadatum.key}`; */
const val = { export class MetadataPatchOperationService implements PatchOperationService {
value: metadatum.value,
language: metadatum.language
}
switch (fieldUpdate.changeType) { /**
case FieldChangeType.ADD: return { op: 'add', path, value: [ val ] }; * Transform a {@link FieldUpdates} object into an array of fast-json-patch Operations for metadata values
case FieldChangeType.REMOVE: return { op: 'remove', path: `${path}/${metadatum.place}` }; * This method first creates an array of {@link MetadataPatchOperation} wrapper operations, which are then
case FieldChangeType.UPDATE: return { op: 'replace', path: `${path}/${metadatum.place}`, value: val }; * iterated over to create the actual patch operations. While iterating, it has the ability to check for previous
default: return undefined; * operations that would modify the operation's position and act accordingly.
} * @param fieldUpdates
*/
fieldUpdatesToPatchOperations(fieldUpdates: FieldUpdates): Operation[] {
const metadataPatch = this.fieldUpdatesToMetadataPatchOperations(fieldUpdates);
// This map stores what metadata fields had a value deleted at which places
// This is used to modify the place of operations to match previous operations
const metadataRemoveMap = new Map<string, number[]>();
const patch = [];
metadataPatch.forEach((operation) => {
// If this operation is removing or editing an existing value, first check the map for previous operations
// If the map contains remove operations before this operation's place, lower the place by 1 for each
if ((operation.op === 'remove' || operation.op === 'replace') && hasValue(operation.place)) {
if (metadataRemoveMap.has(operation.field)) {
metadataRemoveMap.get(operation.field).forEach((index) => {
if (index < operation.place) {
operation.place--;
}
});
}
}
// If this is a remove operation, add its (updated) place to the map, so we can adjust following operations accordingly
if (operation.op === 'remove' && hasValue(operation.place)) {
if (!metadataRemoveMap.has(operation.field)) {
metadataRemoveMap.set(operation.field, []);
}
metadataRemoveMap.get(operation.field).push(operation.place);
}
// Transform the updated operation into a fast-json-patch Operation and add it to the patch
patch.push(operation.toOperation());
});
return patch;
} }
/**
* Transform a {@link FieldUpdates} object into an array of {@link MetadataPatchOperation} wrapper objects
* These wrapper objects contain detailed information about the patch operation that needs to be creates for each update
* This information can then be modified before creating the actual patch
* @param fieldUpdates
*/
fieldUpdatesToMetadataPatchOperations(fieldUpdates: FieldUpdates): MetadataPatchOperation[] {
const metadataPatch = [];
Object.keys(fieldUpdates).forEach((uuid) => {
const update = fieldUpdates[uuid];
const metadatum = update.field as MetadatumViewModel;
const val = {
value: metadatum.value,
language: metadatum.language
}
let operation: MetadataPatchOperation;
switch (update.changeType) {
case FieldChangeType.ADD:
operation = new MetadataPatchOperation('add', metadatum.key, undefined, [ val ]);
break;
case FieldChangeType.REMOVE:
operation = new MetadataPatchOperation('remove', metadatum.key, metadatum.place);
break;
case FieldChangeType.UPDATE:
operation = new MetadataPatchOperation('replace', metadatum.key, metadatum.place, val);
break;
}
metadataPatch.push(operation);
});
return metadataPatch;
}
} }

View File

@@ -1,6 +1,15 @@
import { FieldUpdate, Identifiable } from '../object-updates.reducer'; import { FieldUpdates } from '../object-updates.reducer';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
export interface PatchOperationService<T extends Identifiable> { /**
fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation; * Interface for a service dealing with the transformations of patch operations from the object-updates store
* The implementations of this service know how to deal with the fields of a FieldUpdate and how to transform them
* into patch Operations.
*/
export interface PatchOperationService {
/**
* Transform a {@link FieldUpdates} object into an array of fast-json-patch Operations
* @param fieldUpdates
*/
fieldUpdatesToPatchOperations(fieldUpdates: FieldUpdates): Operation[];
} }