diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts deleted file mode 100644 index dc661e12d7..0000000000 --- a/src/app/core/data/data.service.spec.ts +++ /dev/null @@ -1,1011 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { HttpClient } from '@angular/common/http'; -import { Store } from '@ngrx/store'; -import { compare, Operation } from 'fast-json-patch'; -import { Observable, of as observableOf } from 'rxjs'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Item } from '../shared/item.model'; -import { - createFailedRemoteDataObject, - createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$, -} from '../../shared/remote-data.utils'; -import { ChangeAnalyzer } from './change-analyzer'; -import { DataService } from './data.service'; -import { PatchRequest } from './request.models'; -import { RequestService } from './request.service'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RequestParam } from '../cache/models/request-param.model'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from './remote-data'; -import { RequestEntryState } from './request-entry-state.model'; -import { CoreState } from '../core-state.model'; -import { FindListOptions } from './find-list-options.model'; -import { fakeAsync, tick } from '@angular/core/testing'; - -const endpoint = 'https://rest.api/core'; - -const BOOLEAN = { f: false, t: true }; - -class TestService extends DataService { - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected linkPath: string, - protected halService: HALEndpointService, - protected objectCache: ObjectCacheService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: ChangeAnalyzer - ) { - super(); - } - - public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return observableOf(endpoint); - } -} - -class DummyChangeAnalyzer implements ChangeAnalyzer { - diff(object1: Item, object2: Item): Operation[] { - return compare((object1 as any).metadata, (object2 as any).metadata); - } - -} - -describe('DataService', () => { - let service: TestService; - let options: FindListOptions; - let requestService; - let halService; - let rdbService; - let notificationsService; - let http; - let comparator; - let objectCache; - let store; - let selfLink; - let linksToFollow; - let testScheduler; - let remoteDataMocks; - - function initTestService(): TestService { - requestService = getMockRequestService(); - halService = new HALEndpointServiceStub('url') as any; - rdbService = getMockRemoteDataBuildService(); - notificationsService = {} as NotificationsService; - http = {} as HttpClient; - comparator = new DummyChangeAnalyzer() as any; - objectCache = { - - addPatch: () => { - /* empty */ - }, - getObjectBySelfLink: () => { - /* empty */ - }, - getByHref: () => { - /* empty */ - } - } as any; - store = {} as Store; - selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; - linksToFollow = [ - followLink('a'), - followLink('b') - ]; - - testScheduler = new TestScheduler((actual, expected) => { - // asserting the two objects are equal - // e.g. using chai. - expect(actual).toEqual(expected); - }); - - const timeStamp = new Date().getTime(); - const msToLive = 15 * 60 * 1000; - const payload = { foo: 'bar' }; - const statusCodeSuccess = 200; - const statusCodeError = 404; - const errorMessage = 'not found'; - remoteDataMocks = { - 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, payload, statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, 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( - requestService, - rdbService, - store, - endpoint, - halService, - objectCache, - notificationsService, - http, - comparator, - ); - } - - beforeEach(() => { - service = initTestService(); - }); - - describe('getFindAllHref', () => { - - it('should return an observable with the endpoint', () => { - options = {}; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(endpoint); - } - ); - }); - - it('should include page in href if currentPage provided in options', () => { - options = { currentPage: 2 }; - const expected = `${endpoint}?page=${options.currentPage - 1}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include size in href if elementsPerPage provided in options', () => { - options = { elementsPerPage: 5 }; - const expected = `${endpoint}?size=${options.elementsPerPage}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include sort href if SortOptions provided in options', () => { - const sortOptions = new SortOptions('field1', SortDirection.ASC); - options = { sort: sortOptions }; - const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include startsWith in href if startsWith provided in options', () => { - options = { startsWith: 'ab' }; - const expected = `${endpoint}?startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include all provided options in href', () => { - const sortOptions = new SortOptions('field1', SortDirection.DESC); - options = { - currentPage: 6, - elementsPerPage: 10, - sort: sortOptions, - startsWith: 'ab', - - }; - const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + - `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include all searchParams in href if any provided in options', () => { - options = { - searchParams: [ - new RequestParam('param1', 'test'), - new RequestParam('param2', 'test2'), - ] - }; - const expected = `${endpoint}?param1=test¶m2=test2`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include linkPath in href if any provided', () => { - const expected = `${endpoint}/test/entries`; - - (service as any).getFindAllHref({}, 'test/entries').subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include single linksToFollow as embed', () => { - const expected = `${endpoint}?embed=bundles`; - - (service as any).getFindAllHref({}, null, followLink('bundles')).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include single linksToFollow as embed and its size', () => { - const expected = `${endpoint}?embed.size=bundles=5&embed=bundles`; - const config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 5 - }); - (service as any).getFindAllHref({}, null, followLink('bundles', { findListOptions: config })).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include multiple linksToFollow as embed', () => { - const expected = `${endpoint}?embed=bundles&embed=owningCollection&embed=templateItemOf`; - - (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include multiple linksToFollow as embed and its sizes if given', () => { - const expected = `${endpoint}?embed=bundles&embed.size=owningCollection=2&embed=owningCollection&embed=templateItemOf`; - - const config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 2 - }); - - (service as any).getFindAllHref({}, null, followLink('bundles'), followLink('owningCollection', { findListOptions: config }), followLink('templateItemOf')).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should not include linksToFollow with shouldEmbed = false', () => { - const expected = `${endpoint}?embed=templateItemOf`; - - (service as any).getFindAllHref( - {}, - null, - followLink('bundles', { shouldEmbed: false }), - followLink('owningCollection', { shouldEmbed: false }), - followLink('templateItemOf') - ).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include nested linksToFollow 3lvl', () => { - const expected = `${endpoint}?embed=owningCollection/itemtemplate/relationships`; - - (service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include nested linksToFollow 2lvl and nested embed\'s size', () => { - const expected = `${endpoint}?embed.size=owningCollection/itemtemplate=4&embed=owningCollection/itemtemplate`; - const config: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 4 - }); - (service as any).getFindAllHref({}, null, followLink('owningCollection', {}, followLink('itemtemplate', { findListOptions: config }))).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - }); - - describe('getIDHref', () => { - const endpointMock = 'https://dspace7-internal.atmire.com/server/api/core/items'; - const resourceIdMock = '003c99b4-d4fe-44b0-a945-e12182a7ca89'; - - it('should return endpoint', () => { - const result = (service as any).getIDHref(endpointMock, resourceIdMock); - expect(result).toEqual(endpointMock + '/' + resourceIdMock); - }); - - it('should include single linksToFollow as embed', () => { - const expected = `${endpointMock}/${resourceIdMock}?embed=bundles`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles')); - expect(result).toEqual(expected); - }); - - it('should include multiple linksToFollow as embed', () => { - const expected = `${endpointMock}/${resourceIdMock}?embed=bundles&embed=owningCollection&embed=templateItemOf`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('bundles'), followLink('owningCollection'), followLink('templateItemOf')); - expect(result).toEqual(expected); - }); - - it('should not include linksToFollow with shouldEmbed = false', () => { - const expected = `${endpointMock}/${resourceIdMock}?embed=templateItemOf`; - const result = (service as any).getIDHref( - endpointMock, - resourceIdMock, - followLink('bundles', { shouldEmbed: false }), - followLink('owningCollection', { shouldEmbed: false }), - followLink('templateItemOf') - ); - expect(result).toEqual(expected); - }); - - it('should include nested linksToFollow 3lvl', () => { - const expected = `${endpointMock}/${resourceIdMock}?embed=owningCollection/itemtemplate/relationships`; - const result = (service as any).getIDHref(endpointMock, resourceIdMock, followLink('owningCollection', {}, followLink('itemtemplate', {}, followLink('relationships')))); - expect(result).toEqual(expected); - }); - }); - - describe('patch', () => { - const dso = { - uuid: 'dso-uuid' - }; - const operations = [ - Object.assign({ - op: 'move', - from: '/1', - path: '/5' - }) as Operation - ]; - - beforeEach(() => { - service.patch(dso, operations); - }); - - it('should send a PatchRequest', () => { - expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PatchRequest)); - }); - }); - - describe('update', () => { - let operations; - let dso; - let dso2; - const name1 = 'random string'; - const name2 = 'another random string'; - beforeEach(() => { - operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation]; - - dso = Object.assign(new DSpaceObject(), { - _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name1 }] - }); - - dso2 = Object.assign(new DSpaceObject(), { - _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name2 }] - }); - - spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); - spyOn(objectCache, 'addPatch'); - }); - - it('should call addPatch on the object cache with the right parameters when there are differences', () => { - service.update(dso2).subscribe(); - expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); - }); - - it('should not call addPatch on the object cache with the right parameters when there are no differences', () => { - service.update(dso).subscribe(); - expect(objectCache.addPatch).not.toHaveBeenCalled(); - }); - }); - - describe(`reRequestStaleRemoteData`, () => { - let callback: jasmine.Spy; - - beforeEach(() => { - callback = jasmine.createSpy(); - }); - - - describe(`when shouldReRequest is false`, () => { - it(`shouldn't do anything`, () => { - testScheduler.run(({ cold, expectObservable, flush }) => { - const expected = 'a-b-c-d-e-f'; - const values = { - a: remoteDataMocks.RequestPending, - b: remoteDataMocks.ResponsePending, - c: remoteDataMocks.Success, - d: remoteDataMocks.SuccessStale, - e: remoteDataMocks.Error, - f: remoteDataMocks.ErrorStale, - }; - - expectObservable((service as any).reRequestStaleRemoteData(false, callback)(cold(expected, values))).toBe(expected, values); - // since the callback happens in a tap(), flush to ensure it has been executed - flush(); - expect(callback).not.toHaveBeenCalled(); - }); - }); - }); - - describe(`when shouldReRequest is true`, () => { - it(`should call the callback for stale RemoteData objects, but still pass the source observable unmodified`, () => { - testScheduler.run(({ cold, expectObservable, flush }) => { - const expected = 'a-b'; - const values = { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.ErrorStale, - }; - - expectObservable((service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values))).toBe(expected, values); - // since the callback happens in a tap(), flush to ensure it has been executed - flush(); - expect(callback).toHaveBeenCalledTimes(2); - }); - }); - - it(`should only call the callback for stale RemoteData objects if something is subscribed to it`, (done) => { - testScheduler.run(({ cold, expectObservable }) => { - const expected = 'a'; - const values = { - a: remoteDataMocks.SuccessStale, - }; - - const result$ = (service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values)); - expectObservable(result$).toBe(expected, values); - expect(callback).not.toHaveBeenCalled(); - result$.subscribe(() => { - expect(callback).toHaveBeenCalled(); - done(); - }); - }); - }); - - it(`shouldn't do anything for RemoteData objects that aren't stale`, () => { - testScheduler.run(({ cold, expectObservable, flush }) => { - const expected = 'a-b-c-d'; - const values = { - a: remoteDataMocks.RequestPending, - b: remoteDataMocks.ResponsePending, - c: remoteDataMocks.Success, - d: remoteDataMocks.Error, - }; - - expectObservable((service as any).reRequestStaleRemoteData(true, callback)(cold(expected, values))).toBe(expected, values); - // since the callback happens in a tap(), flush to ensure it has been executed - flush(); - expect(callback).not.toHaveBeenCalled(); - }); - }); - }); - - }); - - describe(`findByHref`, () => { - beforeEach(() => { - spyOn(service as any, 'createAndSendGetRequest').and.callFake((href$) => { href$.subscribe().unsubscribe(); }); - }); - - it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => { - testScheduler.run(({ cold }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findByHref(selfLink, true, true, ...linksToFollow); - expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, {}, [], ...linksToFollow); - }); - }); - - it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findByHref(selfLink, true, true, ...linksToFollow); - expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true); - expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' }); - - service.findByHref(selfLink, false, true, ...linksToFollow); - expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false); - expectObservable(rdbService.buildSingle.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' }); - }); - }); - - it(`should call rdbService.buildSingle with the result from buildHrefFromFindOptions and linksToFollow`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findByHref(selfLink, true, true, ...linksToFollow); - expect(rdbService.buildSingle).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow); - expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' }); - }); - }); - - it(`should return a the output from reRequestStaleRemoteData`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' })); - const expected = 'a'; - const values = { - a: 'bingo!', - }; - - expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findByHref call as a callback`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale })); - - service.findByHref(selfLink, true, true, ...linksToFollow); - expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue(); - spyOn(service, 'findByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); - // prove that the spy we just added hasn't been called yet - expect(service.findByHref).not.toHaveBeenCalled(); - // call the callback passed to reRequestStaleRemoteData - (service as any).reRequestStaleRemoteData.calls.argsFor(0)[1](); - // verify that findByHref _has_ been called now, with the same params as the original call - expect(service.findByHref).toHaveBeenCalledWith(jasmine.anything(), true, true, ...linksToFollow); - // ... except for selflink, which will have been turned in to an observable. - expectObservable((service.findByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink }); - }); - }); - - describe(`when useCachedVersionIfAvailable is true`, () => { - beforeEach(() => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); - }); - - it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = 'a-b-c-d-e'; - const values = { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - }); - - describe(`when useCachedVersionIfAvailable is false`, () => { - beforeEach(() => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); - }); - - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - }); - - }); - - describe(`findAllByHref`, () => { - let findListOptions; - beforeEach(() => { - findListOptions = { currentPage: 5 }; - spyOn(service as any, 'createAndSendGetRequest').and.callFake((href$) => { href$.subscribe().unsubscribe(); }); - }); - - it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => { - testScheduler.run(({ cold }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow); - expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow); - }); - }); - - it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow); - expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true); - expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' }); - - service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow); - expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), false); - expectObservable(rdbService.buildList.calls.argsFor(1)[0]).toBe('(a|)', { a: 'bingo!' }); - }); - }); - - it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); - - service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow); - expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow); - expectObservable(rdbService.buildList.calls.argsFor(0)[0]).toBe('(a|)', { a: 'bingo!' }); - }); - }); - - it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findAllByHref call as a callback`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale })); - - service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow); - expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue(); - spyOn(service, 'findAllByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); - // prove that the spy we just added hasn't been called yet - expect(service.findAllByHref).not.toHaveBeenCalled(); - // call the callback passed to reRequestStaleRemoteData - (service as any).reRequestStaleRemoteData.calls.argsFor(0)[1](); - // verify that findAllByHref _has_ been called now, with the same params as the original call - expect(service.findAllByHref).toHaveBeenCalledWith(jasmine.anything(), findListOptions, true, true, ...linksToFollow); - // ... except for selflink, which will have been turned in to an observable. - expectObservable((service.findAllByHref as jasmine.Spy).calls.argsFor(0)[0]).toBe('(a|)', { a: selfLink }); - }); - }); - - it(`should return a the output from reRequestStaleRemoteData`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' })); - const expected = 'a'; - const values = { - a: 'bingo!', - }; - - expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - describe(`when useCachedVersionIfAvailable is true`, () => { - beforeEach(() => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); - }); - - it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = 'a-b-c-d-e'; - const values = { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findAllByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - }); - - describe(`when useCachedVersionIfAvailable is false`, () => { - beforeEach(() => { - spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); - }); - - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - })); - const expected = '--b-c-d-e'; - const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, - }; - - expectObservable(service.findAllByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); - }); - }); - - }); - }); - - describe('invalidateByHref', () => { - let getByHrefSpy: jasmine.Spy; - - beforeEach(() => { - getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ - requestUUIDs: ['request1', 'request2', 'request3'] - })); - - }); - - it('should call setStaleByUUID for every request associated with this DSO', (done) => { - service.invalidateByHref('some-href').subscribe((ok) => { - expect(ok).toBeTrue(); - expect(getByHrefSpy).toHaveBeenCalledWith('some-href'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3'); - done(); - }); - }); - - it('should call setStaleByUUID even if not subscribing to returned Observable', fakeAsync(() => { - service.invalidateByHref('some-href'); - tick(); - - expect(getByHrefSpy).toHaveBeenCalledWith('some-href'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request3'); - })); - - it('should return an Observable that only emits true once all requests are stale', () => { - testScheduler.run(({ cold, expectObservable }) => { - requestService.setStaleByUUID.and.callFake((uuid) => { - switch (uuid) { // fake requests becoming stale at different times - case 'request1': - return cold('--(t|)', BOOLEAN); - case 'request2': - return cold('----(t|)', BOOLEAN); - case 'request3': - return cold('------(t|)', BOOLEAN); - } - }); - - const done$ = service.invalidateByHref('some-href'); - - // emit true as soon as the final request is stale - expectObservable(done$).toBe('------(t|)', BOOLEAN); - }); - }); - - it('should only fire for the current state of the object (instead of tracking it)', () => { - testScheduler.run(({ cold, flush }) => { - getByHrefSpy.and.returnValue(cold('a---b---c---', { - a: { requestUUIDs: ['request1'] }, // this is the state at the moment we're invalidating the cache - b: { requestUUIDs: ['request2'] }, // we shouldn't keep tracking the state - c: { requestUUIDs: ['request3'] }, // because we may invalidate when we shouldn't - })); - - service.invalidateByHref('some-href'); - flush(); - - // requests from the first state are marked as stale - expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); - - // request from subsequent states are ignored - expect(requestService.setStaleByUUID).not.toHaveBeenCalledWith('request2'); - expect(requestService.setStaleByUUID).not.toHaveBeenCalledWith('request3'); - }); - }); - }); - - describe('delete', () => { - let MOCK_SUCCEEDED_RD; - let MOCK_FAILED_RD; - - let invalidateByHrefSpy: jasmine.Spy; - let buildFromRequestUUIDSpy: jasmine.Spy; - let getIDHrefObsSpy: jasmine.Spy; - let deleteByHrefSpy: jasmine.Spy; - - beforeEach(() => { - invalidateByHrefSpy = spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); - buildFromRequestUUIDSpy = spyOn(rdbService, 'buildFromRequestUUID').and.callThrough(); - getIDHrefObsSpy = spyOn(service, 'getIDHrefObs').and.callThrough(); - deleteByHrefSpy = spyOn(service, 'deleteByHref').and.callThrough(); - - MOCK_SUCCEEDED_RD = createSuccessfulRemoteDataObject({}); - MOCK_FAILED_RD = createFailedRemoteDataObject('something went wrong'); - }); - - it('should retrieve href by ID and call deleteByHref', () => { - getIDHrefObsSpy.and.returnValue(observableOf('some-href')); - buildFromRequestUUIDSpy.and.returnValue(createSuccessfulRemoteDataObject$({})); - - service.delete('some-id', ['a', 'b', 'c']).subscribe(rd => { - expect(getIDHrefObsSpy).toHaveBeenCalledWith('some-id'); - expect(deleteByHrefSpy).toHaveBeenCalledWith('some-href', ['a', 'b', 'c']); - }); - }); - - describe('deleteByHref', () => { - it('should call invalidateByHref if the DELETE request succeeds', (done) => { - buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); - - 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(() => { - buildFromRequestUUIDSpy.and.returnValue(observableOf(MOCK_SUCCEEDED_RD)); - - service.deleteByHref('some-href'); - tick(); - - 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', () => { - testScheduler.run(({ cold, expectObservable }) => { - 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'); - expectObservable(done$).toBe( - '----(r|)', { r: MOCK_SUCCEEDED_RD} // ...and expect the returned Observable to wait until that's done - ); - }); - }); - - 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 - ); - }); - }); - }); - }); -}); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts deleted file mode 100644 index 6176694d9d..0000000000 --- a/src/app/core/data/data.service.ts +++ /dev/null @@ -1,693 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Store } from '@ngrx/store'; -import { Operation } from 'fast-json-patch'; -import { AsyncSubject, combineLatest, from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { - distinctUntilChanged, - filter, - find, - map, - mergeMap, - skipWhile, - switchMap, - take, - takeWhile, - tap, - toArray -} from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { getClassForType } from '../cache/builders/build-decorators'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { RequestParam } from '../cache/models/request-param.model'; -import { ObjectCacheEntry } from '../cache/object-cache.reducer'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { ChangeAnalyzer } from './change-analyzer'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { CreateRequest, DeleteRequest, GetRequest, PatchRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; -import { RestRequestMethod } from './rest-request-method'; -import { UpdateDataService } from './update-data.service'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { NoContent } from '../shared/NoContent.model'; -import { CacheableObject } from '../cache/cacheable-object.model'; -import { CoreState } from '../core-state.model'; -import { FindListOptions } from './find-list-options.model'; - -export abstract class DataService implements UpdateDataService { - protected abstract requestService: RequestService; - protected abstract rdbService: RemoteDataBuildService; - protected abstract store: Store; - protected abstract linkPath: string; - protected abstract halService: HALEndpointService; - protected abstract objectCache: ObjectCacheService; - protected abstract notificationsService: NotificationsService; - protected abstract http: HttpClient; - protected abstract comparator: ChangeAnalyzer; - - /** - * Allows subclasses to reset the response cache time. - */ - protected responseMsToLive: number; - - /** - * Get the endpoint for browsing - * @param options The [[FindListOptions]] object - * @param linkPath The link path for the object - * @returns {Observable} - */ - getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { - return this.getEndpoint(); - } - - /** - * Get the base endpoint for all requests - */ - protected getEndpoint(): Observable { - return this.halService.getEndpoint(this.linkPath); - } - - /** - * Create the HREF with given options object - * - * @param options The [[FindListOptions]] object - * @param linkPath The link path for the object - * @return {Observable} - * Return an observable that emits created HREF - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig[]): Observable { - let endpoint$: Observable; - const args = []; - - endpoint$ = this.getBrowseEndpoint(options).pipe( - filter((href: string) => isNotEmpty(href)), - map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href), - distinctUntilChanged() - ); - - return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); - } - - /** - * Create the HREF for a specific object's search method with given options object - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @return {Observable} - * Return an observable that emits created HREF - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable { - let result$: Observable; - const args = []; - - result$ = this.getSearchEndpoint(searchMethod); - - return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); - } - - /** - * Turn an options object into a query string and combine it with the given HREF - * - * @param href The HREF to which the query string should be appended - * @param options The [[FindListOptions]] object - * @param extraArgs Array with additional params to combine with query string - * @return {Observable} - * Return an observable that emits created HREF - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: FollowLinkConfig[]): string { - let args = [...extraArgs]; - - if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { - /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ - args = this.addHrefArg(href, args, `page=${options.currentPage - 1}`); - } - if (hasValue(options.elementsPerPage)) { - args = this.addHrefArg(href, args, `size=${options.elementsPerPage}`); - } - if (hasValue(options.sort)) { - args = this.addHrefArg(href, args, `sort=${options.sort.field},${options.sort.direction}`); - } - if (hasValue(options.startsWith)) { - args = this.addHrefArg(href, args, `startsWith=${options.startsWith}`); - } - if (hasValue(options.searchParams)) { - options.searchParams.forEach((param: RequestParam) => { - args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`); - }); - } - args = this.addEmbedParams(href, args, ...linksToFollow); - if (isNotEmpty(args)) { - return new URLCombiner(href, `?${args.join('&')}`).toString(); - } else { - return href; - } - } - - /** - * Turn an array of RequestParam into a query string and combine it with the given HREF - * - * @param href The HREF to which the query string should be appended - * @param params Array with additional params to combine with query string - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - * - * @return {Observable} - * Return an observable that emits created HREF - */ - buildHrefWithParams(href: string, params: RequestParam[], ...linksToFollow: FollowLinkConfig[]): string { - - let args = []; - if (hasValue(params)) { - params.forEach((param: RequestParam) => { - args = this.addHrefArg(href, args, `${param.fieldName}=${param.fieldValue}`); - }); - } - - args = this.addEmbedParams(href, args, ...linksToFollow); - - if (isNotEmpty(args)) { - return new URLCombiner(href, `?${args.join('&')}`).toString(); - } else { - return href; - } - } - /** - * Adds the embed options to the link for the request - * @param href The href the params are to be added to - * @param args params for the query string - * @param linksToFollow links we want to embed in query string if shouldEmbed is true - */ - protected addEmbedParams(href: string, args: string[], ...linksToFollow: FollowLinkConfig[]) { - linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { - if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) { - const embedString = 'embed=' + String(linkToFollow.name); - // Add the embeds size if given in the FollowLinkConfig.FindListOptions - if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) { - args = this.addHrefArg(href, args, - 'embed.size=' + String(linkToFollow.name) + '=' + linkToFollow.findListOptions.elementsPerPage); - } - // Adds the nested embeds and their size if given - if (isNotEmpty(linkToFollow.linksToFollow)) { - args = this.addNestedEmbeds(embedString, href, args, ...linkToFollow.linksToFollow); - } else { - args = this.addHrefArg(href, args, embedString); - } - } - }); - return args; - } - - /** - * Add a new argument to the list of arguments, only if it doesn't already exist in the given href, - * or the current list of arguments - * - * @param href The href the arguments are to be added to - * @param currentArgs The current list of arguments - * @param newArg The new argument to add - * @return The next list of arguments, with newArg included if it wasn't already. - * Note this function will not modify any of the input params. - */ - protected addHrefArg(href: string, currentArgs: string[], newArg: string): string[] { - if (href.includes(newArg) || currentArgs.includes(newArg)) { - return [...currentArgs]; - } else { - return [...currentArgs, newArg]; - } - } - - /** - * Add the nested followLinks to the embed param, separated by a /, and their sizes, recursively - * @param embedString embedString so far (recursive) - * @param href The href the params are to be added to - * @param args params for the query string - * @param linksToFollow links we want to embed in query string if shouldEmbed is true - */ - protected addNestedEmbeds(embedString: string, href: string, args: string[], ...linksToFollow: FollowLinkConfig[]): string[] { - let nestEmbed = embedString; - linksToFollow.forEach((linkToFollow: FollowLinkConfig) => { - if (hasValue(linkToFollow) && linkToFollow.shouldEmbed) { - nestEmbed = nestEmbed + '/' + String(linkToFollow.name); - // Add the nested embeds size if given in the FollowLinkConfig.FindListOptions - if (hasValue(linkToFollow.findListOptions) && hasValue(linkToFollow.findListOptions.elementsPerPage)) { - const nestedEmbedSize = 'embed.size=' + nestEmbed.split('=')[1] + '=' + linkToFollow.findListOptions.elementsPerPage; - args = this.addHrefArg(href, args, nestedEmbedSize); - } - if (hasValue(linkToFollow.linksToFollow) && isNotEmpty(linkToFollow.linksToFollow)) { - args = this.addNestedEmbeds(nestEmbed, href, args, ...linkToFollow.linksToFollow); - } else { - args = this.addHrefArg(href, args, nestEmbed); - } - } - }); - return args; - } - - /** - * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded - * info should be added to the objects - * - * @param options Find list options object - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - * @param reRequestOnStale Whether or not the request should automatically be re- - * requested after the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which - * {@link HALLink}s should be automatically resolved - * @return {Observable>>} - * Return an observable that emits object list - */ - findAll(options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - return this.findAllByHref(this.getFindAllHref(options), options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } - - /** - * Create the HREF for a specific object based on its identifier; with possible embed query params based on linksToFollow - * @param endpoint The base endpoint for the type of object - * @param resourceID The identifier for the object - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - getIDHref(endpoint, resourceID, ...linksToFollow: FollowLinkConfig[]): string { - return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow); - } - - /** - * Create an observable for the HREF of a specific object based on its identifier - * @param resourceID The identifier for the object - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - */ - getIDHrefObs(resourceID: string, ...linksToFollow: FollowLinkConfig[]): Observable { - return this.getEndpoint().pipe( - map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow))); - } - - /** - * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of - * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param id ID of object we want to retrieve - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - * @param reRequestOnStale Whether or not the request should automatically be re- - * requested after the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which - * {@link HALLink}s should be automatically resolved - */ - findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - const href$ = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow); - return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } - - /** - * An operator that will call the given function if the incoming RemoteData is stale and - * shouldReRequest is true - * - * @param shouldReRequest Whether or not to call the re-request function if the RemoteData is stale - * @param requestFn The function to call if the RemoteData is stale and shouldReRequest is - * true - */ - protected reRequestStaleRemoteData(shouldReRequest: boolean, requestFn: () => Observable>) { - return (source: Observable>): Observable> => { - if (shouldReRequest === true) { - return source.pipe( - tap((remoteData: RemoteData) => { - if (hasValue(remoteData) && remoteData.isStale) { - requestFn(); - } - }) - ); - } else { - return source; - } - }; - } - - /** - * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of - * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href$ The url of object we want to retrieve. Can be a string or - * an Observable - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - * @param reRequestOnStale Whether or not the request should automatically be re- - * requested after the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which - * {@link HALLink}s should be automatically resolved - */ - findByHref(href$: string | Observable, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - if (typeof href$ === 'string') { - href$ = observableOf(href$); - } - - const requestHref$ = href$.pipe( - isNotEmptyOperator(), - take(1), - map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)) - ); - - this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - - return 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 - // cached completed object - skipWhile((rd: RemoteData) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), - this.reRequestStaleRemoteData(reRequestOnStale, () => - this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) - ); - } - - /** - * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list - * of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object - * @param href$ The url of object we want to retrieve. Can be a string or - * an Observable - * @param findListOptions Find list options object - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - * @param reRequestOnStale Whether or not the request should automatically be re- - * requested after the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which - * {@link HALLink}s should be automatically resolved - */ - findAllByHref(href$: string | Observable, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - if (typeof href$ === 'string') { - href$ = observableOf(href$); - } - - const requestHref$ = href$.pipe( - isNotEmptyOperator(), - take(1), - map((href: string) => this.buildHrefFromFindOptions(href, findListOptions, [], ...linksToFollow)) - ); - - this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - - return 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 - // cached completed object - skipWhile((rd: RemoteData>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), - this.reRequestStaleRemoteData(reRequestOnStale, () => - this.findAllByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) - ); - } - - /** - * Create a GET request for the given href, and send it. - * - * @param href$ The url of object we want to retrieve. Can be a string or - * an Observable - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - */ - protected createAndSendGetRequest(href$: string | Observable, useCachedVersionIfAvailable = true): void { - if (isNotEmpty(href$)) { - if (typeof href$ === 'string') { - href$ = observableOf(href$); - } - - href$.pipe( - isNotEmptyOperator(), - take(1) - ).subscribe((href: string) => { - const requestId = this.requestService.generateRequestId(); - const request = new GetRequest(requestId, href); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.send(request, useCachedVersionIfAvailable); - }); - } - } - - /** - * Return object search endpoint by given search method - * - * @param searchMethod The search method for the object - */ - protected getSearchEndpoint(searchMethod: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - filter((href: string) => isNotEmpty(href)), - map((href: string) => `${href}/search/${searchMethod}`)); - } - - /** - * Make a new FindListRequest with given search method - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's - * no valid cached version. Defaults to true - * @param reRequestOnStale Whether or not the request should automatically be re- - * requested after the response becomes stale - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which - * {@link HALLink}s should be automatically resolved - * @return {Observable>} - * Return an observable that emits response from the server - */ - searchBy(searchMethod: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - - return this.findAllByHref(hrefObs, undefined, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - } - - /** - * Send a patch request for a specified object - * @param {T} object The object to send a patch request for - * @param {Operation[]} operations The patch operations to be performed - */ - patch(object: T, operations: Operation[]): Observable> { - const requestId = this.requestService.generateRequestId(); - - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, object.uuid))); - - hrefObs.pipe( - find((href: string) => hasValue(href)), - ).subscribe((href: string) => { - const request = new PatchRequest(requestId, href, operations); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.send(request); - }); - - return this.rdbService.buildFromRequestUUID(requestId); - } - - createPatchFromCache(object: T): Observable { - const oldVersion$ = this.findByHref(object._links.self.href, true, false); - return oldVersion$.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((oldVersion: T) => this.comparator.diff(oldVersion, object))); - } - - /** - * Send a PUT request for the specified object - * - * @param object The object to send a put request for. - */ - put(object: T): Observable> { - const requestId = this.requestService.generateRequestId(); - const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); - const request = new PutRequest(requestId, object._links.self.href, serializedObject); - - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - - this.requestService.send(request); - - return this.rdbService.buildFromRequestUUID(requestId); - } - - /** - * Add a new patch to the object cache - * The patch is derived from the differences between the given object and its version in the object cache - * @param {DSpaceObject} object The given object - */ - update(object: T): Observable> { - return this.createPatchFromCache(object) - .pipe( - mergeMap((operations: Operation[]) => { - if (isNotEmpty(operations)) { - this.objectCache.addPatch(object._links.self.href, operations); - } - return this.findByHref(object._links.self.href, true, true); - } - ) - ); - } - - /** - * Create a new DSpaceObject on the server, and store the response - * in the object cache - * - * @param {CacheableObject} object - * The object to create - * @param {RequestParam[]} params - * Array with additional params to combine with query string - */ - create(object: T, ...params: RequestParam[]): Observable> { - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.getEndpoint().pipe( - isNotEmptyOperator(), - distinctUntilChanged(), - map((endpoint: string) => this.buildHrefWithParams(endpoint, params)) - ); - - const serializedObject = new DSpaceSerializer(getClassForType(object.type)).serialize(object); - - endpoint$.pipe( - take(1) - ).subscribe((endpoint: string) => { - const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedObject)); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.send(request); - }); - - const result$ = this.rdbService.buildFromRequestUUID(requestId); - - // TODO a dataservice is not the best place to show a notification, - // this should move up to the components that use this method - result$.pipe( - takeWhile((rd: RemoteData) => rd.isLoading, true) - ).subscribe((rd: RemoteData) => { - if (rd.hasFailed) { - this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1)); - } - }); - - return result$; - } - - /** - * Invalidate an existing DSpaceObject by marking all requests it is included in as stale - * @param objectId The id of the object to be invalidated - * @return An Observable that will emit `true` once all requests are stale - */ - invalidate(objectId: string): Observable { - return this.getIDHrefObs(objectId).pipe( - switchMap((href: string) => this.invalidateByHref(href)) - ); - } - - /** - * Invalidate an existing DSpaceObject by marking all requests it is included in as stale - * @param href The self link of the object to be invalidated - * @return An Observable that will emit `true` once all requests are stale - */ - invalidateByHref(href: string): Observable { - const done$ = new AsyncSubject(); - - this.objectCache.getByHref(href).pipe( - take(1), - switchMap((oce: ObjectCacheEntry) => observableFrom(oce.requestUUIDs).pipe( - mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), - toArray(), - )), - ).subscribe(() => { - done$.next(true); - done$.complete(); - }); - - return done$; - } - - /** - * Delete an existing DSpace Object on the server - * @param objectId The id of the object to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, - * errorMessage, timeCompleted, etc - */ - delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { - return this.getIDHrefObs(objectId).pipe( - switchMap((href: string) => this.deleteByHref(href, copyVirtualMetadata)) - ); - } - - /** - * Delete an existing DSpace Object on the server - * @param href The self link of the object to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * @return A RemoteData observable with an empty payload, but still representing the state of the request: statusCode, - * errorMessage, timeCompleted, etc - * Only emits once all request related to the DSO has been invalidated. - */ - deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { - const requestId = this.requestService.generateRequestId(); - - if (copyVirtualMetadata) { - copyVirtualMetadata.forEach((id) => - href += (href.includes('?') ? '&' : '?') - + 'copyVirtualMetadata=' - + id - ); - } - - const request = new DeleteRequest(requestId, href); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } - this.requestService.send(request); - - const response$ = this.rdbService.buildFromRequestUUID(requestId); - - const invalidated$ = new AsyncSubject(); - response$.pipe( - getFirstCompletedRemoteData(), - switchMap((rd: RemoteData) => { - if (rd.hasSucceeded) { - return this.invalidateByHref(href); - } else { - return [true]; - } - }) - ).subscribe(() => { - invalidated$.next(true); - invalidated$.complete(); - }); - - return combineLatest([response$, invalidated$]).pipe( - filter(([_, invalidated]) => invalidated), - map(([response, _]) => response), - ); - } - - /** - * Commit current object changes to the server - * @param method The RestRequestMethod for which de server sync buffer should be committed - */ - commitUpdates(method?: RestRequestMethod) { - this.requestService.commit(method); - } - - /** - * Return the links to traverse from the root of the api to the - * endpoint this DataService represents - * - * e.g. if the api root links to 'foo', and the endpoint at 'foo' - * links to 'bar' the linkPath for the BarDataService would be - * 'foo/bar' - */ - getLinkPath(): string { - return this.linkPath; - } -}