94207: Extract 'wait for invalidation' pattern into a new method

This commit is contained in:
Yury Bondarenko
2022-09-08 12:10:41 +02:00
parent 030b1c33db
commit f8404c540e
10 changed files with 217 additions and 288 deletions

View File

@@ -1,4 +1,4 @@
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model';
import { Item } from '../../shared/item.model'; import { Item } from '../../shared/item.model';
import { PageInfo } from '../../shared/page-info.model'; import { PageInfo } from '../../shared/page-info.model';
@@ -19,6 +19,8 @@ import { HALLink } from '../../shared/hal-link.model';
import { RequestEntryState } from '../../data/request-entry-state.model'; import { RequestEntryState } from '../../data/request-entry-state.model';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { fakeAsync, tick } from '@angular/core/testing';
describe('RemoteDataBuildService', () => { describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService; let service: RemoteDataBuildService;
@@ -763,4 +765,95 @@ describe('RemoteDataBuildService', () => {
}); });
}); });
}); });
describe('buildFromRequestUUIDAndAwait', () => {
let testScheduler;
let callback: jasmine.Spy;
let buildFromRequestUUIDSpy;
const BOOLEAN = { t: true, f: false };
const MOCK_PENDING_RD = createPendingRemoteDataObject();
const MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
const MOCK_FAILED_RD = createFailedRemoteDataObject('failed');
const RDs = {
p: MOCK_PENDING_RD,
s: MOCK_SUCCEEDED_RD,
f: MOCK_FAILED_RD,
};
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
callback = jasmine.createSpy('callback');
callback.and.returnValue(observableOf(undefined));
buildFromRequestUUIDSpy = spyOn(service, 'buildFromRequestUUID').and.callThrough();
});
it('should patch through href & followLinks to buildFromRequestUUID', () => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback, ...linksToFollow);
expect(buildFromRequestUUIDSpy).toHaveBeenCalledWith('some-href', ...linksToFollow);
});
it('should trigger the callback on successful RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD);
expect(callback).toHaveBeenCalled();
done();
});
});
it('should trigger the callback on successful RD even if nothing subscribes to the returned Observable', fakeAsync(() => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback);
tick();
expect(callback).toHaveBeenCalled();
}));
it('should not trigger the callback on pending RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_PENDING_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_PENDING_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should not trigger the callback on failed RD', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
service.buildFromRequestUUIDAndAwait('some-href', callback).subscribe(rd => {
expect(rd).toBe(MOCK_FAILED_RD);
expect(callback).not.toHaveBeenCalled();
done();
});
});
it('should only emit after the callback is done', () => {
testScheduler.run(({ cold: tsCold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
tsCold('-p----s', RDs)
);
callback.and.returnValue(
tsCold(' --t', BOOLEAN)
);
const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback);
expectObservable(done$).toBe(
' -p------s', RDs // resulting duration between pending & successful includes the callback
);
});
});
});
}); });

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
AsyncSubject,
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
Observable, Observable,
of as observableOf, of as observableOf,
@@ -24,6 +25,7 @@ import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-ent
import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators'; import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { ResponseState } from '../../data/response-state.model'; import { ResponseState } from '../../data/response-state.model';
import { getFirstCompletedRemoteData } from '../../shared/operators';
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
@@ -188,6 +190,49 @@ export class RemoteDataBuildService {
return this.toRemoteDataObservable<T>(requestEntry$, payload$); return this.toRemoteDataObservable<T>(requestEntry$, payload$);
} }
/**
* Creates a {@link RemoteData} object for a rest request and its response
* and emits it only after the callback function is completed.
*
* @param requestUUID$ The UUID of the request we want to retrieve
* @param callback A function that returns an Observable. It will only be called once the request has succeeded.
* Then, the response will only be emitted after this callback function has emitted.
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/
buildFromRequestUUIDAndAwait<T>(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<T>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<T>> {
const response$ = this.buildFromRequestUUID(requestUUID$, ...linksToFollow);
const callbackDone$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, execute the callback
return callback(rd);
} else {
// otherwise, emit right away so the subscription doesn't stick around
return [true];
}
}),
).subscribe(() => {
callbackDone$.next(true);
callbackDone$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
// if the request succeeded, wait for the callback to finish
return callbackDone$.pipe(
map(() => rd),
);
} else {
return [rd];
}
})
);
}
/** /**
* Creates a {@link RemoteData} object for a rest request and its response * Creates a {@link RemoteData} object for a rest request and its response
* *

View File

@@ -59,7 +59,8 @@ describe('BitstreamFormatDataService', () => {
function initTestService(halService) { function initTestService(halService) {
rd = createSuccessfulRemoteDataObject({}); rd = createSuccessfulRemoteDataObject({});
rdbService = jasmine.createSpyObj('rdbService', { rdbService = jasmine.createSpyObj('rdbService', {
buildFromRequestUUID: observableOf(rd) buildFromRequestUUID: observableOf(rd),
buildFromRequestUUIDAndAwait: observableOf(rd),
}); });
return new BitstreamFormatDataService( return new BitstreamFormatDataService(

View File

@@ -920,16 +920,16 @@ describe('DataService', () => {
let MOCK_SUCCEEDED_RD; let MOCK_SUCCEEDED_RD;
let MOCK_FAILED_RD; let MOCK_FAILED_RD;
let invalidateByHrefSpy: jasmine.Spy; let buildFromRequestUUIDAndAwaitSpy: jasmine.Spy;
let buildFromRequestUUIDSpy: jasmine.Spy;
let getIDHrefObsSpy: jasmine.Spy; let getIDHrefObsSpy: jasmine.Spy;
let deleteByHrefSpy: jasmine.Spy; let deleteByHrefSpy: jasmine.Spy;
let invalidateByHrefSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); buildFromRequestUUIDAndAwaitSpy = spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough();
getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough(); getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough();
deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough(); deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough();
invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true));
MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({}); MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({});
MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong'); MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong');
@@ -937,7 +937,7 @@ describe('DataService', () => {
it('should retrieve href by ID and call deleteByHref', () => { it('should retrieve href by ID and call deleteByHref', () => {
getIDHrefObsSpy.and.returnValue(observableOf('some-href')); getIDHrefObsSpy.and.returnValue(observableOf('some-href'));
buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({})); buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => { service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => {
expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id'); expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id');
@@ -946,65 +946,27 @@ describe('DataService', () => {
}); });
describe('deleteByHref', () => { describe('deleteByHref', () => {
it('should call invalidateByHref if the DELETE request succeeds', (done) => { beforeEach(() => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); buildFromRequestUUIDAndAwaitSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
service.deleteByHref('some-href').subscribe(rd => {
expect(rd).toBe(MOCK_SUCCEEDED_RD);
expect(invalidateByHrefSpy).toHaveBeenCalled();
done();
});
}); });
it('should call invalidateByHref even if not subscribing to returned Observable', fakeAsync(() => { it('should send a DELETE request', () => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); service.deleteByHref('https://somewhere.org/something/123', ['things', 'stuff']);
service.deleteByHref('some-href'); expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
tick(); method: 'DELETE',
href: 'https://somewhere.org/something/123?copyVirtualMetadata=things&copyVirtualMetadata=stuff'
expect(invalidateByHrefSpy).toHaveBeenCalled(); }));
}));
it('should not call invalidateByHref if the DELETE request fails', (done) => {
buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_FAILED_RD));
service.deleteByHref('some-href').subscribe(rd => {
expect(rd).toBe(MOCK_FAILED_RD);
expect(invalidateByHrefSpy).not.toHaveBeenCalled();
done();
});
}); });
it('should wait for invalidateByHref before emitting', () => { it('should retrieve response via buildFromRequestUUIDAndAwait & invalidate within the callback', () => {
testScheduler.run(({ cold, expectObservable }) => { service.deleteByHref('https://somewhere.org/something/123');
buildFromRequestUUIDSpy.and.returnValue(
cold('(r|)', { r: MOCK_SUCCEEDED_RD}) // RD emits right away
);
invalidateByHrefSpy.and.returnValue(
cold('----(t|)', BOOLEAN) // but we pretend that setting requests to stale takes longer
);
const done$ = service.deleteByHref('some-href'); expect(buildFromRequestUUIDAndAwaitSpy).toHaveBeenCalled();
expectObservable(done$).toBe( expect(buildFromRequestUUIDAndAwaitSpy.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done const callback = buildFromRequestUUIDAndAwaitSpy.calls.argsFor(0)[1];
); callback();
}); expect(invalidateByHrefSpy).toHaveBeenCalledWith('https://somewhere.org/something/123');
});
it('should wait for the DELETE request to resolve before emitting', () => {
testScheduler.run(({ cold, expectObservable }) => {
buildFromRequestUUIDSpy.and.returnValue(
cold('----(r|)', { r: MOCK_SUCCEEDED_RD}) // the request takes a while
);
invalidateByHrefSpy.and.returnValue(
cold('(t|)', BOOLEAN) // but we pretend that setting to stale happens sooner
); // e.g.: maybe already stale before this call?
const done$ = service.deleteByHref('some-href');
expectObservable(done$).toBe(
'----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait for the request
);
});
}); });
}); });
}); });

View File

@@ -649,35 +649,7 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
} }
this.requestService.send(request); this.requestService.send(request);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.invalidateByHref(href));
const invalidated$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return this.invalidateByHref(href);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
/** /**

View File

@@ -95,6 +95,7 @@ describe('GroupDataService', () => {
store = new Store<CoreState>(undefined, undefined, undefined); store = new Store<CoreState>(undefined, undefined, undefined);
service = initTestService(); service = initTestService();
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();
}); });
describe('searchGroups', () => { describe('searchGroups', () => {
@@ -136,6 +137,11 @@ describe('GroupDataService', () => {
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the parent group', () => { it('should invalidate the previous requests of the parent group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2); expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
@@ -156,6 +162,11 @@ describe('GroupDataService', () => {
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the parent group\'', () => { it('should invalidate the previous requests of the parent group\'', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2); expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2);
expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1');
@@ -180,6 +191,11 @@ describe('GroupDataService', () => {
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the EPerson and the group', () => { it('should invalidate the previous requests of the EPerson and the group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href);
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4); expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);
@@ -201,6 +217,11 @@ describe('GroupDataService', () => {
expect(requestService.send).toHaveBeenCalledWith(expected); expect(requestService.send).toHaveBeenCalledWith(expected);
}); });
it('should invalidate the previous requests of the EPerson and the group', () => { it('should invalidate the previous requests of the EPerson and the group', () => {
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect(rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = rdbService.buildFromRequestUUIDAndAwait.calls.argsFor(0)[1];
callback();
expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href);
expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href);
expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4); expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4);

View File

@@ -124,40 +124,10 @@ export class GroupDataService extends DataService<Group> {
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options);
this.requestService.send(postRequest); this.requestService.send(postRequest);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(activeGroup._links.self.href),
const invalidated$ = new AsyncSubject<boolean>(); this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
response$.pipe( ));
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
return observableZip(
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
).pipe(
map((arr: boolean[]) => arr.every((b: boolean) => b === true))
);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
/** /**
@@ -172,40 +142,10 @@ export class GroupDataService extends DataService<Group> {
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id);
this.requestService.send(deleteRequest); this.requestService.send(deleteRequest);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(activeGroup._links.self.href),
const invalidated$ = new AsyncSubject<boolean>(); this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
response$.pipe( ));
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return observableZip(
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)),
).pipe(
map((arr: boolean[]) => arr.every((b: boolean) => b === true))
);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
/** /**
@@ -223,42 +163,12 @@ export class GroupDataService extends DataService<Group> {
const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options);
this.requestService.send(postRequest); this.requestService.send(postRequest);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(ePerson._links.self.href),
const invalidated$ = new AsyncSubject<boolean>(); this.invalidateByHref(activeGroup._links.self.href),
response$.pipe( this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
getFirstCompletedRemoteData(), this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
switchMap((rd: RemoteData<NoContent>) => { ));
if (rd.hasSucceeded) {
return observableZip(
this.invalidateByHref(ePerson._links.self.href),
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
).pipe(
map((arr: boolean[]) => arr.every((b: boolean) => b === true))
);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
/** /**
@@ -272,42 +182,12 @@ export class GroupDataService extends DataService<Group> {
const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id);
this.requestService.send(deleteRequest); this.requestService.send(deleteRequest);
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableZip(
this.invalidateByHref(ePerson._links.self.href),
const invalidated$ = new AsyncSubject<boolean>(); this.invalidateByHref(activeGroup._links.self.href),
response$.pipe( this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
getFirstCompletedRemoteData(), this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
switchMap((rd: RemoteData<NoContent>) => { ));
if (rd.hasSucceeded) {
return observableZip(
this.invalidateByHref(ePerson._links.self.href),
this.invalidateByHref(activeGroup._links.self.href),
this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)),
this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)),
).pipe(
map((arr: boolean[]) => arr.every((b: boolean) => b === true))
);
} else {
return [true];
}
})
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
/** /**

View File

@@ -123,6 +123,9 @@ describe('ResourcePolicyService', () => {
}), }),
buildFromRequestUUID: hot('a|', { buildFromRequestUUID: hot('a|', {
a: resourcePolicyRD a: resourcePolicyRD
}),
buildFromRequestUUIDAndAwait: hot('a|', {
a: resourcePolicyRD
}) })
}); });
ePersonService = jasmine.createSpyObj('ePersonService', { ePersonService = jasmine.createSpyObj('ePersonService', {
@@ -346,6 +349,8 @@ describe('ResourcePolicyService', () => {
}); });
describe('updateTarget', () => { describe('updateTarget', () => {
let buildFromRequestUUIDAndAwaitSpy;
beforeEach(() => { beforeEach(() => {
scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID)); scheduler.schedule(() => service.create(resourcePolicy, resourceUUID, epersonUUID));
}); });
@@ -362,41 +367,17 @@ describe('ResourcePolicyService', () => {
})); }));
}); });
it('should send a PUT request to update the Group', () => { it('should invalidate the ResourcePolicy', () => {
service.updateTarget(resourcePolicyId, requestURL, groupUUID, 'group');
scheduler.flush();
expect(requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({
method: RestRequestMethod.PUT,
uuid: requestUUID,
href: `${resourcePolicy._links.self.href}/group`,
body: 'https://rest.api/rest/api/eperson/groups/' + groupUUID,
}));
});
it('should invalidate the ResourcePolicy if the PUT request succeeds', () => {
service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson'); service.updateTarget(resourcePolicyId, requestURL, epersonUUID, 'eperson');
scheduler.flush(); scheduler.flush();
expect(rdbService.buildFromRequestUUIDAndAwait).toHaveBeenCalled();
expect((rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[0]).toBe(requestService.generateRequestId());
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
callback();
expect((service as any).dataService.invalidateByHref).toHaveBeenCalledWith(resourcePolicy._links.self.href); expect((service as any).dataService.invalidateByHref).toHaveBeenCalledWith(resourcePolicy._links.self.href);
}); });
it('should only emit when invalidation is complete', () => {
const RD = {
p: createPendingRemoteDataObject(),
s: createSuccessfulRemoteDataObject({}),
};
const response$ = cold('(s|)', RD);
const invalidate$ = cold('--(d|)', { d: true });
(rdbService.buildFromRequestUUID as any).and.returnValue(response$);
((service as any).dataService.invalidateByHref).and.returnValue(invalidate$);
const out$ = service.updateTarget(resourcePolicyId, requestURL, groupUUID, 'group');
scheduler.flush();
expect(out$).toBeObservable(cold('--(s|)', RD));
});
}); });
}); });

View File

@@ -263,35 +263,7 @@ export class ResourcePolicyService {
this.requestService.send(request); this.requestService.send(request);
}); });
const response$ = this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => this.dataService.invalidateByHref(resourcePolicyHref));
const invalidated$ = new AsyncSubject<boolean>();
response$.pipe(
getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
return this.dataService.invalidateByHref(resourcePolicyHref);
} else {
return [undefined];
}
}),
).subscribe(() => {
invalidated$.next(true);
invalidated$.complete();
});
return response$.pipe(
switchMap((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
return invalidated$.pipe(
filter((invalidated: boolean) => invalidated),
map(() => rd)
);
} else {
return [rd];
}
})
);
} }
} }

View File

@@ -29,6 +29,7 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab
} }
}, },
buildFromRequestUUID: (id: string) => createSuccessfulRemoteDataObject$({}), buildFromRequestUUID: (id: string) => createSuccessfulRemoteDataObject$({}),
buildFromRequestUUIDAndAwait: (id: string, callback: (rd?: RemoteData<any>) => Observable<any>) => createSuccessfulRemoteDataObject$({}),
buildFromHref: (href: string) => createSuccessfulRemoteDataObject$({}) buildFromHref: (href: string) => createSuccessfulRemoteDataObject$({})
} as RemoteDataBuildService; } as RemoteDataBuildService;
@@ -66,6 +67,7 @@ export function getMockRemoteDataBuildServiceHrefMap(toRemoteDataObservable$?: O
); );
}, },
buildFromRequestUUID: (id: string) => createSuccessfulRemoteDataObject$({}), buildFromRequestUUID: (id: string) => createSuccessfulRemoteDataObject$({}),
buildFromRequestUUIDAndAwait: (id: string, callback: (rd?: RemoteData<any>) => Observable<any>) => createSuccessfulRemoteDataObject$({}),
buildFromHref: (href: string) => createSuccessfulRemoteDataObject$({}) buildFromHref: (href: string) => createSuccessfulRemoteDataObject$({})
} as RemoteDataBuildService; } as RemoteDataBuildService;