mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
65272: Object update PUT to PATCH requests + option to add ignored metadata fields to update
This commit is contained in:
@@ -15,12 +15,12 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { UpdateDataService } from '../../../core/data/update-data.service';
|
import { UpdateDataService } from '../../../core/data/update-data.service';
|
||||||
import { hasNoValue } from '../../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -115,13 +115,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
first(),
|
first(),
|
||||||
switchMap((metadata: MetadatumViewModel[]) => {
|
switchMap((metadata: MetadatumViewModel[]) => {
|
||||||
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) });
|
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) });
|
||||||
return this.updateService.update(updatedItem);
|
return this.updateService.update(updatedItem, ['relation.*', 'relationship.*']);
|
||||||
}),
|
}),
|
||||||
tap(() => this.updateService.commitUpdates()),
|
tap(() => this.updateService.commitUpdates()),
|
||||||
getSucceededRemoteData()
|
getSucceededRemoteData()
|
||||||
).subscribe(
|
).subscribe(
|
||||||
(rd: RemoteData<Item>) => {
|
(rd: RemoteData<Item>) => {
|
||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
|
this.checkAndFixMetadataUUIDs();
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
|
||||||
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
@@ -143,7 +144,21 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the object's metadata as a list and exclude relationship metadata
|
||||||
|
*/
|
||||||
getMetadataAsListExcludingRelationships(): MetadatumViewModel[] {
|
getMetadataAsListExcludingRelationships(): MetadatumViewModel[] {
|
||||||
return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.'));
|
return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
||||||
|
*/
|
||||||
|
checkAndFixMetadataUUIDs() {
|
||||||
|
const metadata = cloneDeep(this.item.metadata);
|
||||||
|
Object.keys(this.item.metadata).forEach((key: string) => {
|
||||||
|
metadata[key] = this.item.metadata[key].map((value) => hasValue(value.uuid) ? value : Object.assign(new MetadataValue(), value));
|
||||||
|
});
|
||||||
|
this.item.metadata = metadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
21
src/app/core/cache/server-sync-buffer.effects.ts
vendored
21
src/app/core/cache/server-sync-buffer.effects.ts
vendored
@@ -15,7 +15,7 @@ import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/s
|
|||||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { PutRequest } from '../data/request.models';
|
import { PatchRequest, PutRequest } from '../data/request.models';
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
@@ -23,6 +23,8 @@ import { GenericConstructor } from '../shared/generic-constructor';
|
|||||||
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { ObjectCacheEntry } from './object-cache.reducer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerSyncBufferEffects {
|
export class ServerSyncBufferEffects {
|
||||||
@@ -96,15 +98,18 @@ export class ServerSyncBufferEffects {
|
|||||||
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||||
*/
|
*/
|
||||||
private applyPatch(href: string): Observable<Action> {
|
private applyPatch(href: string): Observable<Action> {
|
||||||
const patchObject = this.objectCache.getObjectBySelfLink(href).pipe(take(1));
|
const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1));
|
||||||
|
|
||||||
return patchObject.pipe(
|
return patchObject.pipe(
|
||||||
map((object) => {
|
map((entry: ObjectCacheEntry) => {
|
||||||
const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
if (isNotEmpty(entry.patches)) {
|
||||||
|
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||||
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
const objectPatch = flatPatch.filter((op: Operation) => op.path.startsWith('/metadata'));
|
||||||
|
if (isNotEmpty(objectPatch)) {
|
||||||
return new ApplyPatchObjectCacheAction(href)
|
this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, objectPatch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ApplyPatchObjectCacheAction(href);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -262,13 +262,24 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
|||||||
* The patch is derived from the differences between the given object and its version in the object cache
|
* The patch is derived from the differences between the given object and its version in the object cache
|
||||||
* @param {DSpaceObject} object The given object
|
* @param {DSpaceObject} object The given object
|
||||||
*/
|
*/
|
||||||
update(object: T): Observable<RemoteData<T>> {
|
update(object: T, ignoreMetadataFields: string[] = []): Observable<RemoteData<T>> {
|
||||||
|
const ignoreMetadataFieldsPrefix = ignoreMetadataFields.map((field) => field.indexOf('*') > -1 ? field.slice(0, field.indexOf('*')) : field);
|
||||||
const oldVersion$ = this.findByHref(object.self);
|
const oldVersion$ = this.findByHref(object.self);
|
||||||
return oldVersion$.pipe(
|
return oldVersion$.pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
mergeMap((oldVersion: T) => {
|
mergeMap((oldVersion: T) => {
|
||||||
const operations = this.comparator.diff(oldVersion, object);
|
// Fetch operations from difference between old version and new version
|
||||||
|
// Filter out any metadata operations for a field specified under ignoreMetadataFields
|
||||||
|
const operations = this.comparator.diff(oldVersion, object).filter((operation) => {
|
||||||
|
let ignoredFieldFound = false;
|
||||||
|
ignoreMetadataFieldsPrefix.forEach((fieldPrefix) => {
|
||||||
|
if (operation.path.indexOf('/metadata/' + fieldPrefix) > -1) {
|
||||||
|
ignoredFieldFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return !ignoredFieldFound;
|
||||||
|
});
|
||||||
if (isNotEmpty(operations)) {
|
if (isNotEmpty(operations)) {
|
||||||
this.objectCache.addPatch(object.self, operations);
|
this.objectCache.addPatch(object.self, operations);
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,8 @@ import { ChangeAnalyzer } from './change-analyzer';
|
|||||||
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { MetadataMap } from '../shared/metadata.models';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to determine what differs between two
|
* A class to determine what differs between two
|
||||||
@@ -22,6 +24,21 @@ export class DSOChangeAnalyzer<T extends DSpaceObject> implements ChangeAnalyzer
|
|||||||
* The second object to compare
|
* The second object to compare
|
||||||
*/
|
*/
|
||||||
diff(object1: T | NormalizedDSpaceObject<T>, object2: T | NormalizedDSpaceObject<T>): Operation[] {
|
diff(object1: T | NormalizedDSpaceObject<T>, object2: T | NormalizedDSpaceObject<T>): Operation[] {
|
||||||
return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
return compare(this.filterUUIDsFromMetadata(object1.metadata), this.filterUUIDsFromMetadata(object2.metadata))
|
||||||
|
.map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the UUIDs out of a MetadataMap
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
filterUUIDsFromMetadata(metadata: MetadataMap): MetadataMap {
|
||||||
|
const result = cloneDeep(metadata);
|
||||||
|
for (const key of Object.keys(result)) {
|
||||||
|
for (const metadataValue of result[key]) {
|
||||||
|
metadataValue.uuid = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -158,8 +158,8 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
|||||||
/**
|
/**
|
||||||
* Add a new patch to the object cache
|
* Add a new patch to the object cache
|
||||||
*/
|
*/
|
||||||
update(object: Item): Observable<RemoteData<Item>> {
|
update(object: Item, ignoreMetadataFields: string[] = []): Observable<RemoteData<Item>> {
|
||||||
return this.dataService.update(object);
|
return this.dataService.update(object, ignoreMetadataFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -118,6 +118,8 @@ export class HeadRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PatchRequest extends RestRequest {
|
export class PatchRequest extends RestRequest {
|
||||||
|
public responseMsToLive = 60 * 15 * 1000;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
|
@@ -6,6 +6,6 @@ import { RestRequestMethod } from './rest-request-method';
|
|||||||
* Represents a data service to update a given object
|
* Represents a data service to update a given object
|
||||||
*/
|
*/
|
||||||
export interface UpdateDataService<T> {
|
export interface UpdateDataService<T> {
|
||||||
update(object: T): Observable<RemoteData<T>>;
|
update(object: T, ignoreMetadataFields?: string[]): Observable<RemoteData<T>>;
|
||||||
commitUpdates(method?: RestRequestMethod);
|
commitUpdates(method?: RestRequestMethod);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user