102039: Use patch to delete multiple bitstreams at once

This commit is contained in:
Alexandre Vryghem
2023-05-26 18:15:58 +02:00
parent d2fa8cda6a
commit fb66b5abd6
5 changed files with 130 additions and 29 deletions

View File

@@ -1,13 +1,14 @@
import { TestBed } from '@angular/core/testing';
import { BitstreamDataService } from './bitstream-data.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RequestService } from './request.service';
import { Bitstream } from '../shared/bitstream.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { BitstreamFormatDataService } from './bitstream-format-data.service';
import { of as observableOf } from 'rxjs';
import { Observable, of as observableOf } from 'rxjs';
import { BitstreamFormat } from '../shared/bitstream-format.model';
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
import { PutRequest } from './request.models';
import { PatchRequest, PutRequest } from './request.models';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -15,6 +16,11 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-bu
import { testSearchDataImplementation } from './base/search-data.spec';
import { testPatchDataImplementation } from './base/patch-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import objectContaining = jasmine.objectContaining;
import { RemoteData } from './remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
describe('BitstreamDataService', () => {
let service: BitstreamDataService;
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
let rdbService: RemoteDataBuildService;
const bitstreamFormatHref = 'rest-api/bitstreamformats';
const bitstream = Object.assign(new Bitstream(), {
uuid: 'fake-bitstream',
const bitstream1 = Object.assign(new Bitstream(), {
id: 'fake-bitstream1',
uuid: 'fake-bitstream1',
_links: {
self: { href: 'fake-bitstream-self' }
self: { href: 'fake-bitstream1-self' }
}
});
const bitstream2 = Object.assign(new Bitstream(), {
id: 'fake-bitstream2',
uuid: 'fake-bitstream2',
_links: {
self: { href: 'fake-bitstream2-self' }
}
});
const format = Object.assign(new BitstreamFormat(), {
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
});
rdbService = getMockRemoteDataBuildService();
service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
TestBed.configureTestingModule({
providers: [
{ provide: ObjectCacheService, useValue: objectCache },
{ provide: RequestService, useValue: requestService },
{ provide: HALEndpointService, useValue: halService },
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
{ provide: RemoteDataBuildService, useValue: rdbService },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: NotificationsService, useValue: {} },
],
});
service = TestBed.inject(BitstreamDataService);
});
describe('composition', () => {
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
describe('when updating the bitstream\'s format', () => {
beforeEach(() => {
service.updateFormat(bitstream, format);
service.updateFormat(bitstream1, format);
});
it('should send a put request', () => {
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
});
});
describe('removeMultiple', () => {
function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ..._linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<any>> {
callback();
return;
}
beforeEach(() => {
spyOn(service, 'invalidateByHref');
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]) => mockBuildFromRequestUUIDAndAwait(requestUUID$, callback, ...linksToFollow));
});
it('should be able to 1 bitstream', () => {
service.removeMultiple([bitstream1]);
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
href: `${url}/bitstreams`,
body: [
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
],
} as PatchRequest));
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
});
it('should be able to delete multiple bitstreams', () => {
service.removeMultiple([bitstream1, bitstream2]);
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
href: `${url}/bitstreams`,
body: [
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
{ op: 'remove', path: '/bitstreams/fake-bitstream2' },
],
} as PatchRequest));
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
});
});
});

View File

@@ -1,7 +1,7 @@
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { find, map, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -14,7 +14,7 @@ import { Item } from '../shared/item.model';
import { BundleDataService } from './bundle-data.service';
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data';
import { PutRequest } from './request.models';
import { PatchRequest, PutRequest } from './request.models';
import { RequestService } from './request.service';
import { BitstreamFormatDataService } from './bitstream-format-data.service';
import { BitstreamFormat } from '../shared/bitstream-format.model';
@@ -33,7 +33,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { NoContent } from '../shared/NoContent.model';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { dataService } from './base/data-service.decorator';
import { Operation } from 'fast-json-patch';
import { Operation, RemoveOperation } from 'fast-json-patch';
/**
* A service to retrieve {@link Bitstream}s from the REST API
@@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}
/**
* Delete multiple {@link Bitstream}s at once by sending a PATCH request to the backend
*
* @param bitstreams The bitstreams that should be removed
*/
removeMultiple(bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => {
return {
op: 'remove',
path: `/bitstreams/${bitstream.id}`,
};
});
const requestId: string = this.requestService.generateRequestId();
const hrefObs: Observable<string> = this.halService.getEndpoint(this.linkPath);
hrefObs.pipe(
find((href: string) => hasValue(href)),
).subscribe((href: string) => {
const request = new PatchRequest(requestId, href, operations);
if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive;
}
this.requestService.send(request);
});
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableCombineLatest(bitstreams.map((bitstream: Bitstream) => this.invalidateByHref(bitstream._links.self.href))));
}
}

View File

@@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../shared/testing/utils.test';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
let comp: ItemBitstreamsComponent;
let fixture: ComponentFixture<ItemBitstreamsComponent>;
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
let router: any;
let route: ActivatedRoute;
let notificationsService: NotificationsService;
let bitstreamService: BitstreamDataService;
let bitstreamService: BitstreamDataServiceStub;
let objectCache: ObjectCacheService;
let requestService: RequestService;
let searchConfig: SearchConfigurationService;
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
success: successNotification
}
);
bitstreamService = jasmine.createSpyObj('bitstreamService', {
delete: jasmine.createSpy('delete')
});
bitstreamService = new BitstreamDataServiceStub();
objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove')
});
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
describe('when submit is called', () => {
beforeEach(() => {
spyOn(bitstreamService, 'removeMultiple').and.callThrough();
comp.submit();
});
it('should call delete on the bitstreamService for the marked field', () => {
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
it('should call removeMultiple on the bitstreamService for the marked field', () => {
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
});
it('should not call delete on the bitstreamService for the unmarked field', () => {
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
});
});
@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
comp.dropBitstream(bundle, {
fromIndex: 0,
toIndex: 50,
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
finish: () => {
done();
}

View File

@@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
import { Observable, Subscription, zip as observableZip } from 'rxjs';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router';
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
);
// Send out delete requests for all deleted bitstreams
const removedResponses$ = removedBitstreams$.pipe(
const removedResponses$: Observable<RemoteData<NoContent>> = removedBitstreams$.pipe(
take(1),
switchMap((removedBistreams: Bitstream[]) => {
if (isNotEmpty(removedBistreams)) {
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
} else {
return observableOf(undefined);
}
switchMap((removedBitstreams: Bitstream[]) => {
return this.bitstreamService.removeMultiple(removedBitstreams);
})
);
// Perform the setup actions from above in order and display notifications
removedResponses$.pipe(take(1)).subscribe((responses: RemoteData<NoContent>[]) => {
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
this.submitting = false;
});
}

View File

@@ -0,0 +1,13 @@
import { Bitstream } from '../../core/shared/bitstream.model';
import { Observable, of as observableOf } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { NoContent } from '../../core/shared/NoContent.model';
import { RequestEntryState } from '../../core/data/request-entry-state.model';
export class BitstreamDataServiceStub {
removeMultiple(_bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
return observableOf(new RemoteData(0, 0, 0, RequestEntryState.Success));
}
}