diff --git a/package.json b/package.json index f2bb074ff4..60bb3c8d09 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "reflect-metadata": "0.1.10", "rxjs": "5.4.3", "ts-md5": "1.2.2", + "uuid": "^3.1.0", "webfontloader": "1.6.28", "zone.js": "0.8.18" }, @@ -126,6 +127,7 @@ "@types/node": "8.0.34", "@types/serve-static": "1.7.32", "@types/source-map": "0.5.1", + "@types/uuid": "^3.4.3", "@types/webfontloader": "1.6.29", "ajv": "5.2.3", "ajv-keywords": "2.1.0", diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 953de02ab4..08fae7d19a 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -29,7 +29,7 @@ | translate}} - diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 65b32b3c0b..30429c5a8c 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -1,3 +1,4 @@ +import { initMockRequestService } from '../../shared/mocks/mock-request.service'; import { BrowseService } from './browse.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; @@ -85,10 +86,6 @@ describe('BrowseService', () => { }); } - function initMockRequestService() { - return jasmine.createSpyObj('requestService', ['configure']); - } - function initTestService() { return new BrowseService( responseCache, @@ -157,7 +154,7 @@ describe('BrowseService', () => { it('should configure a new BrowseEndpointRequest', () => { const metadatumKey = 'dc.date.issued'; const linkName = 'items'; - const expected = new BrowseEndpointRequest(browsesEndpointURL); + const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL); scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); scheduler.flush(); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 6d8d504b82..a321e14706 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -40,7 +40,7 @@ export class BrowseService extends HALEndpointService { return this.getEndpoint() .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() - .map((endpointURL: string) => new BrowseEndpointRequest(endpointURL)) + .map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL)) .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => { const [successResponse, errorResponse] = this.responseCache.get(request.href) diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index eee6808d86..6fb24328c2 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -39,10 +39,10 @@ export class RemoteDataBuildService { this.objectCache.getRequestHrefBySelfLink(href)); const requestObs = Observable.race( - hrefObs.flatMap((href: string) => this.requestService.get(href)) + hrefObs.flatMap((href: string) => this.requestService.getByHref(href)) .filter((entry) => hasValue(entry)), requestHrefObs.flatMap((requestHref) => - this.requestService.get(requestHref)).filter((entry) => hasValue(entry)) + this.requestService.getByHref(requestHref)).filter((entry) => hasValue(entry)) ); const responseCacheObs = Observable.race( @@ -115,7 +115,7 @@ export class RemoteDataBuildService { hrefObs = Observable.of(hrefObs); } - const requestObs = hrefObs.flatMap((href: string) => this.requestService.get(href)) + const requestObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href)) .filter((entry) => hasValue(entry)); const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href)) .filter((entry) => hasValue(entry)); @@ -169,7 +169,7 @@ export class RemoteDataBuildService { const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType); if (Array.isArray(normalized[relationship])) { normalized[relationship].forEach((href: string) => { - this.requestService.configure(new RestRequest(href)) + this.requestService.configure(new RestRequest(this.requestService.generateRequestId(), href)) }); const rdArr = []; @@ -183,7 +183,7 @@ export class RemoteDataBuildService { links[relationship] = rdArr[0]; } } else { - this.requestService.configure(new RestRequest(normalized[relationship])); + this.requestService.configure(new RestRequest(this.requestService.generateRequestId(), normalized[relationship])); // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 31eb2d0b6a..ae41c38fbe 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -2,20 +2,21 @@ import { Injectable } from '@angular/core'; import { MemoizedSelector, Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; +import { IndexName } from '../index/index.reducer'; import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { hasNoValue } from '../../shared/empty.util'; import { GenericConstructor } from '../shared/generic-constructor'; -import { CoreState } from '../core.reducers'; -import { keySelector } from '../shared/selectors'; +import { coreSelector, CoreState } from '../core.reducers'; +import { pathSelector } from '../shared/selectors'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { - return keySelector('index/uuid', uuid); + return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); } function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector { - return keySelector('data/object', selfLink); + return pathSelector(coreSelector, 'data/object', selfLink); } /** @@ -60,7 +61,7 @@ export class ObjectCacheService { * the cached plain javascript object in to an instance of * a class. * - * e.g. get('c96588c6-72d3-425d-9d47-fa896255a695', Item) + * e.g. getByUUID('c96588c6-72d3-425d-9d47-fa896255a695', Item) * * @param uuid * The UUID of the object to get diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index 65ce8b2bac..77a2402043 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -7,11 +7,11 @@ import { ResponseCacheEntry } from './response-cache.reducer'; import { hasNoValue } from '../../shared/empty.util'; import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions'; import { RestResponse } from './response-cache.models'; -import { CoreState } from '../core.reducers'; -import { keySelector } from '../shared/selectors'; +import { coreSelector, CoreState } from '../core.reducers'; +import { pathSelector } from '../shared/selectors'; function entryFromKeySelector(key: string): MemoizedSelector { - return keySelector('data/response', key); + return pathSelector(coreSelector, 'data/response', key); } /** diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index c0d02be82a..111e1d3eaa 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,6 +1,7 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/Rx'; import { GlobalConfig } from '../../../config'; +import { initMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; @@ -38,10 +39,6 @@ describe('ConfigService', () => { const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; - function initMockRequestService(): RequestService { - return jasmine.createSpyObj('requestService', ['configure']); - } - function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { return jasmine.createSpyObj('responseCache', { get: cold('c-', { @@ -70,7 +67,7 @@ describe('ConfigService', () => { describe('getConfigByHref', () => { it('should configure a new ConfigRequest', () => { - const expected = new ConfigRequest(scopedEndpoint); + const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint); scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe()); scheduler.flush(); @@ -81,7 +78,7 @@ describe('ConfigService', () => { describe('getConfigByName', () => { it('should configure a new ConfigRequest', () => { - const expected = new ConfigRequest(scopedEndpoint); + const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint); scheduler.schedule(() => service.getConfigByName(scopeName).subscribe()); scheduler.flush(); @@ -93,7 +90,7 @@ describe('ConfigService', () => { it('should configure a new ConfigRequest', () => { findOptions.scopeID = scopeID; - const expected = new ConfigRequest(searchEndpoint); + const expected = new ConfigRequest(requestService.generateRequestId(), searchEndpoint); scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe()); scheduler.flush(); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 55c4055ed7..9ad4684300 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -75,14 +75,14 @@ export abstract class ConfigService extends HALEndpointService { return this.getEndpoint() .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); } public getConfigByHref(href: string): Observable { - const request = new ConfigRequest(href); + const request = new ConfigRequest(this.requestService.generateRequestId(), href); this.requestService.configure(request); return this.getConfig(request); @@ -93,7 +93,7 @@ export abstract class ConfigService extends HALEndpointService { .map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); @@ -104,7 +104,7 @@ export abstract class ConfigService extends HALEndpointService { .map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index f144f773a1..7cda10b4ae 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,7 +1,7 @@ import { ObjectCacheEffects } from './cache/object-cache.effects'; import { ResponseCacheEffects } from './cache/response-cache.effects'; -import { UUIDIndexEffects } from './index/uuid-index.effects'; +import { UUIDIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; export const coreEffects = [ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f4c7e2bbcc..768f05f24b 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -37,6 +37,7 @@ import { RouteService } from '../shared/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; +import { UUIDService } from './shared/uuid.service'; const IMPORTS = [ CommonModule, @@ -75,6 +76,7 @@ const PROVIDERS = [ SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionSectionsConfigService, + UUIDService, { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 493c9e96d9..d2898eb3c3 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -2,21 +2,21 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; -import { uuidIndexReducer, UUIDIndexState } from './index/uuid-index.reducer'; +import { indexReducer, IndexState } from './index/index.reducer'; import { requestReducer, RequestState } from './data/request.reducer'; export interface CoreState { 'data/object': ObjectCacheState, 'data/response': ResponseCacheState, 'data/request': RequestState, - 'index/uuid': UUIDIndexState + 'index': IndexState } export const coreReducers: ActionReducerMap = { 'data/object': objectCacheReducer, 'data/response': responseCacheReducer, 'data/request': requestReducer, - 'index/uuid': uuidIndexReducer + 'index': indexReducer }; export const coreSelector = createFeatureSelector('core'); diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index 5f27519a93..6579621610 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -11,7 +11,7 @@ describe('BrowseResponseParsingService', () => { }); describe('parse', () => { - const validRequest = new BrowseEndpointRequest('https://rest.api/discover/browses'); + const validRequest = new BrowseEndpointRequest('clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses'); const validResponse = { payload: { diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 0d78a9fa8d..d34ffc2277 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -2,6 +2,7 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/Rx'; import { GlobalConfig } from '../../../config'; +import { initMockRequestService } from '../../shared/mocks/mock-request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { CacheableObject } from '../cache/object-cache.reducer'; @@ -62,10 +63,6 @@ describe('ComColDataService', () => { }); } - function initMockRequestService(): RequestService { - return jasmine.createSpyObj('requestService', ['configure']); - } - function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { return jasmine.createSpyObj('responseCache', { get: cold('c-', { @@ -110,7 +107,7 @@ describe('ComColDataService', () => { responseCache = initMockResponseCacheService(true); service = initTestService(); - const expected = new FindByIDRequest(communityEndpoint, scopeID); + const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.flush(); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 17d2fb313c..68981121c1 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -33,7 +33,7 @@ export abstract class ComColDataService isNotEmpty(href)) .take(1) .do((href: string) => { - const request = new FindByIDRequest(href, scopeID); + const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID); this.requestService.configure(request); }); diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index 46e6d61f8f..dc5c42cbd5 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -21,7 +21,7 @@ describe('ConfigResponseParsingService', () => { }); describe('parse', () => { - const validRequest = new ConfigRequest('https://rest.api/config/submissiondefinitions/traditional'); + const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional'); const validResponse = { payload: { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 68fa2657f5..3e86bb21b0 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -79,7 +79,7 @@ export abstract class DataService .filter((href: string) => hasValue(href)) .take(1) .subscribe((href: string) => { - const request = new FindAllRequest(href, options); + const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); this.requestService.configure(request); }); @@ -98,7 +98,7 @@ export abstract class DataService .filter((href: string) => hasValue(href)) .take(1) .subscribe((href: string) => { - const request = new FindByIDRequest(href, id); + const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); this.requestService.configure(request); }); @@ -106,7 +106,7 @@ export abstract class DataService } findByHref(href: string): Observable> { - this.requestService.configure(new RestRequest(href)); + this.requestService.configure(new RestRequest(this.requestService.generateRequestId(), href)); return this.rdbService.buildSingle(href, this.normalizedResourceType); } @@ -120,7 +120,7 @@ export abstract class DataService .filter((href: string) => hasValue(href)) .take(1) .subscribe((href: string) => { - const request = new RestRequest(href, RestRequestMethod.Post, dso); + const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso); this.requestService.configure(request); }); diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts index 31f0dc5996..436c365caa 100644 --- a/src/app/core/data/request.actions.ts +++ b/src/app/core/data/request.actions.ts @@ -27,8 +27,14 @@ export class RequestExecuteAction implements Action { type = RequestActionTypes.EXECUTE; payload: string; - constructor(key: string) { - this.payload = key + /** + * Create a new RequestExecuteAction + * + * @param uuid + * the request's uuid + */ + constructor(uuid: string) { + this.payload = uuid } } @@ -42,11 +48,11 @@ export class RequestCompleteAction implements Action { /** * Create a new RequestCompleteAction * - * @param key - * the key under which this request is stored, + * @param uuid + * the request's uuid */ - constructor(key: string) { - this.payload = key; + constructor(uuid: string) { + this.payload = uuid; } } /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index f848d7f0d7..815878b7bf 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -25,7 +25,7 @@ export class RequestEffects { @Effect() execute = this.actions$ .ofType(RequestActionTypes.EXECUTE) .flatMap((action: RequestExecuteAction) => { - return this.requestService.get(action.payload) + return this.requestService.getByUUID(action.payload) .take(1); }) .map((entry: RequestEntry) => entry.request) @@ -42,10 +42,10 @@ export class RequestEffects { .map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)) .do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive)) - .map((response: RestResponse) => new RequestCompleteAction(request.href)) + .map((response: RestResponse) => new RequestCompleteAction(request.uuid)) .catch((error: RequestError) => Observable.of(new ErrorResponse(error)) .do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive)) - .map((response: RestResponse) => new RequestCompleteAction(request.href))); + .map((response: RestResponse) => new RequestCompleteAction(request.uuid))); }); constructor( diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 3a7141d22d..46f40b0abf 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -31,10 +31,12 @@ export enum RestRequestMethod { export class RestRequest { constructor( + public uuid: string, public href: string, public method: RestRequestMethod = RestRequestMethod.Get, public body?: any - ) { } + ) { + } getResponseParser(): GenericConstructor { return DSOResponseParsingService; @@ -43,10 +45,11 @@ export class RestRequest { export class FindByIDRequest extends RestRequest { constructor( + uuid: string, href: string, public resourceID: string ) { - super(href); + super(uuid, href); } } @@ -59,17 +62,18 @@ export class FindAllOptions { export class FindAllRequest extends RestRequest { constructor( + uuid: string, href: string, public options?: FindAllOptions, ) { - super(href); + super(uuid, href); } } export class RootEndpointRequest extends RestRequest { - constructor(EnvConfig: GlobalConfig) { + constructor(uuid: string, EnvConfig: GlobalConfig) { const href = new RESTURLCombiner(EnvConfig, '/').toString(); - super(href); + super(uuid, href); } getResponseParser(): GenericConstructor { @@ -78,8 +82,8 @@ export class RootEndpointRequest extends RestRequest { } export class BrowseEndpointRequest extends RestRequest { - constructor(href: string) { - super(href); + constructor(uuid: string, href: string) { + super(uuid, href); } getResponseParser(): GenericConstructor { @@ -88,8 +92,8 @@ export class BrowseEndpointRequest extends RestRequest { } export class ConfigRequest extends RestRequest { - constructor(href: string) { - super(href); + constructor(uuid: string, href: string) { + super(uuid, href); } getResponseParser(): GenericConstructor { diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts index a6d84ffe80..138d3ea5dd 100644 --- a/src/app/core/data/request.reducer.spec.ts +++ b/src/app/core/data/request.reducer.spec.ts @@ -16,11 +16,13 @@ class NullAction extends RequestCompleteAction { } describe('requestReducer', () => { + const id1 = 'clients/eca2ea1d-6a6a-4f62-8907-176d5fec5014'; + const id2 = 'clients/eb7cde2e-a03f-4f0b-ac5d-888a4ef2b4eb'; const link1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; const link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; const testState: RequestState = { - [link1]: { - request: new RestRequest(link1), + [id1]: { + request: new RestRequest(id1, link1), requestPending: false, responsePending: false, completed: false @@ -44,37 +46,40 @@ describe('requestReducer', () => { it('should add the new RestRequest and set \'requestPending\' to true, \'responsePending\' to false and \'completed\' to false for the given RestRequest in the state, in response to a CONFIGURE action', () => { const state = testState; - const request = new RestRequest(link2); + const request = new RestRequest(id2, link2); const action = new RequestConfigureAction(request); const newState = requestReducer(state, action); - expect(newState[link2].request.href).toEqual(link2); - expect(newState[link2].requestPending).toEqual(true); - expect(newState[link2].responsePending).toEqual(false); - expect(newState[link2].completed).toEqual(false); + expect(newState[id2].request.uuid).toEqual(id2); + expect(newState[id2].request.href).toEqual(link2); + expect(newState[id2].requestPending).toEqual(true); + expect(newState[id2].responsePending).toEqual(false); + expect(newState[id2].completed).toEqual(false); }); 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', () => { const state = testState; - const action = new RequestExecuteAction(link1); + const action = new RequestExecuteAction(id1); const newState = requestReducer(state, action); - expect(newState[link1].request.href).toEqual(link1); - expect(newState[link1].requestPending).toEqual(false); - expect(newState[link1].responsePending).toEqual(true); - expect(newState[link1].completed).toEqual(state[link1].completed); + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].requestPending).toEqual(false); + expect(newState[id1].responsePending).toEqual(true); + expect(newState[id1].completed).toEqual(state[id1].completed); }); 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(link1); + const action = new RequestCompleteAction(id1); const newState = requestReducer(state, action); - expect(newState[link1].request.href).toEqual(link1); - expect(newState[link1].requestPending).toEqual(state[link1].requestPending); - expect(newState[link1].responsePending).toEqual(false); - expect(newState[link1].completed).toEqual(true); + expect(newState[id1].request.uuid).toEqual(id1); + expect(newState[id1].request.href).toEqual(link1); + expect(newState[id1].requestPending).toEqual(state[id1].requestPending); + expect(newState[id1].responsePending).toEqual(false); + expect(newState[id1].completed).toEqual(true); }); }); diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index 628725f745..3ac35d2741 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -12,7 +12,7 @@ export class RequestEntry { } export interface RequestState { - [key: string]: RequestEntry + [uuid: string]: RequestEntry } // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) @@ -41,7 +41,7 @@ export function requestReducer(state = initialState, action: RequestAction): Req function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState { return Object.assign({}, state, { - [action.payload.href]: { + [action.payload.uuid]: { request: action.payload, requestPending: true, responsePending: false, diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index a075244648..6cfbedb5cb 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -10,14 +10,20 @@ import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; import { coreSelector, CoreState } from '../core.reducers'; -import { keySelector } from '../shared/selectors'; +import { IndexName } from '../index/index.reducer'; +import { pathSelector } from '../shared/selectors'; +import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { RestRequest, RestRequestMethod } from './request.models'; import { RequestEntry, RequestState } from './request.reducer'; -function entryFromHrefSelector(href: string): MemoizedSelector { - return keySelector('data/request', href); +function entryFromUUIDSelector(uuid: string): MemoizedSelector { + return pathSelector(coreSelector, 'data/request', uuid); +} + +function uuidFromHrefSelector(href: string): MemoizedSelector { + return pathSelector(coreSelector, 'index', IndexName.REQUEST, href); } export function requestStateSelector(): MemoizedSelector { @@ -32,18 +38,23 @@ export class RequestService { constructor(private objectCache: ObjectCacheService, private responseCache: ResponseCacheService, + private uuidService: UUIDService, private store: Store) { } - isPending(href: string): boolean { + generateRequestId(): string { + return `client/${this.uuidService.generate()}`; + } + + isPending(uuid: string): boolean { // first check requests that haven't made it to the store yet - if (this.requestsOnTheirWayToTheStore.includes(href)) { + if (this.requestsOnTheirWayToTheStore.includes(uuid)) { return true; } // then check the store let isPending = false; - this.store.select(entryFromHrefSelector(href)) + this.store.select(entryFromUUIDSelector(uuid)) .take(1) .subscribe((re: RequestEntry) => { isPending = (hasValue(re) && !re.completed) @@ -52,8 +63,13 @@ export class RequestService { return isPending; } - get(href: string): Observable { - return this.store.select(entryFromHrefSelector(href)); + getByUUID(uuid: string): Observable { + return this.store.select(entryFromUUIDSelector(uuid)); + } + + getByHref(href: string): Observable { + return this.store.select(uuidFromHrefSelector(href)) + .flatMap((uuid: string) => this.getByUUID(uuid)); } configure(request: RestRequest): void { @@ -86,15 +102,15 @@ export class RequestService { ).subscribe((c) => isCached = c); } - const isPending = this.isPending(request.href); + const isPending = this.isPending(request.uuid); return isCached || isPending; } private dispatchRequest(request: RestRequest) { this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(request.href)); - this.trackRequestsOnTheirWayToTheStore(request.href); + this.store.dispatch(new RequestExecuteAction(request.uuid)); + this.trackRequestsOnTheirWayToTheStore(request.uuid); } /** @@ -104,13 +120,13 @@ export class RequestService { * This method will store the href of every request that gets configured in a local variable, and * remove it as soon as it can be found in the store. */ - private trackRequestsOnTheirWayToTheStore(href: string) { - this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, href]; - this.store.select(entryFromHrefSelector(href)) + private trackRequestsOnTheirWayToTheStore(uuid: string) { + this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, uuid]; + this.store.select(entryFromUUIDSelector(uuid)) .filter((re: RequestEntry) => hasValue(re)) .take(1) .subscribe((re: RequestEntry) => { - this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== href) + this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingUUID: string) => pendingUUID !== uuid) }); } } diff --git a/src/app/core/index/index.actions.ts b/src/app/core/index/index.actions.ts new file mode 100644 index 0000000000..014b6561a3 --- /dev/null +++ b/src/app/core/index/index.actions.ts @@ -0,0 +1,69 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; +import { IndexName } from './index.reducer'; + +/** + * The list of HrefIndexAction type definitions + */ +export const IndexActionTypes = { + ADD: type('dspace/core/index/ADD'), + REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE') +}; + +/* tslint:disable:max-classes-per-file */ +/** + * An ngrx action to add an value to the index + */ +export class AddToIndexAction implements Action { + type = IndexActionTypes.ADD; + payload: { + name: IndexName; + value: string; + key: string; + }; + + /** + * Create a new AddToIndexAction + * + * @param name + * the name of the index to add to + * @param key + * the key to add + * @param value + * the self link of the resource the key belongs to + */ + constructor(name: IndexName, key: string, value: string) { + this.payload = { name, key, value }; + } +} + +/** + * An ngrx action to remove an value from the index + */ +export class RemoveFromIndexByValueAction implements Action { + type = IndexActionTypes.REMOVE_BY_VALUE; + payload: { + name: IndexName, + value: string + }; + + /** + * Create a new RemoveFromIndexByValueAction + * + * @param name + * the name of the index to remove from + * @param value + * the value to remove the UUID for + */ + constructor(name: IndexName, value: string) { + this.payload = { name, value }; + } + +} +/* tslint:enable:max-classes-per-file */ + +/** + * A type to encompass all HrefIndexActions + */ +export type IndexAction = AddToIndexAction | RemoveFromIndexByValueAction; diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts new file mode 100644 index 0000000000..05ae529c8e --- /dev/null +++ b/src/app/core/index/index.effects.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; + +import { + ObjectCacheActionTypes, AddToObjectCacheAction, + RemoveFromObjectCacheAction +} from '../cache/object-cache.actions'; +import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions'; +import { RestRequestMethod } from '../data/request.models'; +import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; +import { hasValue } from '../../shared/empty.util'; +import { IndexName } from './index.reducer'; + +@Injectable() +export class UUIDIndexEffects { + + @Effect() addObject$ = this.actions$ + .ofType(ObjectCacheActionTypes.ADD) + .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)) + .map((action: AddToObjectCacheAction) => { + return new AddToIndexAction( + IndexName.OBJECT, + action.payload.objectToCache.uuid, + action.payload.objectToCache.self + ); + }); + + @Effect() removeObject$ = this.actions$ + .ofType(ObjectCacheActionTypes.REMOVE) + .map((action: RemoveFromObjectCacheAction) => { + return new RemoveFromIndexByValueAction( + IndexName.OBJECT, + action.payload + ); + }); + + @Effect() addRequest$ = this.actions$ + .ofType(RequestActionTypes.CONFIGURE) + .filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.Get) + .map((action: RequestConfigureAction) => { + return new AddToIndexAction( + IndexName.REQUEST, + action.payload.href, + action.payload.uuid + ); + }); + + // @Effect() removeRequest$ = this.actions$ + // .ofType(ObjectCacheActionTypes.REMOVE) + // .map((action: RemoveFromObjectCacheAction) => { + // return new RemoveFromIndexByValueAction( + // IndexName.OBJECT, + // action.payload + // ); + // }); + + constructor(private actions$: Actions) { + + } + +} diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts new file mode 100644 index 0000000000..a1cf92aeb3 --- /dev/null +++ b/src/app/core/index/index.reducer.spec.ts @@ -0,0 +1,58 @@ +import * as deepFreeze from 'deep-freeze'; + +import { IndexName, indexReducer, IndexState } from './index.reducer'; +import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; + +class NullAction extends AddToIndexAction { + type = null; + payload = null; + + constructor() { + super(null, null, null); + } +} + +describe('requestReducer', () => { + const key1 = '567a639f-f5ff-4126-807c-b7d0910808c8'; + const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; + const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const testState: IndexState = { + [IndexName.OBJECT]: { + [key1]: val1 + } + }; + deepFreeze(testState); + + it('should return the current state when no valid actions have been made', () => { + const action = new NullAction(); + const newState = indexReducer(testState, action); + + expect(newState).toEqual(testState); + }); + + it('should start with an empty state', () => { + const action = new NullAction(); + const initialState = indexReducer(undefined, action); + + expect(initialState).toEqual(Object.create(null)); + }); + + it('should add the \'key\' with the corresponding \'value\' to the correct substate, in response to an ADD action', () => { + const state = testState; + + const action = new AddToIndexAction(IndexName.REQUEST, key2, val2); + const newState = indexReducer(state, action); + + expect(newState[IndexName.REQUEST][key2]).toEqual(val2); + }); + + it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_VALUE action', () => { + const state = testState; + + const action = new RemoveFromIndexByValueAction(IndexName.OBJECT, val1); + const newState = indexReducer(state, action); + + expect(newState[IndexName.OBJECT][key1]).toBeUndefined(); + }); +}); diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts new file mode 100644 index 0000000000..869dee9e51 --- /dev/null +++ b/src/app/core/index/index.reducer.ts @@ -0,0 +1,62 @@ +import { + IndexAction, + IndexActionTypes, + AddToIndexAction, + RemoveFromIndexByValueAction +} from './index.actions'; + +export enum IndexName { + OBJECT = 'object/uuid-to-self-link', + REQUEST = 'get-request/href-to-uuid' +} + +export interface IndexState { + // TODO this should be `[name in IndexName]: {` but that's currently broken, + // see https://github.com/Microsoft/TypeScript/issues/13042 + [name: string]: { + [key: string]: string + } +} + +// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) +const initialState: IndexState = Object.create(null); + +export function indexReducer(state = initialState, action: IndexAction): IndexState { + switch (action.type) { + + case IndexActionTypes.ADD: { + return addToIndex(state, action as AddToIndexAction); + } + + case IndexActionTypes.REMOVE_BY_VALUE: { + return removeFromIndexByValue(state, action as RemoveFromIndexByValueAction) + } + + default: { + return state; + } + } +} + +function addToIndex(state: IndexState, action: AddToIndexAction): IndexState { + const subState = state[action.payload.name]; + const newSubState = Object.assign({}, subState, { + [action.payload.key]: action.payload.value + }); + return Object.assign({}, state, { + [action.payload.name]: newSubState + }) +} + +function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState { + const subState = state[action.payload.name]; + const newSubState = Object.create(null); + for (const value in subState) { + if (subState[value] !== action.payload.value) { + newSubState[value] = subState[value]; + } + } + return Object.assign({}, state, { + [action.payload.name]: newSubState + }); +} diff --git a/src/app/core/index/uuid-index.actions.ts b/src/app/core/index/uuid-index.actions.ts deleted file mode 100644 index 0bea11204c..0000000000 --- a/src/app/core/index/uuid-index.actions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Action } from '@ngrx/store'; - -import { type } from '../../shared/ngrx/type'; - -/** - * The list of HrefIndexAction type definitions - */ -export const UUIDIndexActionTypes = { - ADD: type('dspace/core/index/uuid/ADD'), - REMOVE_HREF: type('dspace/core/index/uuid/REMOVE_HREF') -}; - -/* tslint:disable:max-classes-per-file */ -/** - * An ngrx action to add an href to the index - */ -export class AddToUUIDIndexAction implements Action { - type = UUIDIndexActionTypes.ADD; - payload: { - href: string; - uuid: string; - }; - - /** - * Create a new AddToUUIDIndexAction - * - * @param uuid - * the uuid to add - * @param href - * the self link of the resource the uuid belongs to - */ - constructor(uuid: string, href: string) { - this.payload = { href, uuid }; - } -} - -/** - * An ngrx action to remove an href from the index - */ -export class RemoveHrefFromUUIDIndexAction implements Action { - type = UUIDIndexActionTypes.REMOVE_HREF; - payload: string; - - /** - * Create a new RemoveHrefFromUUIDIndexAction - * - * @param href - * the href to remove the UUID for - */ - constructor(href: string) { - this.payload = href; - } - -} -/* tslint:enable:max-classes-per-file */ - -/** - * A type to encompass all HrefIndexActions - */ -export type UUIDIndexAction = AddToUUIDIndexAction | RemoveHrefFromUUIDIndexAction; diff --git a/src/app/core/index/uuid-index.effects.ts b/src/app/core/index/uuid-index.effects.ts deleted file mode 100644 index 2f5900ed04..0000000000 --- a/src/app/core/index/uuid-index.effects.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Effect, Actions } from '@ngrx/effects'; - -import { - ObjectCacheActionTypes, AddToObjectCacheAction, - RemoveFromObjectCacheAction -} from '../cache/object-cache.actions'; -import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions'; -import { hasValue } from '../../shared/empty.util'; - -@Injectable() -export class UUIDIndexEffects { - - @Effect() add$ = this.actions$ - .ofType(ObjectCacheActionTypes.ADD) - .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)) - .map((action: AddToObjectCacheAction) => { - return new AddToUUIDIndexAction( - action.payload.objectToCache.uuid, - action.payload.objectToCache.self - ); - }); - - @Effect() remove$ = this.actions$ - .ofType(ObjectCacheActionTypes.REMOVE) - .map((action: RemoveFromObjectCacheAction) => { - return new RemoveHrefFromUUIDIndexAction(action.payload); - }); - - constructor(private actions$: Actions) { - - } - -} diff --git a/src/app/core/index/uuid-index.reducer.spec.ts b/src/app/core/index/uuid-index.reducer.spec.ts deleted file mode 100644 index e477d8df2e..0000000000 --- a/src/app/core/index/uuid-index.reducer.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as deepFreeze from 'deep-freeze'; - -import { uuidIndexReducer, UUIDIndexState } from './uuid-index.reducer'; -import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions'; - -class NullAction extends AddToUUIDIndexAction { - type = null; - payload = null; - - constructor() { - super(null, null); - } -} - -describe('requestReducer', () => { - const link1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; - const link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; - const uuid1 = '567a639f-f5ff-4126-807c-b7d0910808c8'; - const uuid2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb'; - const testState: UUIDIndexState = { - [uuid1]: link1 - }; - deepFreeze(testState); - - it('should return the current state when no valid actions have been made', () => { - const action = new NullAction(); - const newState = uuidIndexReducer(testState, action); - - expect(newState).toEqual(testState); - }); - - it('should start with an empty state', () => { - const action = new NullAction(); - const initialState = uuidIndexReducer(undefined, action); - - expect(initialState).toEqual(Object.create(null)); - }); - - it('should add the \'uuid\' with the corresponding \'href\' to the state, in response to an ADD action', () => { - const state = testState; - - const action = new AddToUUIDIndexAction(uuid2, link2); - const newState = uuidIndexReducer(state, action); - - expect(newState[uuid2]).toEqual(link2); - }); - - it('should remove the given \'href\' from its corresponding \'uuid\' in the state, in response to a REMOVE_HREF action', () => { - const state = testState; - - const action = new RemoveHrefFromUUIDIndexAction(link1); - const newState = uuidIndexReducer(state, action); - - expect(newState[uuid1]).toBeUndefined(); - }); -}); diff --git a/src/app/core/index/uuid-index.reducer.ts b/src/app/core/index/uuid-index.reducer.ts deleted file mode 100644 index 191dd8f463..0000000000 --- a/src/app/core/index/uuid-index.reducer.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - UUIDIndexAction, - UUIDIndexActionTypes, - AddToUUIDIndexAction, - RemoveHrefFromUUIDIndexAction -} from './uuid-index.actions'; - -export interface UUIDIndexState { - [uuid: string]: string -} - -// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) -const initialState: UUIDIndexState = Object.create(null); - -export function uuidIndexReducer(state = initialState, action: UUIDIndexAction): UUIDIndexState { - switch (action.type) { - - case UUIDIndexActionTypes.ADD: { - return addToUUIDIndex(state, action as AddToUUIDIndexAction); - } - - case UUIDIndexActionTypes.REMOVE_HREF: { - return removeHrefFromUUIDIndex(state, action as RemoveHrefFromUUIDIndexAction) - } - - default: { - return state; - } - } -} - -function addToUUIDIndex(state: UUIDIndexState, action: AddToUUIDIndexAction): UUIDIndexState { - return Object.assign({}, state, { - [action.payload.uuid]: action.payload.href - }); -} - -function removeHrefFromUUIDIndex(state: UUIDIndexState, action: RemoveHrefFromUUIDIndexAction): UUIDIndexState { - const newState = Object.create(null); - for (const uuid in state) { - if (state[uuid] !== action.payload) { - newState[uuid] = state[uuid]; - } - } - return newState; -} diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index f7adc1eccf..22d9a15fd7 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -1,5 +1,6 @@ import { cold, hot } from 'jasmine-marbles'; import { GlobalConfig } from '../../../config/global-config.interface'; +import { initMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RootEndpointRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; @@ -38,7 +39,7 @@ describe('HALEndpointService', () => { }) }); - requestService = jasmine.createSpyObj('requestService', ['configure']); + requestService = initMockRequestService(); envConfig = { rest: { baseUrl: 'https://rest.api/' } @@ -53,7 +54,7 @@ describe('HALEndpointService', () => { it('should configure a new RootEndpointRequest', () => { (service as any).getEndpointMap(); - const expected = new RootEndpointRequest(envConfig); + const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig); expect(requestService.configure).toHaveBeenCalledWith(expected); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index fa11fed308..84587f1eea 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -14,7 +14,7 @@ export abstract class HALEndpointService { protected abstract EnvConfig: GlobalConfig; protected getEndpointMap(): Observable { - const request = new RootEndpointRequest(this.EnvConfig); + const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig); this.requestService.configure(request); return this.responseCache.get(request.href) .map((entry: ResponseCacheEntry) => entry.response) diff --git a/src/app/core/shared/selectors.ts b/src/app/core/shared/selectors.ts index 06f444b2e6..7bd35d39c1 100644 --- a/src/app/core/shared/selectors.ts +++ b/src/app/core/shared/selectors.ts @@ -1,13 +1,17 @@ import { createSelector, MemoizedSelector } from '@ngrx/store'; -import { coreSelector, CoreState } from '../core.reducers'; -import { hasValue } from '../../shared/empty.util'; +import { hasNoValue, isEmpty } from '../../shared/empty.util'; -export function keySelector(subState: string, key: string): MemoizedSelector { - return createSelector(coreSelector, (state: CoreState) => { - if (hasValue(state[subState])) { - return state[subState][key]; - } else { - return undefined; - } - }); +export function pathSelector(selector: MemoizedSelector, ...path: string[]): MemoizedSelector { + return createSelector(selector, (state: any) => getSubState(state, path)); +} + +function getSubState(state: any, path: string[]) { + const current = path[0]; + const remainingPath = path.slice(1); + const subState = state[current]; + if (hasNoValue(subState) || isEmpty(remainingPath)) { + return subState; + } else { + return getSubState(subState, remainingPath); + } } diff --git a/src/app/core/shared/uuid.service.ts b/src/app/core/shared/uuid.service.ts new file mode 100644 index 0000000000..6c02facbac --- /dev/null +++ b/src/app/core/shared/uuid.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; +import * as uuidv4 from 'uuid/v4'; + +@Injectable() +export class UUIDService { + generate(): string { + return uuidv4(); + } +} diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts new file mode 100644 index 0000000000..9ece96da3e --- /dev/null +++ b/src/app/shared/mocks/mock-request.service.ts @@ -0,0 +1,8 @@ +import { RequestService } from '../../core/data/request.service'; + +export function initMockRequestService(): RequestService { + return jasmine.createSpyObj('requestService', { + configure: () => false, + generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78' + }); +} diff --git a/yarn.lock b/yarn.lock index 91b2a787e2..6fc8d3ab0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -225,6 +225,12 @@ version "0.5.1" resolved "https://registry.yarnpkg.com/@types/source-map/-/source-map-0.5.1.tgz#7e74db5d06ab373a712356eebfaea2fad0ea2367" +"@types/key@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/key/-/key-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" + dependencies: + "@types/node" "*" + "@types/webfontloader@1.6.29": version "1.6.29" resolved "https://registry.yarnpkg.com/@types/webfontloader/-/webfontloader-1.6.29.tgz#c6b5f6eb8ca31d0aae6b02b6c1300349dd93ea8e" @@ -1009,7 +1015,7 @@ cache-base@^1.0.1: dependencies: collection-visit "^1.0.0" component-emitter "^1.2.1" - get-value "^2.0.6" + getByUUID-value "^2.0.6" has-value "^1.0.0" isobject "^3.0.1" set-value "^2.0.0" @@ -1751,7 +1757,7 @@ dateformat@^1.0.11, dateformat@^1.0.6: version "1.0.12" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" dependencies: - get-stdin "^4.0.1" + getByUUID-stdin "^4.0.1" meow "^3.3.0" debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8: @@ -2317,7 +2323,7 @@ execa@^0.7.0: resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" dependencies: cross-spawn "^5.0.1" - get-stream "^3.0.0" + getByUUID-stream "^3.0.0" is-stream "^1.1.0" npm-run-path "^2.0.0" p-finally "^1.0.0" @@ -2731,25 +2737,25 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" -get-caller-file@^1.0.1: +getByUUID-caller-file@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + resolved "https://registry.yarnpkg.com/getByUUID-caller-file/-/getByUUID-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" -get-stdin@^4.0.1: +getByUUID-stdin@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + resolved "https://registry.yarnpkg.com/getByUUID-stdin/-/getByUUID-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" -get-stdin@^5.0.1: +getByUUID-stdin@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + resolved "https://registry.yarnpkg.com/getByUUID-stdin/-/getByUUID-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" -get-stream@^3.0.0: +getByUUID-stream@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + resolved "https://registry.yarnpkg.com/getByUUID-stream/-/getByUUID-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" -get-value@^2.0.3, get-value@^2.0.6: +getByUUID-value@^2.0.3, getByUUID-value@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + resolved "https://registry.yarnpkg.com/getByUUID-value/-/getByUUID-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" getpass@^0.1.1: version "0.1.7" @@ -2842,7 +2848,7 @@ got@^6.7.1: dependencies: create-error-class "^3.0.0" duplexer3 "^0.1.4" - get-stream "^3.0.0" + getByUUID-stream "^3.0.0" is-redirect "^1.0.0" is-retry-allowed "^1.0.0" is-stream "^1.0.0" @@ -2974,7 +2980,7 @@ has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" dependencies: - get-value "^2.0.3" + getByUUID-value "^2.0.3" has-values "^0.1.4" isobject "^2.0.0" @@ -2982,7 +2988,7 @@ has-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" dependencies: - get-value "^2.0.6" + getByUUID-value "^2.0.6" has-values "^1.0.0" isobject "^3.0.0" @@ -4652,7 +4658,7 @@ node-sass@4.5.3: chalk "^1.1.1" cross-spawn "^3.0.0" gaze "^1.0.0" - get-stdin "^4.0.1" + getByUUID-stdin "^4.0.1" glob "^7.0.3" in-publish "^2.0.0" lodash.assign "^4.2.0" @@ -5223,7 +5229,7 @@ postcss-cli@4.1.1: chokidar "^1.6.1" dependency-graph "^0.5.0" fs-extra "^4.0.1" - get-stdin "^5.0.1" + getByUUID-stdin "^5.0.1" globby "^6.1.0" ora "^1.1.0" postcss "^6.0.1" @@ -5802,7 +5808,7 @@ protractor-istanbul-plugin@2.0.0: fs-extra "^0.22.1" merge "^1.2.0" q "^1.4.1" - uuid "^2.0.1" + key "^2.0.1" protractor@5.1.2: version "5.1.2" @@ -6224,7 +6230,7 @@ request@2, request@^2.78.0, request@^2.79.0: stringstream "~0.0.5" tough-cookie "~2.3.3" tunnel-agent "^0.6.0" - uuid "^3.1.0" + key "^3.1.0" request@2.79.0: version "2.79.0" @@ -6249,7 +6255,7 @@ request@2.79.0: stringstream "~0.0.4" tough-cookie "~2.3.0" tunnel-agent "~0.4.1" - uuid "^3.0.0" + key "^3.0.0" request@2.81.0, request@~2.81.0: version "2.81.0" @@ -6276,7 +6282,7 @@ request@2.81.0, request@~2.81.0: stringstream "~0.0.4" tough-cookie "~2.3.0" tunnel-agent "^0.6.0" - uuid "^3.0.0" + key "^3.0.0" require-directory@^2.1.1: version "2.1.1" @@ -6783,7 +6789,7 @@ sockjs@0.3.18: resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207" dependencies: faye-websocket "^0.10.0" - uuid "^2.0.2" + key "^2.0.2" sort-keys@^1.0.0: version "1.1.2" @@ -7056,7 +7062,7 @@ strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" dependencies: - get-stdin "^4.0.1" + getByUUID-stdin "^4.0.1" strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" @@ -7459,7 +7465,7 @@ union-value@^1.0.0: resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" dependencies: arr-union "^3.1.0" - get-value "^2.0.6" + getByUUID-value "^2.0.6" is-extendable "^0.1.1" set-value "^0.4.3" @@ -7604,13 +7610,13 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@^2.0.1, uuid@^2.0.2: +key@^2.0.1, key@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + resolved "https://registry.yarnpkg.com/key/-/key-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0, uuid@^3.1.0: +key@^3.0.0, key@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + resolved "https://registry.yarnpkg.com/key/-/key-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" v8flags@^3.0.0: version "3.0.1" @@ -7999,7 +8005,7 @@ yargs@^6.6.0: camelcase "^3.0.0" cliui "^3.2.0" decamelize "^1.1.1" - get-caller-file "^1.0.1" + getByUUID-caller-file "^1.0.1" os-locale "^1.4.0" read-pkg-up "^1.0.1" require-directory "^2.1.1" @@ -8017,7 +8023,7 @@ yargs@^7.0.0: camelcase "^3.0.0" cliui "^3.2.0" decamelize "^1.1.1" - get-caller-file "^1.0.1" + getByUUID-caller-file "^1.0.1" os-locale "^1.4.0" read-pkg-up "^1.0.1" require-directory "^2.1.1" @@ -8035,7 +8041,7 @@ yargs@^8.0.1, yargs@^8.0.2: camelcase "^4.1.0" cliui "^3.2.0" decamelize "^1.1.1" - get-caller-file "^1.0.1" + getByUUID-caller-file "^1.0.1" os-locale "^2.0.0" read-pkg-up "^2.0.0" require-directory "^2.1.1"