Skip to content

Commit 5dda198

Browse files
authored
Merge pull request DSpace#2280 from atmire/w2p-102039_Multiple_Bitstream_deletion_endpoint
Fix multiple bitstream deletion not working
2 parents 21c7f43 + fb66b5a commit 5dda198

5 files changed

Lines changed: 130 additions & 29 deletions

File tree

src/app/core/data/bitstream-data.service.spec.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1+
import { TestBed } from '@angular/core/testing';
12
import { BitstreamDataService } from './bitstream-data.service';
23
import { ObjectCacheService } from '../cache/object-cache.service';
34
import { RequestService } from './request.service';
45
import { Bitstream } from '../shared/bitstream.model';
56
import { HALEndpointService } from '../shared/hal-endpoint.service';
67
import { BitstreamFormatDataService } from './bitstream-format-data.service';
7-
import { of as observableOf } from 'rxjs';
8+
import { Observable, of as observableOf } from 'rxjs';
89
import { BitstreamFormat } from '../shared/bitstream-format.model';
910
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
10-
import { PutRequest } from './request.models';
11+
import { PatchRequest, PutRequest } from './request.models';
1112
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
1213
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
1314
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
1415
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
1516
import { testSearchDataImplementation } from './base/search-data.spec';
1617
import { testPatchDataImplementation } from './base/patch-data.spec';
1718
import { testDeleteDataImplementation } from './base/delete-data.spec';
19+
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
20+
import { NotificationsService } from '../../shared/notifications/notifications.service';
21+
import objectContaining = jasmine.objectContaining;
22+
import { RemoteData } from './remote-data';
23+
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
1824

1925
describe('BitstreamDataService', () => {
2026
let service: BitstreamDataService;
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
2531
let rdbService: RemoteDataBuildService;
2632
const bitstreamFormatHref = 'rest-api/bitstreamformats';
2733

28-
const bitstream = Object.assign(new Bitstream(), {
29-
uuid: 'fake-bitstream',
34+
const bitstream1 = Object.assign(new Bitstream(), {
35+
id: 'fake-bitstream1',
36+
uuid: 'fake-bitstream1',
3037
_links: {
31-
self: { href: 'fake-bitstream-self' }
38+
self: { href: 'fake-bitstream1-self' }
39+
}
40+
});
41+
const bitstream2 = Object.assign(new Bitstream(), {
42+
id: 'fake-bitstream2',
43+
uuid: 'fake-bitstream2',
44+
_links: {
45+
self: { href: 'fake-bitstream2-self' }
3246
}
3347
});
3448
const format = Object.assign(new BitstreamFormat(), {
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
5064
});
5165
rdbService = getMockRemoteDataBuildService();
5266

53-
service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
67+
TestBed.configureTestingModule({
68+
providers: [
69+
{ provide: ObjectCacheService, useValue: objectCache },
70+
{ provide: RequestService, useValue: requestService },
71+
{ provide: HALEndpointService, useValue: halService },
72+
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
73+
{ provide: RemoteDataBuildService, useValue: rdbService },
74+
{ provide: DSOChangeAnalyzer, useValue: {} },
75+
{ provide: NotificationsService, useValue: {} },
76+
],
77+
});
78+
service = TestBed.inject(BitstreamDataService);
5479
});
5580

5681
describe('composition', () => {
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
6287

6388
describe('when updating the bitstream\'s format', () => {
6489
beforeEach(() => {
65-
service.updateFormat(bitstream, format);
90+
service.updateFormat(bitstream1, format);
6691
});
6792

6893
it('should send a put request', () => {
6994
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
7095
});
7196
});
97+
98+
describe('removeMultiple', () => {
99+
function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ..._linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<any>> {
100+
callback();
101+
return;
102+
}
103+
104+
beforeEach(() => {
105+
spyOn(service, 'invalidateByHref');
106+
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]) => mockBuildFromRequestUUIDAndAwait(requestUUID$, callback, ...linksToFollow));
107+
});
108+
109+
it('should be able to 1 bitstream', () => {
110+
service.removeMultiple([bitstream1]);
111+
112+
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
113+
href: `${url}/bitstreams`,
114+
body: [
115+
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
116+
],
117+
} as PatchRequest));
118+
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
119+
});
120+
121+
it('should be able to delete multiple bitstreams', () => {
122+
service.removeMultiple([bitstream1, bitstream2]);
123+
124+
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
125+
href: `${url}/bitstreams`,
126+
body: [
127+
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
128+
{ op: 'remove', path: '/bitstreams/fake-bitstream2' },
129+
],
130+
} as PatchRequest));
131+
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
132+
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
133+
});
134+
});
72135
});

src/app/core/data/bitstream-data.service.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HttpHeaders } from '@angular/common/http';
22
import { Injectable } from '@angular/core';
33
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
4-
import { map, switchMap, take } from 'rxjs/operators';
4+
import { find, map, switchMap, take } from 'rxjs/operators';
55
import { hasValue } from '../../shared/empty.util';
66
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
77
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -14,7 +14,7 @@ import { Item } from '../shared/item.model';
1414
import { BundleDataService } from './bundle-data.service';
1515
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
1616
import { RemoteData } from './remote-data';
17-
import { PutRequest } from './request.models';
17+
import { PatchRequest, PutRequest } from './request.models';
1818
import { RequestService } from './request.service';
1919
import { BitstreamFormatDataService } from './bitstream-format-data.service';
2020
import { BitstreamFormat } from '../shared/bitstream-format.model';
@@ -33,7 +33,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
3333
import { NoContent } from '../shared/NoContent.model';
3434
import { IdentifiableDataService } from './base/identifiable-data.service';
3535
import { dataService } from './base/data-service.decorator';
36-
import { Operation } from 'fast-json-patch';
36+
import { Operation, RemoveOperation } from 'fast-json-patch';
3737

