finished up response cache refactoring

This commit is contained in:
lotte
2018-10-18 15:59:41 +02:00
parent 2330e96158
commit ec5f977dd2
17 changed files with 174 additions and 150 deletions

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, Subscription } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list';
@@ -15,7 +15,7 @@ import { Item } from '../core/shared/item.model';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { filter, first, flatMap, map } from 'rxjs/operators';
import { filter, flatMap, map, tap } from 'rxjs/operators';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { toDSpaceObjectListRD } from '../core/shared/operators';

View File

@@ -84,8 +84,8 @@ describe('SearchService', () => {
const remoteDataBuildService = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
return observableCombineLatest(requestEntryObs, payloadObs).pipe(
map(([req, res, pay]) => {
return { req, res, pay };
map(([req, pay]) => {
return { req, pay };
})
);
},
@@ -175,9 +175,6 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
});
});
describe('when getConfig is called without a scope', () => {
@@ -203,9 +200,6 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
});
});
describe('when getConfig is called with a scope', () => {
@@ -233,9 +227,6 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl);
});
});
});
});

View File

@@ -7,7 +7,7 @@ import {
Router,
UrlSegmentGroup
} from '@angular/router';
import { filter, flatMap, map, switchMap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import {
FacetConfigSuccessResponse,
@@ -47,7 +47,6 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { ViewMode } from '../../core/shared/view-mode.model';
import { ResourceType } from '../../core/shared/resource-type';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { RequestEntry } from '../../core/data/request.reducer';
/**
* Service that performs all general actions that have to do with the search page
@@ -100,7 +99,7 @@ export class SearchService implements OnDestroy {
configureRequest(this.requestService)
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache
@@ -113,10 +112,11 @@ export class SearchService implements OnDestroy {
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
map((sqr: SearchQueryResponse) => {
return sqr.objects.map((nsr: NormalizedSearchResult) =>
this.rdb.buildSingle(nsr.dspaceObject));
return sqr.objects.map((nsr: NormalizedSearchResult) => {
return this.rdb.buildSingle(nsr.dspaceObject);
})
}),
flatMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
);
// Create search results again with the correct dso objects linked to each result
@@ -180,7 +180,7 @@ export class SearchService implements OnDestroy {
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache
@@ -223,7 +223,7 @@ export class SearchService implements OnDestroy {
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache

View File

@@ -1,7 +1,6 @@
import { AuthStatusResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
@@ -11,7 +10,7 @@ import { ObjectCacheState } from '../cache/object-cache.reducer';
describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService;
const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig;
const EnvConfig = { cache: { msToLive: 1000 } } as any;
const store = new MockStore<ObjectCacheState>({});
const objectCacheService = new ObjectCacheService(store as any);

View File

@@ -8,6 +8,8 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service';
import { RequestEntry } from '../data/request.reducer';
import { of as observableOf } from 'rxjs';
describe('BrowseService', () => {
let scheduler: TestScheduler;
@@ -76,7 +78,11 @@ describe('BrowseService', () => {
})
];
const getRequestEntry$ = (successful: boolean) => {
return observableOf({
response: { isSuccessful: successful, payload: browseDefinitions } as any
} as RequestEntry)
};
function initTestService() {
return new BrowseService(
@@ -93,7 +99,7 @@ describe('BrowseService', () => {
describe('getBrowseDefinitions', () => {
beforeEach(() => {
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(halService, 'getEndpoint').and
@@ -131,7 +137,7 @@ describe('BrowseService', () => {
const mockAuthorName = 'Donald Smith';
beforeEach(() => {
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
@@ -204,7 +210,7 @@ describe('BrowseService', () => {
describe('if getBrowseDefinitions fires', () => {
beforeEach(() => {
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
@@ -259,7 +265,7 @@ describe('BrowseService', () => {
describe('if getBrowseDefinitions doesn\'t fire', () => {
it('should return undefined', () => {
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import {
ensureArrayHasValue, hasValue,
ensureArrayHasValue,
hasValueOperator,
isEmpty,
isNotEmpty,
@@ -34,7 +34,6 @@ import {
import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { RequestEntry } from '../data/request.reducer';
@Injectable()
export class BrowseService {

View File

@@ -5,7 +5,7 @@ import {
race as observableRace
} from 'rxjs';
import { Injectable } from '@angular/core';
import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators';
import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
@@ -32,6 +32,8 @@ export class RemoteDataBuildService {
}
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
console.log('call buildSingle', href$);
if (typeof href$ === 'string') {
href$ = observableOf(href$);
}
@@ -42,19 +44,18 @@ export class RemoteDataBuildService {
const requestEntry$ = observableRace(
href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService))
);
requestHref$.pipe(getRequestFromSelflink(this.requestService)),
).pipe(first());
// always use self link if that is cached, only if it isn't, get it via the response.
const payload$ =
observableCombineLatest(
href$.pipe(
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined)
),
switchMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined)),
requestEntry$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceSelfLinks: string[]) => {
switchMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
} else {
@@ -83,7 +84,7 @@ export class RemoteDataBuildService {
}
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
return observableCombineLatest(requestEntry$, requestEntry$.pipe(startWith(undefined)), payload$).pipe(
return observableCombineLatest(requestEntry$, payload$).pipe(
map(([reqEntry, payload]) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
@@ -124,9 +125,8 @@ export class RemoteDataBuildService {
}));
}),
startWith([]),
distinctUntilChanged()
distinctUntilChanged(),
);
// tDomainList$.subscribe((t) => {console.log('domainlist', t)});
const pageInfo$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((response: DSOSuccessResponse) => {
@@ -152,7 +152,6 @@ export class RemoteDataBuildService {
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
const links: any = {};
const relationships = getRelationships(normalized.constructor) || [];
relationships.forEach((relationship: string) => {
@@ -196,8 +195,6 @@ export class RemoteDataBuildService {
}
});
const domainModel = getMapsTo(normalized.constructor);
// console.log('domain model', normalized);
return Object.assign(new domainModel(), normalized, links);
}

View File

@@ -12,6 +12,8 @@ import { FindAllOptions, FindByIDRequest } from './request.models';
import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestEntry } from './request.reducer';
import { of as observableOf } from 'rxjs';
const LINK_NAME = 'test';
@@ -34,6 +36,7 @@ class TestService extends ComColDataService<NormalizedTestObject, any> {
super();
}
}
/* tslint:enable:max-classes-per-file */
describe('ComColDataService', () => {
@@ -52,6 +55,11 @@ describe('ComColDataService', () => {
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID
});
const getRequestEntry$ = (successful: boolean) => {
return observableOf({
response: { isSuccessful: successful } as any
} as RequestEntry)
};
const communitiesEndpoint = 'https://rest.api/core/communities';
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
@@ -97,7 +105,7 @@ describe('ComColDataService', () => {
it('should configure a new FindByIDRequest for the scope Community', () => {
cds = initMockCommunityDataService();
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
objectCache = initMockObjectCacheService();
service = initTestService();
@@ -112,7 +120,7 @@ describe('ComColDataService', () => {
describe('if the scope Community can be found', () => {
beforeEach(() => {
cds = initMockCommunityDataService();
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
objectCache = initMockObjectCacheService();
service = initTestService();
});
@@ -125,16 +133,16 @@ describe('ComColDataService', () => {
it('should return the endpoint to fetch resources within the given scope', () => {
const result = service.getBrowseEndpoint(options);
const expected = cold('--e-', { e: scopedEndpoint });
const expected = '--e-';
expect(result).toBeObservable(expected);
scheduler.expectObservable(result).toBe(expected, { e: scopedEndpoint });
});
});
describe('if the scope Community can\'t be found', () => {
beforeEach(() => {
cds = initMockCommunityDataService();
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(false));
objectCache = initMockObjectCacheService();
service = initTestService();
});

View File

@@ -2,16 +2,20 @@ import * as deepFreeze from 'deep-freeze';
import { requestReducer, RequestState } from './request.reducer';
import {
RequestCompleteAction, RequestConfigureAction, RequestExecuteAction
RequestCompleteAction,
RequestConfigureAction,
RequestExecuteAction, ResetResponseTimestampsAction
} from './request.actions';
import { GetRequest, RestRequest } from './request.models';
import { GetRequest } from './request.models';
import { RestResponse } from '../cache/response.models';
const response = new RestResponse(true, 'OK');
class NullAction extends RequestCompleteAction {
type = null;
payload = null;
constructor() {
super(null);
super(null, null);
}
}
@@ -25,7 +29,8 @@ describe('requestReducer', () => {
request: new GetRequest(id1, link1),
requestPending: false,
responsePending: false,
completed: false
completed: false,
response: undefined
}
};
deepFreeze(testState);
@@ -56,6 +61,7 @@ describe('requestReducer', () => {
expect(newState[id2].requestPending).toEqual(true);
expect(newState[id2].responsePending).toEqual(false);
expect(newState[id2].completed).toEqual(false);
expect(newState[id2].response).toEqual(undefined);
});
it('should set \'requestPending\' to false, \'responsePending\' to true and leave \'completed\' untouched for the given RestRequest in the state, in response to an EXECUTE action', () => {
@@ -69,11 +75,13 @@ describe('requestReducer', () => {
expect(newState[id1].requestPending).toEqual(false);
expect(newState[id1].responsePending).toEqual(true);
expect(newState[id1].completed).toEqual(state[id1].completed);
expect(newState[id1].response).toEqual(undefined)
});
it('should leave \'requestPending\' untouched, set \'responsePending\' to false and \'completed\' to true for the given RestRequest in the state, in response to a COMPLETE action', () => {
const state = testState;
const action = new RequestCompleteAction(id1);
const action = new RequestCompleteAction(id1, response);
const newState = requestReducer(state, action);
expect(newState[id1].request.uuid).toEqual(id1);
@@ -81,5 +89,25 @@ describe('requestReducer', () => {
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
expect(newState[id1].responsePending).toEqual(false);
expect(newState[id1].completed).toEqual(true);
expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful)
expect(newState[id1].response.statusCode).toEqual(response.statusCode)
expect(newState[id1].response.timeAdded).toBeTruthy()
});
it('should leave \'requestPending\' untouched, should leave \'responsePending\' untouched and leave \'completed\' untouched, but update the response\'s timeAdded for the given RestRequest in the state, in response to a COMPLETE action', () => {
const update = Object.assign({}, testState[id1], {response});
const state = Object.assign({}, testState, {[id1]: update});
const timeStamp = 1000;
const action = new ResetResponseTimestampsAction(timeStamp);
const newState = requestReducer(state, action);
expect(newState[id1].request.uuid).toEqual(state[id1].request.uuid);
expect(newState[id1].request.href).toEqual(state[id1].request.href);
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
expect(newState[id1].responsePending).toEqual(state[id1].responsePending);
expect(newState[id1].completed).toEqual(state[id1].completed);
expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful);
expect(newState[id1].response.statusCode).toEqual(response.statusCode);
expect(newState[id1].response.timeAdded).toBe(timeStamp);
});
});

View File

@@ -56,12 +56,13 @@ function configureRequest(state: RequestState, action: RequestConfigureAction):
}
function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState {
return Object.assign({}, state, {
const obs = Object.assign({}, state, {
[action.payload]: Object.assign({}, state[action.payload], {
requestPending: false,
responsePending: true
})
});
return obs;
}
/**
@@ -76,16 +77,13 @@ function executeRequest(state: RequestState, action: RequestExecuteAction): Requ
*/
function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
const time = new Date().getTime();
const ob = Object.assign({}, state, {
return Object.assign({}, state, {
[action.payload.uuid]: Object.assign({}, state[action.payload.uuid], {
responsePending: false,
completed: true,
response: Object.assign({}, action.payload.response, { timeAdded: time })
})
});
console.log(ob);
return ob;
}
function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) {

View File

@@ -46,13 +46,10 @@ describe('RequestService', () => {
objectCache = getMockObjectCacheService();
(objectCache.hasBySelfLink as any).and.returnValue(false);
(responseCache.has as any).and.returnValue(false);
(responseCache.get as any).and.returnValue(observableOf(undefined));
uuidService = getMockUUIDService();
store = new Store<CoreState>(new BehaviorSubject({}), new ActionsSubject(), null);
selectSpy = spyOnProperty(ngrx, 'select')
selectSpy = spyOnProperty(ngrx, 'select');
selectSpy.and.callFake(() => {
return () => {
return () => cold('a', { a: undefined });
@@ -322,7 +319,7 @@ describe('RequestService', () => {
describe('when the request is cached', () => {
describe('in the ObjectCache', () => {
beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValues(true);
(objectCache.hasBySelfLink as any).and.returnValue(true);
});
it('should return true', () => {
@@ -334,12 +331,13 @@ describe('RequestService', () => {
});
describe('in the responseCache', () => {
beforeEach(() => {
(responseCache.has as any).and.returnValues(true);
spyOn(serviceAsAny, 'isReusable').and.returnValue(observableOf(true));
spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(undefined));
});
describe('and it\'s a DSOSuccessResponse', () => {
beforeEach(() => {
(responseCache.get as any).and.returnValues(observableOf({
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
response: {
isSuccessful: true,
resourceSelfLinks: [
@@ -361,6 +359,7 @@ describe('RequestService', () => {
});
it('should return false if not all top level links in the response are cached in the object cache', () => {
(objectCache.hasBySelfLink as any).and.returnValues(false, true, false);
spyOn(service, 'isPending').and.returnValue(false);
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = false;
@@ -368,11 +367,12 @@ describe('RequestService', () => {
expect(result).toEqual(expected);
});
});
describe('and it isn\'t a DSOSuccessResponse', () => {
beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValues(false);
(responseCache.has as any).and.returnValues(true);
(responseCache.get as any).and.returnValues(observableOf({
(objectCache.hasBySelfLink as any).and.returnValue(false);
(service as any).isReusable.and.returnValue(observableOf(true));
(serviceAsAny.getByHref as any).and.returnValue(observableOf({
response: {
isSuccessful: true
}

View File

@@ -1,5 +1,6 @@
import { merge as observableMerge, Observable, of as observableOf } from 'rxjs';
import {
distinctUntilChanged,
filter,
find,
first,
@@ -109,10 +110,10 @@ export class RequestService {
map((resourceSelfLinks: string[]) => resourceSelfLinks
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
));
const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true));
observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c);
const isPending = this.isPending(request);
return isCached || isPending;
}
@@ -144,7 +145,7 @@ export class RequestService {
}
/**
* Check whether a ResponseCacheEntry should still be cached
* Check whether a Response should still be cached
*
* @param entry
* the entry to check

View File

@@ -124,10 +124,10 @@ describe('RegistryService', () => {
};
const rdbStub = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
return observableCombineLatest(requestEntryObs,
responseCacheObs, payloadObs).pipe(map(([req, res, pay]) => {
return { req, res, pay };
payloadObs).pipe(map(([req, pay]) => {
return { req, pay };
})
);
},
@@ -160,10 +160,10 @@ describe('RegistryService', () => {
page: pageInfo
});
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo);
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
(registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */
registryService.getMetadataSchemas(pagination).subscribe((value) => {
});
@@ -181,10 +181,6 @@ describe('RegistryService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
});
it('should call get on the request service with the correct request url', () => {
expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams);
});
});
describe('when requesting metadataschema by name', () => {
@@ -193,10 +189,10 @@ describe('RegistryService', () => {
page: pageInfo
});
const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo);
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
(registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */
registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => {
});
@@ -214,10 +210,6 @@ describe('RegistryService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint);
});
it('should call get on the request service with the correct request url', () => {
expect((registryService as any).responseCache.get.calls.argsFor(0)[0]).toContain(endpoint);
});
});
describe('when requesting metadatafields', () => {
@@ -226,10 +218,10 @@ describe('RegistryService', () => {
page: pageInfo
});
const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, '200', pageInfo);
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
(registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */
registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => {
});
@@ -247,10 +239,6 @@ describe('RegistryService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
});
it('should call get on the request service with the correct request url', () => {
expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams);
});
});
describe('when requesting bitstreamformats', () => {
@@ -259,10 +247,10 @@ describe('RegistryService', () => {
page: pageInfo
});
const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, '200', pageInfo);
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
const responseEntry = Object.assign(new RequestEntry(), { response: response });
beforeEach(() => {
(registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
(registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */
registryService.getBitstreamFormats(pagination).subscribe((value) => {
});
@@ -280,9 +268,5 @@ describe('RegistryService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams);
});
it('should call get on the request service with the correct request url', () => {
expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams);
});
});
});

View File

@@ -1,33 +1,44 @@
import { cold, hot } from 'jasmine-marbles';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { GlobalConfig } from '../../../config/global-config.interface';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service';
import { EndpointMapRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer';
import { of as observableOf } from 'rxjs';
describe('HALEndpointService', () => {
let service: HALEndpointService;
let requestService: RequestService;
let envConfig: GlobalConfig;
let requestEntry;
const endpointMap = {
test: 'https://rest.api/test',
};
const linkPath = 'test';
beforeEach(() => {
requestEntry = {
request: { responseMsToLive: 1000 } as any,
requestPending: false,
responsePending: false,
completed: true,
response: { endpointMap: endpointMap } as any
} as RequestEntry;
requestService = getMockRequestService(observableOf(requestEntry));
envConfig = {
rest: { baseUrl: 'https://rest.api/' }
} as any;
service = new HALEndpointService(
requestService,
envConfig
);
});
describe('getRootEndpointMap', () => {
beforeEach(() => {
requestService = getMockRequestService();
envConfig = {
rest: { baseUrl: 'https://rest.api/' }
} as any;
service = new HALEndpointService(
requestService,
envConfig
);
});
it('should configure a new EndpointMapRequest', () => {
(service as any).getRootEndpointMap();
@@ -37,8 +48,8 @@ describe('HALEndpointService', () => {
it('should return an Observable of the endpoint map', () => {
const result = (service as any).getRootEndpointMap();
const expected = cold('b-', { b: endpointMap });
expect(result).toBeObservable(expected);
const expected = '(b|)';
getTestScheduler().expectObservable(result).toBe(expected, { b: endpointMap });
});
});

View File

@@ -1,9 +1,8 @@
import { Observable, of as observableOf } from 'rxjs';
import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
import {
distinctUntilChanged,
filter,
flatMap,
map,
mergeMap,
startWith,
switchMap,
tap
@@ -11,12 +10,13 @@ import {
import { RequestService } from '../data/request.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { EndpointMapRequest } from '../data/request.models';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response.models';
import { getResponseFromEntry } from './operators';
import { URLCombiner } from '../url-combiner/url-combiner';
@Injectable()
export class HALEndpointService {
@@ -36,48 +36,41 @@ export class HALEndpointService {
private getEndpointMapAt(href): Observable<EndpointMap> {
const request = new EndpointMapRequest(this.requestService.generateRequestId(), href);
this.requestService.getByUUID(request.uuid).pipe(
getResponseFromEntry(),
map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged()).subscribe((t) => console.log('uuid', t));
this.requestService.getByHref(request.href).pipe(
getResponseFromEntry(),
map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged()).subscribe((t) => console.log('href', t));
this.requestService.configure(request);
return this.requestService.getByHref(request.href).pipe( /*<-- changing this to UUID breaks it */
return this.requestService.getByHref(request.href).pipe(
getResponseFromEntry(),
map((response: EndpointMapSuccessResponse) => response.endpointMap),
distinctUntilChanged());
}
public getEndpoint(linkPath: string): Observable<string> {
return this.getEndpointAt(...linkPath.split('/'));
return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/'));
}
private getEndpointAt(...path: string[]): Observable<string> {
if (isEmpty(path)) {
path = ['/'];
private getEndpointAt(href: string, ...halNames: string[]): Observable<string> {
if (isEmpty(halNames)) {
throw new Error('cant\'t fetch the URL without the HAL link names')
}
const nextHref$ = this.getEndpointMapAt(href).pipe(
map((endpointMap: EndpointMap): string => {
/*TODO remove if/else block once the rest response contains _links for facets*/
const nextName = halNames[0];
if (hasValue(endpointMap) && hasValue(endpointMap[nextName])) {
return endpointMap[nextName];
} else {
return new URLCombiner(href, nextName).toString();
}
})
) as Observable<string>;
if (halNames.length === 1) {
return nextHref$;
} else {
return nextHref$.pipe(
switchMap((nextHref) => this.getEndpointAt(nextHref, ...halNames.slice(1)))
);
}
let currentPath;
const pipeArguments = path
.map((subPath: string, index: number) => [
switchMap((href: string) => this.getEndpointMapAt(href)),
map((endpointMap: EndpointMap) => {
if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) {
currentPath = endpointMap[subPath];
return endpointMap[subPath];
} else {
/*TODO remove if/else block once the rest response contains _links for facets*/
currentPath += '/' + subPath;
return currentPath;
}
}),
])
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
return observableOf(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged());
}
public isEnabledOnRestApi(linkPath: string): Observable<boolean> {

View File

@@ -25,6 +25,14 @@ describe('Core Module - RxJS Operators', () => {
e: { response: { isSuccessful: 1, resourceSelfLinks: [] } }
};
const testResponses = {
a: testRCEs.a.response,
b: testRCEs.b.response,
c: testRCEs.c.response,
d: testRCEs.d.response,
e: testRCEs.e.response
};
beforeEach(() => {
scheduler = getTestScheduler();
});
@@ -66,7 +74,7 @@ describe('Core Module - RxJS Operators', () => {
it('should only return responses for which isSuccessful === true', () => {
const source = hot('abcde', testRCEs);
const result = source.pipe(filterSuccessfulResponses());
const expected = cold('a--d-', testRCEs);
const expected = cold('a--d-', testResponses);
expect(result).toBeObservable(expected)
});

View File

@@ -58,6 +58,7 @@ export const getSucceededRemoteData = () =>
export const toDSpaceObjectListRD = () =>
<T extends DSpaceObject>(source: Observable<RemoteData<PaginatedList<SearchResult<T>>>>): Observable<RemoteData<PaginatedList<T>>> =>
source.pipe(
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;