diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 098f075c10..bfd148e90a 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -21,6 +21,8 @@ import { RequestEntryState } from '../request-entry-state.model'; import { fakeAsync, tick } from '@angular/core/testing'; import { BaseDataService } from './base-data.service'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub'; +import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; const endpoint = 'https://rest.api/core'; @@ -46,7 +48,7 @@ describe('BaseDataService', () => { let requestService; let halService; let rdbService; - let objectCache; + let objectCache: ObjectCacheServiceStub; let selfLink; let linksToFollow; let testScheduler; @@ -56,24 +58,7 @@ describe('BaseDataService', () => { requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; rdbService = getMockRemoteDataBuildService(); - objectCache = { - - addPatch: () => { - /* empty */ - }, - getObjectBySelfLink: () => { - /* empty */ - }, - getByHref: () => { - /* empty */ - }, - addDependency: () => { - /* empty */ - }, - removeDependents: () => { - /* empty */ - }, - } as any; + objectCache = new ObjectCacheServiceStub(); selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), @@ -104,7 +89,7 @@ describe('BaseDataService', () => { return new TestService( requestService, rdbService, - objectCache, + objectCache as ObjectCacheService, halService, ); } @@ -567,7 +552,7 @@ describe('BaseDataService', () => { getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ requestUUIDs: ['request1', 'request2', 'request3'], dependentRequestUUIDs: ['request4', 'request5'] - })); + } as ObjectCacheEntry)); }); diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index edd6d9e2a4..473dbae0c7 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -24,6 +24,7 @@ import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALDataService } from './hal-data-service.interface'; import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { HALLink } from '../../shared/hal-link.model'; export const EMBED_SEPARATOR = '%2F'; /** @@ -268,7 +269,7 @@ export class BaseDataService implements HALDataServic this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - return this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( + const response$: Observable> = this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( // 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 // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a @@ -277,6 +278,22 @@ export class BaseDataService implements HALDataServic this.reRequestStaleRemoteData(reRequestOnStale, () => 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) => { + 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 implements HALDataServic this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - return this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( + const response$: Observable>> = this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( // 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 // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a @@ -311,6 +328,26 @@ export class BaseDataService implements HALDataServic this.reRequestStaleRemoteData(reRequestOnStale, () => 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>) => { + 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); + } + } + } + } + } + } + }), + ); } /** diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 65f8b3ab2c..c1a7ac64c2 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -24,6 +24,7 @@ import { testFindAllDataImplementation } from './base/find-all-data.spec'; import { testSearchDataImplementation } from './base/search-data.spec'; import { testPatchDataImplementation } from './base/patch-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; const url = 'fake-url'; const collectionId = 'fake-collection-id'; @@ -35,7 +36,7 @@ describe('CollectionDataService', () => { let translate: TranslateService; let notificationsService: any; let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; + let objectCache: ObjectCacheServiceStub; let halService: any; const mockCollection1: Collection = Object.assign(new Collection(), { @@ -205,14 +206,12 @@ describe('CollectionDataService', () => { buildFromRequestUUID: buildResponse$, buildSingle: buildResponse$ }); - objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') - }); + objectCache = new ObjectCacheServiceStub(); halService = new HALEndpointServiceStub(url); notificationsService = new NotificationsServiceStub(); 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); } }); diff --git a/src/app/core/data/relationship-data.service.spec.ts b/src/app/core/data/relationship-data.service.spec.ts index 4432d5213a..8ce67d19e0 100644 --- a/src/app/core/data/relationship-data.service.spec.ts +++ b/src/app/core/data/relationship-data.service.spec.ts @@ -23,6 +23,7 @@ import { FindListOptions } from './find-list-options.model'; import { testSearchDataImplementation } from './base/search-data.spec'; import { MetadataValue } from '../shared/metadata.models'; import { MetadataRepresentationType } from '../shared/metadata-representation/metadata-representation.model'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; describe('RelationshipDataService', () => { let service: RelationshipDataService; @@ -114,14 +115,7 @@ describe('RelationshipDataService', () => { 'href': buildList$, 'https://rest.api/core/publication/relationships': relationships$ }); - const objectCache = Object.assign({ - /* 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 objectCache = new ObjectCacheServiceStub(); const itemService = jasmine.createSpyObj('itemService', { findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)), @@ -133,7 +127,7 @@ describe('RelationshipDataService', () => { requestService, rdbService, halService, - objectCache, + objectCache as ObjectCacheService, itemService, null, jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v), diff --git a/src/app/core/data/relationship-type-data.service.spec.ts b/src/app/core/data/relationship-type-data.service.spec.ts index 6a788446d8..ecd84f8288 100644 --- a/src/app/core/data/relationship-type-data.service.spec.ts +++ b/src/app/core/data/relationship-type-data.service.spec.ts @@ -10,6 +10,7 @@ import { RequestService } from './request.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { hasValueOperator } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; describe('RelationshipTypeDataService', () => { let service: RelationshipTypeDataService; @@ -28,7 +29,7 @@ describe('RelationshipTypeDataService', () => { let buildList; let rdbService; - let objectCache; + let objectCache: ObjectCacheServiceStub; function init() { restEndpointURL = 'https://rest.api/relationshiptypes'; @@ -60,21 +61,14 @@ describe('RelationshipTypeDataService', () => { buildList = createSuccessfulRemoteDataObject(createPaginatedList([relationshipType1, relationshipType2])); rdbService = getMockRemoteDataBuildService(undefined, observableOf(buildList)); - objectCache = Object.assign({ - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - remove: () => { - }, - hasBySelfLinkObservable: () => observableOf(false) - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - }) as ObjectCacheService; - + objectCache = new ObjectCacheServiceStub(); } function initTestService() { return new RelationshipTypeDataService( requestService, rdbService, - objectCache, + objectCache as ObjectCacheService, halService, ); } diff --git a/src/app/core/resource-policy/resource-policy-data.service.spec.ts b/src/app/core/resource-policy/resource-policy-data.service.spec.ts index 7cfcaabb5d..e4c54d862c 100644 --- a/src/app/core/resource-policy/resource-policy-data.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy-data.service.spec.ts @@ -20,13 +20,14 @@ import { FindListOptions } from '../data/find-list-options.model'; import { EPersonDataService } from '../eperson/eperson-data.service'; import { GroupDataService } from '../eperson/group-data.service'; import { RestRequestMethod } from '../data/rest-request-method'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; describe('ResourcePolicyService', () => { let scheduler: TestScheduler; let service: ResourcePolicyDataService; let requestService: RequestService; let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; + let objectCache: ObjectCacheServiceStub; let halService: HALEndpointService; let responseCacheEntry: RequestEntry; let ePersonService: EPersonDataService; @@ -139,14 +140,14 @@ describe('ResourcePolicyService', () => { a: 'https://rest.api/rest/api/eperson/groups/' + groupUUID }), }); - objectCache = {} as ObjectCacheService; + objectCache = new ObjectCacheServiceStub(); const notificationsService = {} as NotificationsService; const comparator = {} as any; service = new ResourcePolicyDataService( requestService, rdbService, - objectCache, + objectCache as ObjectCacheService, halService, notificationsService, comparator, diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index faa5823520..e8ff2b479d 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -25,6 +25,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RequestEntry } from '../../data/request-entry.model'; import { VocabularyDataService } from './vocabulary.data.service'; import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service'; +import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub'; describe('VocabularyService', () => { let scheduler: TestScheduler; @@ -205,6 +206,7 @@ describe('VocabularyService', () => { function initTestService() { hrefOnlyDataService = getMockHrefOnlyDataService(); + objectCache = new ObjectCacheServiceStub() as ObjectCacheService; return new VocabularyService( requestService, diff --git a/src/app/core/supervision-order/supervision-order-data.service.spec.ts b/src/app/core/supervision-order/supervision-order-data.service.spec.ts index b12817fa1a..b25d440fa2 100644 --- a/src/app/core/supervision-order/supervision-order-data.service.spec.ts +++ b/src/app/core/supervision-order/supervision-order-data.service.spec.ts @@ -17,13 +17,14 @@ import { RestResponse } from '../cache/response.models'; import { RequestEntry } from '../data/request-entry.model'; import { FindListOptions } from '../data/find-list-options.model'; import { GroupDataService } from '../eperson/group-data.service'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; describe('SupervisionOrderService', () => { let scheduler: TestScheduler; let service: SupervisionOrderDataService; let requestService: RequestService; let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; + let objectCache: ObjectCacheServiceStub; let halService: HALEndpointService; let responseCacheEntry: RequestEntry; let groupService: GroupDataService; @@ -127,14 +128,14 @@ describe('SupervisionOrderService', () => { a: 'https://rest.api/rest/api/group/groups/' + groupUUID }), }); - objectCache = {} as ObjectCacheService; + objectCache = new ObjectCacheServiceStub(); const notificationsService = {} as NotificationsService; const comparator = {} as any; service = new SupervisionOrderDataService( requestService, rdbService, - objectCache, + objectCache as ObjectCacheService, halService, notificationsService, comparator, diff --git a/src/app/shared/testing/object-cache-service.stub.ts b/src/app/shared/testing/object-cache-service.stub.ts new file mode 100644 index 0000000000..f62f3575c3 --- /dev/null +++ b/src/app/shared/testing/object-cache-service.stub.ts @@ -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 { + return observableOf(undefined); + } + + hasByHref$(_href: string): Observable { + return observableOf(false); + } + + addDependency(_href$: string | Observable, _dependsOnHref$: string | Observable): void { + } + + removeDependents(_href: string): void { + } + +}