3838
/**
3939
* A service to retrieve {@link Bitstream}s from the REST API
@@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
277277
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
278278
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
279279
}
280+
281+
/**
282+
* Delete multiple {@link Bitstream}s at once by sending a PATCH request to the backend
283+
*
284+
* @param bitstreams The bitstreams that should be removed
285+
*/
286+
removeMultiple(bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
287+
const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => {
288+
return {
289+
op: 'remove',
290+
path: `/bitstreams/${bitstream.id}`,
291+
};
292+
});
293+
const requestId: string = this.requestService.generateRequestId();
294+
295+
const hrefObs: Observable<string> = this.halService.getEndpoint(this.linkPath);
296+
297+
hrefObs.pipe(
298+
find((href: string) => hasValue(href)),
299+
).subscribe((href: string) => {
300+
const request = new PatchRequest(requestId, href, operations);
301+
if (hasValue(this.responseMsToLive)) {
302+
request.responseMsToLive = this.responseMsToLive;
303+
}
304+
this.requestService.send(request);
305+
});
306+
307+
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableCombineLatest(bitstreams.map((bitstream: Bitstream) => this.invalidateByHref(bitstream._links.self.href))));
308+
}
309+
280310
}

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc
2525
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
2626
import { createPaginatedList } from '../../../shared/testing/utils.test';
2727
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
28+
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
2829

2930
let comp: ItemBitstreamsComponent;
3031
let fixture: ComponentFixture<ItemBitstreamsComponent>;
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
7172
let router: any;
7273
let route: ActivatedRoute;
7374
let notificationsService: NotificationsService;
74-
let bitstreamService: BitstreamDataService;
75+
let bitstreamService: BitstreamDataServiceStub;
7576
let objectCache: ObjectCacheService;
7677
let requestService: RequestService;
7778
let searchConfig: SearchConfigurationService;
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
112113
success: successNotification
113114
}
114115
);
115-
bitstreamService = jasmine.createSpyObj('bitstreamService', {
116-
delete: jasmine.createSpy('delete')
117-
});
116+
bitstreamService = new BitstreamDataServiceStub();
118117
objectCache = jasmine.createSpyObj('objectCache', {
119118
remove: jasmine.createSpy('remove')
120119
});
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
179178

180179
describe('when submit is called', () => {
181180
beforeEach(() => {
181+
spyOn(bitstreamService, 'removeMultiple').and.callThrough();
182182
comp.submit();
183183
});
184184

185-
it('should call delete on the bitstreamService for the marked field', () => {
186-
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
185+
it('should call removeMultiple on the bitstreamService for the marked field', () => {
186+
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
187187
});
188188

189-
it('should not call delete on the bitstreamService for the unmarked field', () => {
190-
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
189+
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
190+
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
191191
});
192192
});
193193

@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
210210
comp.dropBitstream(bundle, {
211211
fromIndex: 0,
212212
toIndex: 50,
213-
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
214213
finish: () => {
215214
done();
216215
}

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
22
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
33
import { filter, map, switchMap, take } from 'rxjs/operators';
4-
import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
4+
import { Observable, Subscription, zip as observableZip } from 'rxjs';
55
import { ItemDataService } from '../../../core/data/item-data.service';
66
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
77
import { ActivatedRoute, Router } from '@angular/router';
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
133133
);
134134

135135
// Send out delete requests for all deleted bitstreams
136-
const removedResponses$ = removedBitstreams$.pipe(
136+
const removedResponses$: Observable<RemoteData<NoContent>> = removedBitstreams$.pipe(
137137
take(1),
138-
switchMap((removedBistreams: Bitstream[]) => {
139-
if (isNotEmpty(removedBistreams)) {
140-
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
141-
} else {
142-
return observableOf(undefined);
143-
}
138+
switchMap((removedBitstreams: Bitstream[]) => {
139+
return this.bitstreamService.removeMultiple(removedBitstreams);
144140
})
145141
);
146142

147143
// Perform the setup actions from above in order and display notifications
148-
removedResponses$.pipe(take(1)).subscribe((responses: RemoteData<NoContent>[]) => {
149-
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
144+
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
145+
this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
150146
this.submitting = false;
151147
});
152148
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Bitstream } from '../../core/shared/bitstream.model';
2+
import { Observable, of as observableOf } from 'rxjs';
3+
import { RemoteData } from '../../core/data/remote-data';
4+
import { NoContent } from '../../core/shared/NoContent.model';
5+
import { RequestEntryState } from '../../core/data/request-entry-state.model';
6+
7+
export class BitstreamDataServiceStub {
8+
9+
removeMultiple(_bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
10+
return observableOf(new RemoteData(0, 0, 0, RequestEntryState.Success));
11+
}
12+
13+
}

0 commit comments

Comments
 (0)