Merge branch 'feature-process_polling-7.6' into feature-process_polling

# Conflicts:
#	src/app/core/data/processes/process-data.service.ts
This commit is contained in:
Alexandre Vryghem
2023-12-22 14:04:34 +01:00
16 changed files with 361 additions and 181 deletions

View File

@@ -21,6 +21,10 @@ import { RequestEntryState } from '../request-entry-state.model';
import { fakeAsync, tick } from '@angular/core/testing'; import { fakeAsync, tick } from '@angular/core/testing';
import { BaseDataService } from './base-data.service'; import { BaseDataService } from './base-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
import { HALLink } from '../../shared/hal-link.model';
import { createPaginatedList } from '../../../shared/testing/utils.test';
const endpoint = 'https://rest.api/core'; const endpoint = 'https://rest.api/core';
@@ -46,34 +50,18 @@ describe('BaseDataService', () => {
let requestService; let requestService;
let halService; let halService;
let rdbService; let rdbService;
let objectCache; let objectCache: ObjectCacheServiceStub;
let selfLink; let selfLink;
let linksToFollow; let linksToFollow;
let testScheduler; let testScheduler;
let remoteDataMocks; let remoteDataMocks: { [responseType: string]: RemoteData<any> };
let remoteDataPageMocks: { [responseType: string]: RemoteData<any> };
function initTestService(): TestService { function initTestService(): TestService {
requestService = getMockRequestService(); requestService = getMockRequestService();
halService = new HALEndpointServiceStub('url') as any; halService = new HALEndpointServiceStub('url') as any;
rdbService = getMockRemoteDataBuildService(); rdbService = getMockRemoteDataBuildService();
objectCache = { objectCache = new ObjectCacheServiceStub();
addPatch: () => {
/* empty */
},
getObjectBySelfLink: () => {
/* empty */
},
getByHref: () => {
/* empty */
},
addDependency: () => {
/* empty */
},
removeDependents: () => {
/* empty */
},
} as any;
selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
linksToFollow = [ linksToFollow = [
followLink('a'), followLink('a'),
@@ -88,7 +76,25 @@ describe('BaseDataService', () => {
const timeStamp = new Date().getTime(); const timeStamp = new Date().getTime();
const msToLive = 15 * 60 * 1000; const msToLive = 15 * 60 * 1000;
const payload = { foo: 'bar' }; const payload = {
foo: 'bar',
_links: {
self: Object.assign(new HALLink(), {
href: 'self-test-link',
}),
followLink1: Object.assign(new HALLink(), {
href: 'follow-link-1',
}),
followLink2: [
Object.assign(new HALLink(), {
href: 'follow-link-2-1',
}),
Object.assign(new HALLink(), {
href: 'follow-link-2-2',
}),
],
}
};
const statusCodeSuccess = 200; const statusCodeSuccess = 200;
const statusCodeError = 404; const statusCodeError = 404;
const errorMessage = 'not found'; const errorMessage = 'not found';
@@ -100,11 +106,19 @@ describe('BaseDataService', () => {
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
}; };
remoteDataPageMocks = {
RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess),
SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess),
Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError),
ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError),
};
return new TestService( return new TestService(
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache as ObjectCacheService,
halService, halService,
); );
} }
@@ -375,6 +389,27 @@ describe('BaseDataService', () => {
}); });
it('should link all the followLinks of a cached object by calling addDependency', () => {
spyOn(objectCache, 'addDependency').and.callThrough();
testScheduler.run(({ cold, expectObservable, flush }) => {
spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d', {
a: remoteDataMocks.Success,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
}));
const expected = '--b-c-d';
const values = {
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
};
expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values);
flush();
expect(objectCache.addDependency).toHaveBeenCalledTimes(4);
});
});
}); });
describe(`findListByHref`, () => { describe(`findListByHref`, () => {
@@ -387,8 +422,8 @@ describe('BaseDataService', () => {
it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => { it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => {
testScheduler.run(({ cold }) => { testScheduler.run(({ cold }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow); expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow);
@@ -398,8 +433,8 @@ describe('BaseDataService', () => {
it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => { it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true); expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true);
@@ -414,8 +449,8 @@ describe('BaseDataService', () => {
it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => { it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success }));
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow); expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow);
@@ -426,12 +461,12 @@ describe('BaseDataService', () => {
it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => { it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!');
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale }));
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.SuccessStale }));
service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow);
expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue(); expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue();
spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale }));
// prove that the spy we just added hasn't been called yet // prove that the spy we just added hasn't been called yet
expect(service.findListByHref).not.toHaveBeenCalled(); expect(service.findListByHref).not.toHaveBeenCalled();
// call the callback passed to reRequestStaleRemoteData // call the callback passed to reRequestStaleRemoteData
@@ -446,7 +481,7 @@ describe('BaseDataService', () => {
it(`should return a the output from reRequestStaleRemoteData`, () => { it(`should return a the output from reRequestStaleRemoteData`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink);
spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success }));
spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' }));
const expected = 'a'; const expected = 'a';
const values = { const values = {
@@ -466,19 +501,19 @@ describe('BaseDataService', () => {
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => { it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.Success, a: remoteDataPageMocks.Success,
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
})); }));
const expected = 'a-b-c-d-e'; const expected = 'a-b-c-d-e';
const values = { const values = {
a: remoteDataMocks.Success, a: remoteDataPageMocks.Success,
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
@@ -488,18 +523,18 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.SuccessStale, a: remoteDataPageMocks.SuccessStale,
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '--b-c-d-e';
const values = { const values = {
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values);
@@ -518,18 +553,18 @@ describe('BaseDataService', () => {
it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.Success, a: remoteDataPageMocks.Success,
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '--b-c-d-e';
const values = { const values = {
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
@@ -539,24 +574,45 @@ describe('BaseDataService', () => {
it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => {
testScheduler.run(({ cold, expectObservable }) => { testScheduler.run(({ cold, expectObservable }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.SuccessStale, a: remoteDataPageMocks.SuccessStale,
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
})); }));
const expected = '--b-c-d-e'; const expected = '--b-c-d-e';
const values = { const values = {
b: remoteDataMocks.RequestPending, b: remoteDataPageMocks.RequestPending,
c: remoteDataMocks.ResponsePending, c: remoteDataPageMocks.ResponsePending,
d: remoteDataMocks.Success, d: remoteDataPageMocks.Success,
e: remoteDataMocks.SuccessStale, e: remoteDataPageMocks.SuccessStale,
}; };
expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values);
}); });
}); });
it('should link all the followLinks of the cached objects by calling addDependency', () => {
spyOn(objectCache, 'addDependency').and.callThrough();
testScheduler.run(({ cold, expectObservable, flush }) => {
spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d', {
a: remoteDataPageMocks.SuccessStale,
b: remoteDataPageMocks.RequestPending,
c: remoteDataPageMocks.ResponsePending,
d: remoteDataPageMocks.Success,
}));
const expected = '--b-c-d';
const values = {
b: remoteDataPageMocks.RequestPending,
c: remoteDataPageMocks.ResponsePending,
d: remoteDataPageMocks.Success,
};
expectObservable(service.findListByHref(selfLink, findListOptions, false, false, ...linksToFollow)).toBe(expected, values);
flush();
expect(objectCache.addDependency).toHaveBeenCalledTimes(4);
});
});
}); });
}); });
@@ -567,7 +623,7 @@ describe('BaseDataService', () => {
getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({
requestUUIDs: ['request1', 'request2', 'request3'], requestUUIDs: ['request1', 'request2', 'request3'],
dependentRequestUUIDs: ['request4', 'request5'] dependentRequestUUIDs: ['request4', 'request5']
})); } as ObjectCacheEntry));
}); });

View File

@@ -24,6 +24,7 @@ import { ObjectCacheEntry } from '../../cache/object-cache.reducer';
import { ObjectCacheService } from '../../cache/object-cache.service'; import { ObjectCacheService } from '../../cache/object-cache.service';
import { HALDataService } from './hal-data-service.interface'; import { HALDataService } from './hal-data-service.interface';
import { getFirstCompletedRemoteData } from '../../shared/operators'; import { getFirstCompletedRemoteData } from '../../shared/operators';
import { HALLink } from '../../shared/hal-link.model';
export const EMBED_SEPARATOR = '%2F'; export const EMBED_SEPARATOR = '%2F';
/** /**
@@ -268,7 +269,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe( const response$: Observable<RemoteData<T>> = this.rdbService.buildSingle<T>(requestHref$, ...linksToFollow).pipe(
// This skip ensures that if a stale object is present in the cache when you do a // This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request // call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
@@ -277,6 +278,22 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.reRequestStaleRemoteData(reRequestOnStale, () => this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
); );
return response$.pipe(
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
tap((remoteDataObject: RemoteData<T>) => {
if (hasValue(remoteDataObject?.payload?._links)) {
for (const followLink of Object.values(remoteDataObject.payload._links)) {
// followLink can be either an individual HALLink or a HALLink[]
const followLinksList: HALLink[] = [].concat(followLink);
for (const individualFollowLink of followLinksList) {
if (hasValue(individualFollowLink?.href)) {
this.addDependency(response$, individualFollowLink.href);
}
}
}
}
}),
);
} }
/** /**
@@ -302,7 +319,7 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable);
return this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe( const response$: Observable<RemoteData<PaginatedList<T>>> = this.rdbService.buildList<T>(requestHref$, ...linksToFollow).pipe(
// This skip ensures that if a stale object is present in the cache when you do a // This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request // call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
@@ -311,6 +328,26 @@ export class BaseDataService<T extends CacheableObject> implements HALDataServic
this.reRequestStaleRemoteData(reRequestOnStale, () => this.reRequestStaleRemoteData(reRequestOnStale, () =>
this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
); );
return response$.pipe(
// Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object
tap((remoteDataObject: RemoteData<PaginatedList<T>>) => {
if (hasValue(remoteDataObject?.payload?.page)) {
for (const object of remoteDataObject.payload.page) {
if (hasValue(object?._links)) {
for (const followLink of Object.values(object._links)) {
// followLink can be either an individual HALLink or a HALLink[]
const followLinksList: HALLink[] = [].concat(followLink);
for (const individualFollowLink of followLinksList) {
if (hasValue(individualFollowLink?.href)) {
this.addDependency(response$, individualFollowLink.href);
}
}
}
}
}
}
}),
);
} }
/** /**

View File

@@ -24,6 +24,7 @@ import { testFindAllDataImplementation } from './base/find-all-data.spec';
import { testSearchDataImplementation } from './base/search-data.spec'; import { testSearchDataImplementation } from './base/search-data.spec';
import { testPatchDataImplementation } from './base/patch-data.spec'; import { testPatchDataImplementation } from './base/patch-data.spec';
import { testDeleteDataImplementation } from './base/delete-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
const url = 'fake-url'; const url = 'fake-url';
const collectionId = 'fake-collection-id'; const collectionId = 'fake-collection-id';
@@ -35,7 +36,7 @@ describe('CollectionDataService', () => {
let translate: TranslateService; let translate: TranslateService;
let notificationsService: any; let notificationsService: any;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheServiceStub;
let halService: any; let halService: any;
const mockCollection1: Collection = Object.assign(new Collection(), { const mockCollection1: Collection = Object.assign(new Collection(), {
@@ -205,14 +206,12 @@ describe('CollectionDataService', () => {
buildFromRequestUUID: buildResponse$, buildFromRequestUUID: buildResponse$,
buildSingle: buildResponse$ buildSingle: buildResponse$
}); });
objectCache = jasmine.createSpyObj('objectCache', { objectCache = new ObjectCacheServiceStub();
remove: jasmine.createSpy('remove')
});
halService = new HALEndpointServiceStub(url); halService = new HALEndpointServiceStub(url);
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();
translate = getMockTranslateService(); translate = getMockTranslateService();
service = new CollectionDataService(requestService, rdbService, objectCache, halService, null, notificationsService, null, null, translate); service = new CollectionDataService(requestService, rdbService, objectCache as ObjectCacheService, halService, null, notificationsService, null, null, translate);
} }
}); });

View File

@@ -12,7 +12,7 @@ import { Bitstream } from '../../shared/bitstream.model';
import { RemoteData } from '../remote-data'; import { RemoteData } from '../remote-data';
import { BitstreamDataService } from '../bitstream-data.service'; import { BitstreamDataService } from '../bitstream-data.service';
import { IdentifiableDataService } from '../base/identifiable-data.service'; import { IdentifiableDataService } from '../base/identifiable-data.service';
import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; import { FindAllData, FindAllDataImpl } from '../base/find-all-data';
import { FindListOptions } from '../find-list-options.model'; import { FindListOptions } from '../find-list-options.model';
import { dataService } from '../base/data-service.decorator'; import { dataService } from '../base/data-service.decorator';
@@ -35,26 +35,11 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v
@Injectable() @Injectable()
@dataService(PROCESS) @dataService(PROCESS)
export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> { export class ProcessDataService extends IdentifiableDataService<Process> implements FindAllData<Process>, DeleteData<Process> {
private findAllData: FindAllData<Process>; private findAllData: FindAllData<Process>;
private deleteData: DeleteData<Process>; private deleteData: DeleteData<Process>;
protected activelyBeingPolled: Map<string, NodeJS.Timeout> = new Map(); protected activelyBeingPolled: Map<string, NodeJS.Timeout> = new Map();
/**
* Return true if the given process has the given status
* @protected
*/
protected static statusIs(process: Process, status: ProcessStatus): boolean {
return hasValue(process) && process.processStatus === status;
}
/**
* Return true if the given process has the status COMPLETED or FAILED
*/
public static hasCompletedOrFailed(process: Process): boolean {
return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) ||
ProcessDataService.statusIs(process, ProcessStatus.FAILED);
}
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
@@ -71,6 +56,22 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
} }
/**
* Return true if the given process has the given status
* @protected
*/
protected static statusIs(process: Process, status: ProcessStatus): boolean {
return hasValue(process) && process.processStatus === status;
}
/**
* Return true if the given process has the status COMPLETED or FAILED
*/
public static hasCompletedOrFailed(process: Process): boolean {
return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) ||
ProcessDataService.statusIs(process, ProcessStatus.FAILED);
}
/** /**
* Get the endpoint for the files of the process * Get the endpoint for the files of the process
* @param processId The ID of the process * @param processId The ID of the process
@@ -153,11 +154,14 @@ export class ProcessDataService extends IdentifiableDataService<Process> impleme
* status. That makes it more convenient to retrieve that process for a component: you can replace * status. That makes it more convenient to retrieve that process for a component: you can replace
* a findByID call with this method, rather than having to do a separate findById, and then call * a findByID call with this method, rather than having to do a separate findById, and then call
* this method * this method
* @param processId *
* @param pollingIntervalInMs * @param processId The ID of the {@link Process} to poll
* @param pollingIntervalInMs The interval for how often the request needs to be polled
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be
* automatically resolved
*/ */
public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000): Observable<RemoteData<Process>> { public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000, ...linksToFollow: FollowLinkConfig<Process>[]): Observable<RemoteData<Process>> {
const process$ = this.findById(processId, true, true, followLink('script')) const process$: Observable<RemoteData<Process>> = this.findById(processId, true, true, ...linksToFollow)
.pipe( .pipe(
getAllCompletedRemoteData(), getAllCompletedRemoteData(),
); );

View File

@@ -23,6 +23,7 @@ import { FindListOptions } from './find-list-options.model';
import { testSearchDataImplementation } from './base/search-data.spec'; import { testSearchDataImplementation } from './base/search-data.spec';
import { MetadataValue } from '../shared/metadata.models'; import { MetadataValue } from '../shared/metadata.models';
import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('RelationshipDataService', () => { describe('RelationshipDataService', () => {
let service: RelationshipDataService; let service: RelationshipDataService;
@@ -114,14 +115,7 @@ describe('RelationshipDataService', () => {
'href': buildList$, 'href': buildList$,
'https://rest.api/core/publication/relationships': relationships$ 'https://rest.api/core/publication/relationships': relationships$
}); });
const objectCache = Object.assign({ const objectCache = new ObjectCacheServiceStub();
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
remove: () => {
},
hasBySelfLinkObservable: () => observableOf(false),
hasByHref$: () => observableOf(false)
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}) as ObjectCacheService;
const itemService = jasmine.createSpyObj('itemService', { const itemService = jasmine.createSpyObj('itemService', {
findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)), findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)),
@@ -133,7 +127,7 @@ describe('RelationshipDataService', () => {
requestService, requestService,
rdbService, rdbService,
halService, halService,
objectCache, objectCache as ObjectCacheService,
itemService, itemService,
null, null,
jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v), jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v),

View File

@@ -10,6 +10,7 @@ import { RequestService } from './request.service';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { hasValueOperator } from '../../shared/empty.util'; import { hasValueOperator } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('RelationshipTypeDataService', () => { describe('RelationshipTypeDataService', () => {
let service: RelationshipTypeDataService; let service: RelationshipTypeDataService;
@@ -28,7 +29,7 @@ describe('RelationshipTypeDataService', () => {
let buildList; let buildList;
let rdbService; let rdbService;
let objectCache; let objectCache: ObjectCacheServiceStub;
function init() { function init() {
restEndpointURL = 'https://rest.api/relationshiptypes'; restEndpointURL = 'https://rest.api/relationshiptypes';
@@ -60,21 +61,14 @@ describe('RelationshipTypeDataService', () => {
buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2])); buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2]));
rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList)); rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList));
objectCache = Object.assign({ objectCache = new ObjectCacheServiceStub();
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
remove: () => {
},
hasBySelfLinkObservable: () => observableOf(false)
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
}) as ObjectCacheService;
} }
function initTestService() { function initTestService() {
return new RelationshipTypeDataService( return new RelationshipTypeDataService(
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache as ObjectCacheService,
halService, halService,
); );
} }

View File

@@ -20,13 +20,14 @@ import { FindListOptions } from '../data/find-list-options.model';
import { EPersonDataService } from '../eperson/eperson-data.service'; import { EPersonDataService } from '../eperson/eperson-data.service';
import { GroupDataService } from '../eperson/group-data.service'; import { GroupDataService } from '../eperson/group-data.service';
import { RestRequestMethod } from '../data/rest-request-method'; import { RestRequestMethod } from '../data/rest-request-method';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('ResourcePolicyService', () => { describe('ResourcePolicyService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: ResourcePolicyDataService; let service: ResourcePolicyDataService;
let requestService: RequestService; let requestService: RequestService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheServiceStub;
let halService: HALEndpointService; let halService: HALEndpointService;
let responseCacheEntry: RequestEntry; let responseCacheEntry: RequestEntry;
let ePersonService: EPersonDataService; let ePersonService: EPersonDataService;
@@ -139,14 +140,14 @@ describe('ResourcePolicyService', () => {
a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID
}), }),
}); });
objectCache = {} as ObjectCacheService; objectCache = new ObjectCacheServiceStub();
const notificationsService = {} as NotificationsService; const notificationsService = {} as NotificationsService;
const comparator = {} as any; const comparator = {} as any;
service = new ResourcePolicyDataService( service = new ResourcePolicyDataService(
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache as ObjectCacheService,
halService, halService,
notificationsService, notificationsService,
comparator, comparator,

View File

@@ -25,6 +25,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntry } from '../../data/request-entry.model';
import { VocabularyDataService } from './vocabulary.data.service'; import { VocabularyDataService } from './vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service'; import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service';
import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub';
describe('VocabularyService', () => { describe('VocabularyService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -205,6 +206,7 @@ describe('VocabularyService', () => {
function initTestService() { function initTestService() {
hrefOnlyDataService = getMockHrefOnlyDataService(); hrefOnlyDataService = getMockHrefOnlyDataService();
objectCache = new ObjectCacheServiceStub() as ObjectCacheService;
return new VocabularyService( return new VocabularyService(
requestService, requestService,

View File

@@ -17,13 +17,14 @@ import { RestResponse } from '../cache/response.models';
import { RequestEntry } from '../data/request-entry.model'; import { RequestEntry } from '../data/request-entry.model';
import { FindListOptions } from '../data/find-list-options.model'; import { FindListOptions } from '../data/find-list-options.model';
import { GroupDataService } from '../eperson/group-data.service'; import { GroupDataService } from '../eperson/group-data.service';
import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub';
describe('SupervisionOrderService', () => { describe('SupervisionOrderService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
let service: SupervisionOrderDataService; let service: SupervisionOrderDataService;
let requestService: RequestService; let requestService: RequestService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheServiceStub;
let halService: HALEndpointService; let halService: HALEndpointService;
let responseCacheEntry: RequestEntry; let responseCacheEntry: RequestEntry;
let groupService: GroupDataService; let groupService: GroupDataService;
@@ -127,14 +128,14 @@ describe('SupervisionOrderService', () => {
a: 'https://rest.api/rest/api/group/groups/' + groupUUID a: 'https://rest.api/rest/api/group/groups/' + groupUUID
}), }),
}); });
objectCache = {} as ObjectCacheService; objectCache = new ObjectCacheServiceStub();
const notificationsService = {} as NotificationsService; const notificationsService = {} as NotificationsService;
const comparator = {} as any; const comparator = {} as any;
service = new SupervisionOrderDataService( service = new SupervisionOrderDataService(
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache as ObjectCacheService,
halService, halService,
notificationsService, notificationsService,
comparator, comparator,

View File

@@ -27,7 +27,6 @@ import { ProcessDataService } from '../../core/data/processes/process-data.servi
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
@@ -35,6 +34,10 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { getProcessListRoute } from '../process-page-routing.paths'; import { getProcessListRoute } from '../process-page-routing.paths';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RouterTestingModule } from '@angular/router/testing';
import { RouterStub } from '../../shared/testing/router.stub';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
describe('ProcessDetailComponent', () => { describe('ProcessDetailComponent', () => {
let component: ProcessDetailComponent; let component: ProcessDetailComponent;
@@ -44,44 +47,18 @@ describe('ProcessDetailComponent', () => {
let nameService: DSONameService; let nameService: DSONameService;
let bitstreamDataService: BitstreamDataService; let bitstreamDataService: BitstreamDataService;
let httpClient: HttpClient; let httpClient: HttpClient;
let route: ActivatedRoute; let route: ActivatedRouteStub;
let router: RouterStub;
let modalService;
let notificationsService: NotificationsServiceStub;
let process: Process; let process: Process;
let fileName: string; let fileName: string;
let files: Bitstream[]; let files: Bitstream[];
let processOutput; let processOutput: string;
let modalService;
let notificationsService;
let router;
function init() { function init() {
processOutput = 'Process Started';
process = Object.assign(new Process(), {
processId: 1,
scriptName: 'script-name',
processStatus: 'COMPLETED',
parameters: [
{
name: '-f',
value: 'file.xml'
},
{
name: '-i',
value: 'identifier'
}
],
_links: {
self: {
href: 'https://rest.api/processes/1'
},
output: {
href: 'https://rest.api/processes/1/output'
}
}
});
fileName = 'fake-file-name'; fileName = 'fake-file-name';
files = [ files = [
Object.assign(new Bitstream(), { Object.assign(new Bitstream(), {
@@ -99,6 +76,33 @@ describe('ProcessDetailComponent', () => {
} }
}) })
]; ];
processOutput = 'Process Started';
process = Object.assign(new Process(), {
processId: 1,
scriptName: 'script-name',
processStatus: 'COMPLETED',
parameters: [
{
name: '-f',
value: 'file.xml'
},
{
name: '-i',
value: 'identifier'
}
],
files: createSuccessfulRemoteDataObject$(Object.assign(new PaginatedList(), {
page: files,
})),
_links: {
self: {
href: 'https://rest.api/processes/1'
},
output: {
href: 'https://rest.api/processes/1/output'
}
},
});
const logBitstream = Object.assign(new Bitstream(), { const logBitstream = Object.assign(new Bitstream(), {
id: 'output.log', id: 'output.log',
_links: { _links: {
@@ -127,33 +131,22 @@ describe('ProcessDetailComponent', () => {
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();
router = jasmine.createSpyObj('router', { router = new RouterStub();
navigateByUrl:{}
});
route = jasmine.createSpyObj('route', { route = new ActivatedRouteStub({
data: observableOf({ process: createSuccessfulRemoteDataObject$(process) }), id: process.processId,
snapshot: { }, {
params: { id: process.processId } process: createSuccessfulRemoteDataObject$(process),
}
}); });
} }
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ void TestBed.configureTestingModule({
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), RouterTestingModule],
providers: [ providers: [
{ { provide: ActivatedRoute, useValue: route },
provide: ActivatedRoute,
useValue: {
data: observableOf({ process: createSuccessfulRemoteDataObject(process) }),
snapshot: {
params: { id: process.processId }
}
}
},
{ provide: ProcessDataService, useValue: processService }, { provide: ProcessDataService, useValue: processService },
{ provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: DSONameService, useValue: nameService }, { provide: DSONameService, useValue: nameService },
@@ -258,6 +251,8 @@ describe('ProcessDetailComponent', () => {
describe('deleteProcess', () => { describe('deleteProcess', () => {
it('should delete the process and navigate back to the overview page on success', () => { it('should delete the process and navigate back to the overview page on success', () => {
spyOn(component, 'closeModal'); spyOn(component, 'closeModal');
spyOn(router, 'navigateByUrl').and.callThrough();
component.deleteProcess(process); component.deleteProcess(process);
expect(processService.delete).toHaveBeenCalledWith(process.processId); expect(processService.delete).toHaveBeenCalledWith(process.processId);
@@ -268,6 +263,7 @@ describe('ProcessDetailComponent', () => {
it('should delete the process and not navigate on error', () => { it('should delete the process and not navigate on error', () => {
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); (processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
spyOn(component, 'closeModal'); spyOn(component, 'closeModal');
spyOn(router, 'navigateByUrl').and.callThrough();
component.deleteProcess(process); component.deleteProcess(process);

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, Inject, NgZone, OnInit, PLATFORM_ID } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { finalize, map, switchMap, take, tap, find, startWith } from 'rxjs/operators'; import { finalize, map, switchMap, take, tap, find, startWith } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -27,6 +27,7 @@ import { getProcessListRoute } from '../process-page-routing.paths';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { PROCESS_PAGE_FOLLOW_LINKS } from '../process-page.resolver';
@Component({ @Component({
selector: 'ds-process-detail', selector: 'ds-process-detail',
@@ -84,8 +85,6 @@ export class ProcessDetailComponent implements OnInit {
*/ */
protected modalRef: NgbModalRef; protected modalRef: NgbModalRef;
private refreshTimerSub?: Subscription;
constructor( constructor(
@Inject(PLATFORM_ID) protected platformId: object, @Inject(PLATFORM_ID) protected platformId: object,
protected route: ActivatedRoute, protected route: ActivatedRoute,
@@ -109,7 +108,7 @@ export class ProcessDetailComponent implements OnInit {
this.processRD$ = this.route.data.pipe( this.processRD$ = this.route.data.pipe(
switchMap((data) => { switchMap((data) => {
if (isPlatformBrowser(this.platformId)) { if (isPlatformBrowser(this.platformId)) {
return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000); return this.processService.autoRefreshUntilCompletion(this.route.snapshot.params.id, 5000, ...PROCESS_PAGE_FOLLOW_LINKS);
} else { } else {
return [data.process as RemoteData<Process>]; return [data.process as RemoteData<Process>];
} }
@@ -125,7 +124,7 @@ export class ProcessDetailComponent implements OnInit {
this.filesRD$ = this.processRD$.pipe( this.filesRD$ = this.processRD$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
switchMap((process: Process) => this.processService.getFiles(process.processId)) switchMap((process: Process) => process.files),
); );
} }

View File

@@ -7,6 +7,10 @@ import { followLink } from '../shared/utils/follow-link-config.model';
import { ProcessDataService } from '../core/data/processes/process-data.service'; import { ProcessDataService } from '../core/data/processes/process-data.service';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
export const PROCESS_PAGE_FOLLOW_LINKS = [
followLink('files'),
];
/** /**
* This class represents a resolver that requests a specific process before the route is activated * This class represents a resolver that requests a specific process before the route is activated
*/ */
@@ -23,7 +27,7 @@ export class ProcessPageResolver implements Resolve<RemoteData<Process>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> {
return this.processService.findById(route.params.id, false, true, followLink('script')).pipe( return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }

View File

@@ -0,0 +1,34 @@
import { typedObject } from '../../core/cache/builders/build-decorators';
import { excludeFromEquals } from '../../core/utilities/equals.decorators';
import { autoserialize } from 'cerialize';
import { ResourceType } from '../../core/shared/resource-type';
import { FILETYPES } from './filetypes.resource-type';
/**
* Object representing the file types of the {@link Bitstream}s of a {@link Process}
*/
@typedObject
export class Filetypes {
static type = FILETYPES;
/**
* The id of this {@link Filetypes}
*/
@autoserialize
id: string;
/**
* The values of this {@link Filetypes}
*/
@autoserialize
values: string[];
/**
* The object type
*/
@excludeFromEquals
@autoserialize
type: ResourceType;
}

View File

@@ -0,0 +1,8 @@
/**
* The resource type for {@link Filetypes}
*
* Needs to be in a separate file to prevent circular dependencies in webpack.
*/
import { ResourceType } from '../../core/shared/resource-type';
export const FILETYPES = new ResourceType('filetypes');

View File

@@ -13,6 +13,10 @@ import { RemoteData } from '../../core/data/remote-data';
import { SCRIPT } from '../scripts/script.resource-type'; import { SCRIPT } from '../scripts/script.resource-type';
import { Script } from '../scripts/script.model'; import { Script } from '../scripts/script.model';
import { CacheableObject } from '../../core/cache/cacheable-object.model'; import { CacheableObject } from '../../core/cache/cacheable-object.model';
import { BITSTREAM } from '../../core/shared/bitstream.resource-type';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { Filetypes } from './filetypes.model';
import { FILETYPES } from './filetypes.resource-type';
/** /**
* Object representing a process * Object representing a process
@@ -78,7 +82,8 @@ export class Process implements CacheableObject {
self: HALLink, self: HALLink,
script: HALLink, script: HALLink,
output: HALLink, output: HALLink,
files: HALLink files: HALLink,
filetypes: HALLink,
}; };
/** /**
@@ -94,4 +99,19 @@ export class Process implements CacheableObject {
*/ */
@link(PROCESS_OUTPUT_TYPE) @link(PROCESS_OUTPUT_TYPE)
output?: Observable<RemoteData<Bitstream>>; output?: Observable<RemoteData<Bitstream>>;
/**
* The files created by this Process
* Will be undefined unless the output {@link HALLink} has been resolved.
*/
@link(BITSTREAM, true)
files?: Observable<RemoteData<PaginatedList<Bitstream>>>;
/**
* The filetypes present in this Process
* Will be undefined unless the output {@link HALLink} has been resolved.
*/
@link(FILETYPES)
filetypes?: Observable<RemoteData<Filetypes>>;
} }

View File

@@ -0,0 +1,31 @@
import { Observable, of as observableOf } from 'rxjs';
import { CacheableObject } from '../../core/cache/cacheable-object.model';
import { ObjectCacheEntry } from '../../core/cache/object-cache.reducer';
/* eslint-disable @typescript-eslint/no-empty-function */
/**
* Stub class of {@link ObjectCacheService}
*/
export class ObjectCacheServiceStub {
add(_object: CacheableObject, _msToLive: number, _requestUUID: string, _alternativeLink?: string): void {
}
remove(_href: string): void {
}
getByHref(_href: string): Observable<ObjectCacheEntry> {
return observableOf(undefined);
}
hasByHref$(_href: string): Observable<boolean> {
return observableOf(false);
}
addDependency(_href$: string | Observable<string>, _dependsOnHref$: string | Observable<string>): void {
}
removeDependents(_href: string): void {
}
}