resolved merge conflicts

This commit is contained in:
William Welling
2017-09-29 12:59:33 -05:00
32 changed files with 611 additions and 457 deletions

View File

@@ -8,13 +8,13 @@ import { ResponseCacheService } from '../response-cache.service';
import { RequestEntry } from '../../data/request.reducer'; import { RequestEntry } from '../../data/request.reducer';
import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { ResponseCacheEntry } from '../response-cache.reducer'; import { ResponseCacheEntry } from '../response-cache.reducer';
import { ErrorResponse, SuccessResponse } from '../response-cache.models'; import { ErrorResponse, DSOSuccessResponse } from '../response-cache.models';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { GenericConstructor } from '../../shared/generic-constructor'; import { GenericConstructor } from '../../shared/generic-constructor';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { NormalizedObjectFactory } from '../models/normalized-object-factory'; import { NormalizedObjectFactory } from '../models/normalized-object-factory';
import { Request } from '../../data/request.models'; import { RestRequest } from '../../data/request.models';
import { PageInfo } from '../../shared/page-info.model'; import { PageInfo } from "../../shared/page-info.model";
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
@@ -22,22 +22,30 @@ export class RemoteDataBuildService {
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
protected requestService: RequestService protected requestService: RequestService
) { ) {
} }
buildSingle<TNormalized extends CacheableObject, TDomain>(href: string, buildSingle<TNormalized extends CacheableObject, TDomain>(
normalizedType: GenericConstructor<TNormalized>): RemoteData<TDomain> { hrefObs: string | Observable<string>,
const requestHrefObs = this.objectCache.getRequestHrefBySelfLink(href); normalizedType: GenericConstructor<TNormalized>
): RemoteData<TDomain> {
if (typeof hrefObs === 'string') {
hrefObs = Observable.of(hrefObs);
}
const requestHrefObs = hrefObs.flatMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href));
const requestObs = Observable.race( const requestObs = Observable.race(
this.requestService.get(href).filter((entry) => hasValue(entry)), hrefObs.flatMap((href: string) => this.requestService.get(href))
.filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) => requestHrefObs.flatMap((requestHref) =>
this.requestService.get(requestHref)).filter((entry) => hasValue(entry)) this.requestService.get(requestHref)).filter((entry) => hasValue(entry))
); );
const responseCacheObs = Observable.race( const responseCacheObs = Observable.race(
this.responseCache.get(href).filter((entry) => hasValue(entry)), hrefObs.flatMap((href: string) => this.responseCache.get(href))
.filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry)) requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
); );
@@ -60,7 +68,7 @@ export class RemoteDataBuildService {
/* tslint:disable:no-string-literal */ /* tslint:disable:no-string-literal */
const pageInfo = responseCacheObs const pageInfo = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo'])) .filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).pageInfo) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo)
.map((pInfo: PageInfo) => { .map((pInfo: PageInfo) => {
if (isNotEmpty(pageInfo) && pInfo.currentPage >= 0) { if (isNotEmpty(pageInfo) && pInfo.currentPage >= 0) {
return Object.assign({}, pInfo, {currentPage: pInfo.currentPage + 1}); return Object.assign({}, pInfo, {currentPage: pInfo.currentPage + 1});
@@ -74,13 +82,14 @@ export class RemoteDataBuildService {
// always use self link if that is cached, only if it isn't, get it via the response. // always use self link if that is cached, only if it isn't, get it via the response.
const payload = const payload =
Observable.combineLatest( Observable.combineLatest(
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType).startWith(undefined), hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href, normalizedType))
.startWith(undefined),
responseCacheObs responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
.flatMap((resourceUUIDs: string[]) => { .flatMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceUUIDs)) { if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.get(resourceUUIDs[0], normalizedType); return this.objectCache.getBySelfLink(resourceSelfLinks[0], normalizedType);
} else { } else {
return Observable.of(undefined); return Observable.of(undefined);
} }
@@ -100,7 +109,7 @@ export class RemoteDataBuildService {
}).distinctUntilChanged(); }).distinctUntilChanged();
return new RemoteData( return new RemoteData(
href, hrefObs,
requestPending, requestPending,
responsePending, responsePending,
isSuccessFul, isSuccessFul,
@@ -111,11 +120,18 @@ export class RemoteDataBuildService {
); );
} }
buildList<TNormalized extends CacheableObject, TDomain>(href: string, buildList<TNormalized extends CacheableObject, TDomain>(
normalizedType: GenericConstructor<TNormalized>): RemoteData<TDomain[]> { hrefObs: string | Observable<string>,
const requestObs = this.requestService.get(href) normalizedType: GenericConstructor<TNormalized>
): RemoteData<TDomain[]> {
if (typeof hrefObs === 'string') {
hrefObs = Observable.of(hrefObs);
}
const requestObs = hrefObs.flatMap((href: string) => this.requestService.get(href))
.filter((entry) => hasValue(entry));
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
.filter((entry) => hasValue(entry)); .filter((entry) => hasValue(entry));
const responseCacheObs = this.responseCache.get(href).filter((entry) => hasValue(entry));
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(); const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged();
@@ -136,13 +152,13 @@ export class RemoteDataBuildService {
/* tslint:disable:no-string-literal */ /* tslint:disable:no-string-literal */
const pageInfo = responseCacheObs const pageInfo = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo'])) .filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).pageInfo) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo)
.distinctUntilChanged(); .distinctUntilChanged();
/* tslint:enable:no-string-literal */ /* tslint:enable:no-string-literal */
const payload = responseCacheObs const payload = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
.flatMap((resourceUUIDs: string[]) => { .flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs, normalizedType) return this.objectCache.getList(resourceUUIDs, normalizedType)
.map((normList: TNormalized[]) => { .map((normList: TNormalized[]) => {
@@ -154,7 +170,7 @@ export class RemoteDataBuildService {
.distinctUntilChanged(); .distinctUntilChanged();
return new RemoteData( return new RemoteData(
href, hrefObs,
requestPending, requestPending,
responsePending, responsePending,
isSuccessFul, isSuccessFul,
@@ -179,7 +195,7 @@ export class RemoteDataBuildService {
// are dispatched, but sometimes don't arrive. I'm unsure why atm. // are dispatched, but sometimes don't arrive. I'm unsure why atm.
setTimeout(() => { setTimeout(() => {
normalized[relationship].forEach((href: string) => { normalized[relationship].forEach((href: string) => {
this.requestService.configure(new Request(href)) this.requestService.configure(new RestRequest(href))
}); });
}, 0); }, 0);
@@ -197,7 +213,7 @@ export class RemoteDataBuildService {
// without the setTimeout, the actions inside requestService.configure // without the setTimeout, the actions inside requestService.configure
// are dispatched, but sometimes don't arrive. I'm unsure why atm. // are dispatched, but sometimes don't arrive. I'm unsure why atm.
setTimeout(() => { setTimeout(() => {
this.requestService.configure(new Request(normalized[relationship])); this.requestService.configure(new RestRequest(normalized[relationship]));
}, 0); }, 0);
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
@@ -264,7 +280,7 @@ export class RemoteDataBuildService {
// This is an aggregated object, it doesn't necessarily correspond // This is an aggregated object, it doesn't necessarily correspond
// to a single REST endpoint, so instead of a self link, use the // to a single REST endpoint, so instead of a self link, use the
// current time in ms for a somewhat unique id // current time in ms for a somewhat unique id
`${new Date().getTime()}`, Observable.of(`${new Date().getTime()}`),
requestPending, requestPending,
responsePending, responsePending,
isSuccessFul, isSuccessFul,

View File

@@ -16,26 +16,26 @@ class NullAction extends RemoveFromObjectCacheAction {
} }
describe('objectCacheReducer', () => { describe('objectCacheReducer', () => {
const uuid1 = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const uuid2 = '28b04544-1766-4e82-9728-c4e93544ecd3'; const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
const testState = { const testState = {
[uuid1]: { [selfLink1]: {
data: { data: {
uuid: uuid1, self: selfLink1,
foo: 'bar' foo: 'bar'
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000, msToLive: 900000,
requestHref: 'https://rest.api/endpoint/uuid1' requestHref: selfLink1
}, },
[uuid2]: { [selfLink2]: {
data: { data: {
uuid: uuid2, self: selfLink2,
foo: 'baz' foo: 'baz'
}, },
timeAdded: new Date().getTime(), timeAdded: new Date().getTime(),
msToLive: 900000, msToLive: 900000,
requestHref: 'https://rest.api/endpoint/uuid2' requestHref: selfLink2
} }
}; };
deepFreeze(testState); deepFreeze(testState);
@@ -56,38 +56,38 @@ describe('objectCacheReducer', () => {
it('should add the payload to the cache in response to an ADD action', () => { it('should add the payload to the cache in response to an ADD action', () => {
const state = Object.create(null); const state = Object.create(null);
const objectToCache = { uuid: uuid1 }; const objectToCache = { self: selfLink1 };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const requestHref = 'https://rest.api/endpoint/uuid1'; const requestHref = 'https://rest.api/endpoint/selfLink1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(state, action); const newState = objectCacheReducer(state, action);
expect(newState[uuid1].data).toEqual(objectToCache); expect(newState[selfLink1].data).toEqual(objectToCache);
expect(newState[uuid1].timeAdded).toEqual(timeAdded); expect(newState[selfLink1].timeAdded).toEqual(timeAdded);
expect(newState[uuid1].msToLive).toEqual(msToLive); expect(newState[selfLink1].msToLive).toEqual(msToLive);
}); });
it('should overwrite an object in the cache in response to an ADD action if it already exists', () => { it('should overwrite an object in the cache in response to an ADD action if it already exists', () => {
const objectToCache = { uuid: uuid1, foo: 'baz', somethingElse: true }; const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const requestHref = 'https://rest.api/endpoint/uuid1'; const requestHref = 'https://rest.api/endpoint/selfLink1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
const newState = objectCacheReducer(testState, action); const newState = objectCacheReducer(testState, action);
/* tslint:disable:no-string-literal */ /* tslint:disable:no-string-literal */
expect(newState[uuid1].data['foo']).toBe('baz'); expect(newState[selfLink1].data['foo']).toBe('baz');
expect(newState[uuid1].data['somethingElse']).toBe(true); expect(newState[selfLink1].data['somethingElse']).toBe(true);
/* tslint:enable:no-string-literal */ /* tslint:enable:no-string-literal */
}); });
it('should perform the ADD action without affecting the previous state', () => { it('should perform the ADD action without affecting the previous state', () => {
const state = Object.create(null); const state = Object.create(null);
const objectToCache = { uuid: uuid1 }; const objectToCache = { self: selfLink1 };
const timeAdded = new Date().getTime(); const timeAdded = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const requestHref = 'https://rest.api/endpoint/uuid1'; const requestHref = 'https://rest.api/endpoint/selfLink1';
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
deepFreeze(state); deepFreeze(state);
@@ -95,11 +95,11 @@ describe('objectCacheReducer', () => {
}); });
it('should remove the specified object from the cache in response to the REMOVE action', () => { it('should remove the specified object from the cache in response to the REMOVE action', () => {
const action = new RemoveFromObjectCacheAction(uuid1); const action = new RemoveFromObjectCacheAction(selfLink1);
const newState = objectCacheReducer(testState, action); const newState = objectCacheReducer(testState, action);
expect(testState[uuid1]).not.toBeUndefined(); expect(testState[selfLink1]).not.toBeUndefined();
expect(newState[uuid1]).toBeUndefined(); expect(newState[selfLink1]).toBeUndefined();
}); });
it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => { it("shouldn't do anything in response to the REMOVE action for an object that isn't cached", () => {
@@ -112,7 +112,7 @@ describe('objectCacheReducer', () => {
}); });
it('should perform the REMOVE action without affecting the previous state', () => { it('should perform the REMOVE action without affecting the previous state', () => {
const action = new RemoveFromObjectCacheAction(uuid1); const action = new RemoveFromObjectCacheAction(selfLink1);
// testState has already been frozen above // testState has already been frozen above
objectCacheReducer(testState, action); objectCacheReducer(testState, action);
}); });

View File

@@ -8,11 +8,11 @@ import { CacheEntry } from './cache-entry';
/** /**
* An interface to represent objects that can be cached * An interface to represent objects that can be cached
* *
* A cacheable object should have a uuid * A cacheable object should have a self link
*/ */
export interface CacheableObject { export interface CacheableObject {
uuid: string; uuid?: string;
self?: string; self: string;
} }
/** /**
@@ -28,11 +28,11 @@ export class ObjectCacheEntry implements CacheEntry {
/** /**
* The ObjectCache State * The ObjectCache State
* *
* Consists of a map with UUIDs as keys, * Consists of a map with self links as keys,
* and ObjectCacheEntries as values * and ObjectCacheEntries as values
*/ */
export interface ObjectCacheState { export interface ObjectCacheState {
[uuid: string]: ObjectCacheEntry [href: string]: ObjectCacheEntry
} }
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
@@ -81,7 +81,7 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
*/ */
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.objectToCache.uuid]: { [action.payload.objectToCache.self]: {
data: action.payload.objectToCache, data: action.payload.objectToCache,
timeAdded: action.payload.timeAdded, timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive, msToLive: action.payload.msToLive,

View File

@@ -8,12 +8,12 @@ import { CoreState } from '../core.reducers';
class TestClass implements CacheableObject { class TestClass implements CacheableObject {
constructor( constructor(
public uuid: string, public self: string,
public foo: string public foo: string
) { } ) { }
test(): string { test(): string {
return this.foo + this.uuid; return this.foo + this.self;
} }
} }
@@ -21,12 +21,11 @@ describe('ObjectCacheService', () => {
let service: ObjectCacheService; let service: ObjectCacheService;
let store: Store<CoreState>; let store: Store<CoreState>;
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const requestHref = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
const msToLive = 900000; const msToLive = 900000;
const objectToCache = { const objectToCache = {
uuid: uuid, self: selfLink,
foo: 'bar' foo: 'bar'
}; };
const cacheEntry = { const cacheEntry = {
@@ -48,73 +47,73 @@ describe('ObjectCacheService', () => {
describe('add', () => { describe('add', () => {
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => { it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
service.add(objectToCache, msToLive, requestHref); service.add(objectToCache, msToLive, selfLink);
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestHref)); expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, selfLink));
}); });
}); });
describe('remove', () => { describe('remove', () => {
it('should dispatch a REMOVE action with the UUID of the object to remove', () => { it('should dispatch a REMOVE action with the self link of the object to remove', () => {
service.remove(uuid); service.remove(selfLink);
expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(uuid)); expect(store.dispatch).toHaveBeenCalledWith(new RemoveFromObjectCacheAction(selfLink));
}); });
}); });
describe('get', () => { describe('getBySelfLink', () => {
it('should return an observable of the cached object with the specified UUID and type', () => { it('should return an observable of the cached object with the specified self link and type', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
let testObj: any; let testObj: any;
// due to the implementation of spyOn above, this subscribe will be synchronous // due to the implementation of spyOn above, this subscribe will be synchronous
service.get(uuid, TestClass).take(1).subscribe((o) => testObj = o); service.getBySelfLink(selfLink, TestClass).take(1).subscribe((o) => testObj = o);
expect(testObj.uuid).toBe(uuid); expect(testObj.self).toBe(selfLink);
expect(testObj.foo).toBe('bar'); expect(testObj.foo).toBe('bar');
// this only works if testObj is an instance of TestClass // this only works if testObj is an instance of TestClass
expect(testObj.test()).toBe('bar' + uuid); expect(testObj.test()).toBe('bar' + selfLink);
}); });
it('should not return a cached object that has exceeded its time to live', () => { it('should not return a cached object that has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
let getObsHasFired = false; let getObsHasFired = false;
const subscription = service.get(uuid, TestClass).subscribe((o) => getObsHasFired = true); const subscription = service.getBySelfLink(selfLink, TestClass).subscribe((o) => getObsHasFired = true);
expect(getObsHasFired).toBe(false); expect(getObsHasFired).toBe(false);
subscription.unsubscribe(); subscription.unsubscribe();
}); });
}); });
describe('getList', () => { describe('getList', () => {
it('should return an observable of the array of cached objects with the specified UUID and type', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => {
spyOn(service, 'get').and.returnValue(Observable.of(new TestClass(uuid, 'bar'))); spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(new TestClass(selfLink, 'bar')));
let testObjs: any[]; let testObjs: any[];
service.getList([uuid, uuid], TestClass).take(1).subscribe((arr) => testObjs = arr); service.getList([selfLink, selfLink], TestClass).take(1).subscribe((arr) => testObjs = arr);
expect(testObjs[0].uuid).toBe(uuid); expect(testObjs[0].self).toBe(selfLink);
expect(testObjs[0].foo).toBe('bar'); expect(testObjs[0].foo).toBe('bar');
expect(testObjs[0].test()).toBe('bar' + uuid); expect(testObjs[0].test()).toBe('bar' + selfLink);
expect(testObjs[1].uuid).toBe(uuid); expect(testObjs[1].self).toBe(selfLink);
expect(testObjs[1].foo).toBe('bar'); expect(testObjs[1].foo).toBe('bar');
expect(testObjs[1].test()).toBe('bar' + uuid); expect(testObjs[1].test()).toBe('bar' + selfLink);
}); });
}); });
describe('has', () => { describe('has', () => {
it('should return true if the object with the supplied UUID is cached and still valid', () => { it('should return true if the object with the supplied self link is cached and still valid', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry));
expect(service.has(uuid)).toBe(true); expect(service.hasBySelfLink(selfLink)).toBe(true);
}); });
it("should return false if the object with the supplied UUID isn't cached", () => { it("should return false if the object with the supplied self link isn't cached", () => {
spyOn(store, 'select').and.returnValue(Observable.of(undefined)); spyOn(store, 'select').and.returnValue(Observable.of(undefined));
expect(service.has(uuid)).toBe(false); expect(service.hasBySelfLink(selfLink)).toBe(false);
}); });
it('should return false if the object with the supplied UUID is cached but has exceeded its time to live', () => { it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry));
expect(service.has(uuid)).toBe(false); expect(service.hasBySelfLink(selfLink)).toBe(false);
}); });
}); });

View File

@@ -10,12 +10,12 @@ import { GenericConstructor } from '../shared/generic-constructor';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { keySelector } from '../shared/selectors';
function objectFromUuidSelector(uuid: string): MemoizedSelector<CoreState, ObjectCacheEntry> { function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
return keySelector<ObjectCacheEntry>('data/object', uuid); return keySelector<string>('index/uuid', uuid);
} }
function uuidFromHrefSelector(href: string): MemoizedSelector<CoreState, string> { function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
return keySelector<string>('index/href', href); return keySelector<ObjectCacheEntry>('data/object', selfLink);
} }
/** /**
@@ -35,7 +35,7 @@ export class ObjectCacheService {
* @param msToLive * @param msToLive
* The number of milliseconds it should be cached for * The number of milliseconds it should be cached for
* @param requestHref * @param requestHref
* The href of the request that resulted in this object * The selfLink of the request that resulted in this object
* This isn't necessarily the same as the object's self * This isn't necessarily the same as the object's self
* link, it could have been part of a list for example * link, it could have been part of a list for example
*/ */
@@ -69,55 +69,55 @@ export class ObjectCacheService {
* @return Observable<T> * @return Observable<T>
* An observable of the requested object * An observable of the requested object
*/ */
get<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> { getByUUID<T extends CacheableObject>(uuid: string, type: GenericConstructor<T>): Observable<T> {
return this.getEntry(uuid) return this.store.select(selfLinkFromUuidSelector(uuid))
.flatMap((selfLink: string) => this.getBySelfLink(selfLink, type))
}
getBySelfLink<T extends CacheableObject>(selfLink: string, type: GenericConstructor<T>): Observable<T> {
return this.getEntry(selfLink)
.map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T); .map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data) as T);
} }
getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> { private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
return this.store.select(uuidFromHrefSelector(href)) return this.store.select(entryFromSelfLinkSelector(selfLink))
.flatMap((uuid: string) => this.get(uuid, type))
}
private getEntry(uuid: string): Observable<ObjectCacheEntry> {
return this.store.select(objectFromUuidSelector(uuid))
.filter((entry) => this.isValid(entry)) .filter((entry) => this.isValid(entry))
.distinctUntilChanged(); .distinctUntilChanged();
} }
getRequestHref(uuid: string): Observable<string> { getRequestHrefBySelfLink(selfLink: string): Observable<string> {
return this.getEntry(uuid) return this.getEntry(selfLink)
.map((entry: ObjectCacheEntry) => entry.requestHref) .map((entry: ObjectCacheEntry) => entry.requestHref)
.distinctUntilChanged(); .distinctUntilChanged();
} }
getRequestHrefBySelfLink(self: string): Observable<string> { getRequestHrefByUUID(uuid: string): Observable<string> {
return this.store.select(uuidFromHrefSelector(self)) return this.store.select(selfLinkFromUuidSelector(uuid))
.flatMap((uuid: string) => this.getRequestHref(uuid)); .flatMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink));
} }
/** /**
* Get an observable for an array of objects of the same type * Get an observable for an array of objects of the same type
* with the specified UUIDs * with the specified self links
* *
* The type needs to be specified as well, in order to turn * The type needs to be specified as well, in order to turn
* the cached plain javascript object in to an instance of * the cached plain javascript object in to an instance of
* a class. * a class.
* *
* e.g. getList([ * e.g. getList([
* 'c96588c6-72d3-425d-9d47-fa896255a695', * 'http://localhost:8080/api/core/collections/c96588c6-72d3-425d-9d47-fa896255a695',
* 'cff860da-cf5f-4fda-b8c9-afb7ec0b2d9e' * 'http://localhost:8080/api/core/collections/cff860da-cf5f-4fda-b8c9-afb7ec0b2d9e'
* ], Collection) * ], Collection)
* *
* @param uuids * @param selfLinks
* An array of UUIDs of the objects to get * An array of self links of the objects to get
* @param type * @param type
* The type of the objects to get * The type of the objects to get
* @return Observable<Array<T>> * @return Observable<Array<T>>
*/ */
getList<T extends CacheableObject>(uuids: string[], type: GenericConstructor<T>): Observable<T[]> { getList<T extends CacheableObject>(selfLinks: string[], type: GenericConstructor<T>): Observable<T[]> {
return Observable.combineLatest( return Observable.combineLatest(
uuids.map((id: string) => this.get<T>(id, type)) selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink, type))
); );
} }
@@ -130,12 +130,12 @@ export class ObjectCacheService {
* true if the object with the specified UUID is cached, * true if the object with the specified UUID is cached,
* false otherwise * false otherwise
*/ */
has(uuid: string): boolean { hasByUUID(uuid: string): boolean {
let result: boolean; let result: boolean;
this.store.select(objectFromUuidSelector(uuid)) this.store.select(selfLinkFromUuidSelector(uuid))
.take(1) .take(1)
.subscribe((entry) => result = this.isValid(entry)); .subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
return result; return result;
} }
@@ -143,18 +143,18 @@ export class ObjectCacheService {
/** /**
* Check whether the object with the specified self link is cached * Check whether the object with the specified self link is cached
* *
* @param href * @param selfLink
* The self link of the object to check * The self link of the object to check
* @return boolean * @return boolean
* true if the object with the specified self link is cached, * true if the object with the specified self link is cached,
* false otherwise * false otherwise
*/ */
hasBySelfLink(href: string): boolean { hasBySelfLink(selfLink: string): boolean {
let result = false; let result = false;
this.store.select(uuidFromHrefSelector(href)) this.store.select(entryFromSelfLinkSelector(selfLink))
.take(1) .take(1)
.subscribe((uuid: string) => result = this.has(uuid)); .subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry));
return result; return result;
} }
@@ -175,7 +175,7 @@ export class ObjectCacheService {
const timeOutdated = entry.timeAdded + entry.msToLive; const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated; const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) { if (isOutDated) {
this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.uuid)); this.store.dispatch(new RemoveFromObjectCacheAction(entry.data.self));
} }
return !isOutDated; return !isOutDated;
} }

View File

@@ -1,7 +1,7 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
import { Response } from './response-cache.models'; import { RestResponse } from './response-cache.models';
/** /**
* The list of ResponseCacheAction type definitions * The list of ResponseCacheAction type definitions
@@ -17,12 +17,12 @@ export class ResponseCacheAddAction implements Action {
type = ResponseCacheActionTypes.ADD; type = ResponseCacheActionTypes.ADD;
payload: { payload: {
key: string, key: string,
response: Response response: RestResponse
timeAdded: number; timeAdded: number;
msToLive: number; msToLive: number;
}; };
constructor(key: string, response: Response, timeAdded: number, msToLive: number) { constructor(key: string, response: RestResponse, timeAdded: number, msToLive: number) {
this.payload = { key, response, timeAdded, msToLive }; this.payload = { key, response, timeAdded, msToLive };
} }
} }

View File

@@ -2,16 +2,16 @@ import { RequestError } from '../data/request.models';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class Response { export class RestResponse {
constructor( constructor(
public isSuccessful: boolean, public isSuccessful: boolean,
public statusCode: string public statusCode: string
) { } ) { }
} }
export class SuccessResponse extends Response { export class DSOSuccessResponse extends RestResponse {
constructor( constructor(
public resourceUUIDs: string[], public resourceSelfLinks: string[],
public statusCode: string, public statusCode: string,
public pageInfo?: PageInfo public pageInfo?: PageInfo
) { ) {
@@ -19,7 +19,20 @@ export class SuccessResponse extends Response {
} }
} }
export class ErrorResponse extends Response { export class EndpointMap {
[linkName: string]: string
}
export class RootSuccessResponse extends RestResponse {
constructor(
public endpointMap: EndpointMap,
public statusCode: string,
) {
super(true, statusCode);
}
}
export class ErrorResponse extends RestResponse {
errorMessage: string; errorMessage: string;
constructor(error: RequestError) { constructor(error: RequestError) {

View File

@@ -5,14 +5,14 @@ import {
} from './response-cache.actions'; } from './response-cache.actions';
import { CacheEntry } from './cache-entry'; import { CacheEntry } from './cache-entry';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { Response } from './response-cache.models'; import { RestResponse } from './response-cache.models';
/** /**
* An entry in the ResponseCache * An entry in the ResponseCache
*/ */
export class ResponseCacheEntry implements CacheEntry { export class ResponseCacheEntry implements CacheEntry {
key: string; key: string;
response: Response; response: RestResponse;
timeAdded: number; timeAdded: number;
msToLive: number; msToLive: number;
} }

View File

@@ -6,7 +6,7 @@ import { Observable } from 'rxjs/Observable';
import { ResponseCacheEntry } from './response-cache.reducer'; import { ResponseCacheEntry } from './response-cache.reducer';
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } from '../../shared/empty.util';
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions'; import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
import { Response } from './response-cache.models'; import { RestResponse } from './response-cache.models';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { keySelector } from '../shared/selectors';
@@ -23,7 +23,7 @@ export class ResponseCacheService {
private store: Store<CoreState> private store: Store<CoreState>
) { } ) { }
add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> { add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
if (!this.has(key)) { if (!this.has(key)) {
// this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions)); // this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive)); this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));

View File

@@ -1,12 +1,12 @@
import { ObjectCacheEffects } from './data/object-cache.effects'; import { ObjectCacheEffects } from './data/object-cache.effects';
import { RequestCacheEffects } from './data/request-cache.effects'; import { RequestCacheEffects } from './data/request-cache.effects';
import { HrefIndexEffects } from './index/href-index.effects'; import { UUIDIndexEffects } from './index/uuid-index.effects';
import { RequestEffects } from './data/request.effects'; import { RequestEffects } from './data/request.effects';
export const coreEffects = [ export const coreEffects = [
RequestCacheEffects, RequestCacheEffects,
RequestEffects, RequestEffects,
ObjectCacheEffects, ObjectCacheEffects,
HrefIndexEffects, UUIDIndexEffects,
]; ];

View File

@@ -15,6 +15,8 @@ import { coreEffects } from './core.effects';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store'; import { StoreModule } from '@ngrx/store';
import { coreReducers } from './core.reducers'; import { coreReducers } from './core.reducers';
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { RootResponseParsingService } from './data/root-response-parsing.service';
import { ApiService } from '../shared/api.service'; import { ApiService } from '../shared/api.service';
@@ -41,14 +43,16 @@ const PROVIDERS = [
ApiService, ApiService,
CommunityDataService, CommunityDataService,
CollectionDataService, CollectionDataService,
DSOResponseParsingService,
DSpaceRESTv2Service, DSpaceRESTv2Service,
HostWindowService, HostWindowService,
ItemDataService, ItemDataService,
ObjectCacheService, ObjectCacheService,
PaginationComponentOptions, PaginationComponentOptions,
ResponseCacheService,
RequestService,
RemoteDataBuildService, RemoteDataBuildService,
RequestService,
ResponseCacheService,
RootResponseParsingService,
ServerResponseService, ServerResponseService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory } { provide: NativeWindowService, useFactory: NativeWindowFactory }
]; ];

View File

@@ -2,21 +2,21 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer'; import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { hrefIndexReducer, HrefIndexState } from './index/href-index.reducer'; import { uuidIndexReducer, UUIDIndexState } from './index/uuid-index.reducer';
import { requestReducer, RequestState } from './data/request.reducer'; import { requestReducer, RequestState } from './data/request.reducer';
export interface CoreState { export interface CoreState {
'data/object': ObjectCacheState, 'data/object': ObjectCacheState,
'data/response': ResponseCacheState, 'data/response': ResponseCacheState,
'data/request': RequestState, 'data/request': RequestState,
'index/href': HrefIndexState 'index/uuid': UUIDIndexState
} }
export const coreReducers: ActionReducerMap<CoreState> = { export const coreReducers: ActionReducerMap<CoreState> = {
'data/object': objectCacheReducer, 'data/object': objectCacheReducer,
'data/response': responseCacheReducer, 'data/response': responseCacheReducer,
'data/request': requestReducer, 'data/request': requestReducer,
'index/href': hrefIndexReducer 'index/uuid': uuidIndexReducer
}; };
export const coreSelector = createFeatureSelector<CoreState>('core'); export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -3,7 +3,6 @@ import { Store } from '@ngrx/store';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { NormalizedCollection } from '../cache/models/normalized-collection.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -13,11 +12,10 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable() @Injectable()
export class CollectionDataService extends DataService<NormalizedCollection, Collection> { export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
protected resourceEndpoint = '/core/collections'; protected linkName = 'collections';
protected browseEndpoint = '/discover/browses/dateissued/collections'; protected browseEndpoint = '/discover/browses/dateissued/collections';
constructor( constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,

View File

@@ -4,7 +4,6 @@ import { Store } from '@ngrx/store';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { Community } from '../shared/community.model'; import { Community } from '../shared/community.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -14,11 +13,10 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable() @Injectable()
export class CommunityDataService extends DataService<NormalizedCommunity, Community> { export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
protected resourceEndpoint = '/core/communities'; protected linkName = 'communities';
protected browseEndpoint = '/discover/browses/dateissued/communities'; protected browseEndpoint = '/discover/browses/dateissued/communities';
constructor( constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,

View File

@@ -1,26 +1,28 @@
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, Request } from './request.models'; import {
FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest,
RootEndpointRequest
} from './request.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { Inject } from '@angular/core'; import { GlobalConfig } from '../../../config';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
export abstract class DataService<TNormalized extends CacheableObject, TDomain> { export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
protected abstract objectCache: ObjectCacheService;
protected abstract responseCache: ResponseCacheService; protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract rdbService: RemoteDataBuildService; protected abstract rdbService: RemoteDataBuildService;
protected abstract store: Store<CoreState>; protected abstract store: Store<CoreState>;
protected abstract resourceEndpoint: string; protected abstract linkName: string;
protected abstract browseEndpoint: string; protected abstract browseEndpoint: string;
constructor( constructor(
@@ -30,15 +32,40 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
} }
protected getFindAllHref(options: FindAllOptions = {}): string { private getEndpointMap(): Observable<EndpointMap> {
const request = new RootEndpointRequest(this.EnvConfig);
this.requestService.configure(request);
return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
.map((response: RootSuccessResponse) => response.endpointMap)
.distinctUntilChanged();
}
public getEndpoint(): Observable<string> {
const request = new RootEndpointRequest(this.EnvConfig);
this.requestService.configure(request);
return this.getEndpointMap()
.map((map: EndpointMap) => map[this.linkName])
.distinctUntilChanged();
}
public isEnabledOnRestApi(): Observable<boolean> {
return this.getEndpointMap()
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
.startWith(undefined)
.distinctUntilChanged();
}
protected getFindAllHref(endpoint, options: FindAllOptions = {}): string {
let result; let result;
const args = []; const args = [];
if (hasValue(options.scopeID)) { if (hasValue(options.scopeID)) {
result = this.browseEndpoint; result = new RESTURLCombiner(this.EnvConfig, this.browseEndpoint).toString();
args.push(`scope=${options.scopeID}`); args.push(`scope=${options.scopeID}`);
} else { } else {
result = this.resourceEndpoint; result = endpoint;
} }
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
@@ -61,31 +88,41 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
result = `${result}?${args.join('&')}`; result = `${result}?${args.join('&')}`;
} }
return new RESTURLCombiner(this.EnvConfig, result).toString(); return result;
} }
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> { findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
const href = this.getFindAllHref(options); const hrefObs = this.getEndpoint()
.map((endpoint: string) => this.getFindAllHref(endpoint, options));
hrefObs
.subscribe((href: string) => {
const request = new FindAllRequest(href, options); const request = new FindAllRequest(href, options);
this.requestService.configure(request); this.requestService.configure(request);
return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType); });
// return this.rdbService.buildList(href);
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
} }
protected getFindByIDHref(resourceID): string { protected getFindByIDHref(endpoint, resourceID): string {
return new RESTURLCombiner(this.EnvConfig, `${this.resourceEndpoint}/${resourceID}`).toString(); return `${endpoint}/${resourceID}`;
} }
findById(id: string): RemoteData<TDomain> { findById(id: string): RemoteData<TDomain> {
const href = this.getFindByIDHref(id); const hrefObs = this.getEndpoint()
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
hrefObs
.subscribe((href: string) => {
const request = new FindByIDRequest(href, id); const request = new FindByIDRequest(href, id);
this.requestService.configure(request); this.requestService.configure(request);
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType); });
// return this.rdbService.buildSingle(href);
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
} }
findByHref(href: string): RemoteData<TDomain> { findByHref(href: string): RemoteData<TDomain> {
this.requestService.configure(new Request(href)); this.requestService.configure(new RestRequest(href));
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType); return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
// return this.rdbService.buildSingle(href)); // return this.rdbService.buildSingle(href));
} }

View File

@@ -0,0 +1,149 @@
import { ObjectCacheService } from '../cache/object-cache.service';
import { Inject, Injectable } from '@angular/core';
import { GlobalConfig } from '../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../config';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
import { ResourceType } from '../shared/resource-type';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { CacheableObject } from '../cache/object-cache.reducer';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models';
import { RestRequest } from './request.models';
import { PageInfo } from '../shared/page-info.model';
import { ResponseParsingService } from './parsing.service';
function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
}
function isPaginatedResponse(halObj: any) {
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
}
function flattenSingleKeyObject(obj: any): any {
const keys = Object.keys(obj);
if (keys.length !== 1) {
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
}
return obj[keys[0]];
}
/* tslint:disable:max-classes-per-file */
class ProcessRequestDTO {
[key: string]: NormalizedObject[]
}
@Injectable()
export class DSOResponseParsingService implements ResponseParsingService {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private objectCache: ObjectCacheService,
) {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const processRequestDTO = this.process(data.payload, request.href);
const selfLinks = flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
}
protected process(data: any, requestHref: string): ProcessRequestDTO {
if (isNotEmpty(data)) {
if (isPaginatedResponse(data)) {
return this.process(data._embedded, requestHref);
} else if (isObjectLevel(data)) {
return { topLevel: this.deserializeAndCache(data, requestHref) };
} else {
const result = new ProcessRequestDTO();
if (Array.isArray(data)) {
result.topLevel = [];
data.forEach((datum) => {
if (isPaginatedResponse(datum)) {
const obj = this.process(datum, requestHref);
result.topLevel = [...result.topLevel, ...flattenSingleKeyObject(obj)];
} else {
result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)];
}
});
} else {
Object.keys(data)
.filter((property) => data.hasOwnProperty(property))
.filter((property) => hasValue(data[property]))
.forEach((property) => {
if (isPaginatedResponse(data[property])) {
const obj = this.process(data[property], requestHref);
result[property] = flattenSingleKeyObject(obj);
} else {
result[property] = this.deserializeAndCache(data[property], requestHref);
}
});
}
return result;
}
}
}
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
if (Array.isArray(obj)) {
let result = [];
obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)])
return result;
}
const type: ResourceType = obj.type;
if (hasValue(type)) {
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
if (hasValue(normObjConstructor)) {
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
let processed;
if (isNotEmpty(obj._embedded)) {
processed = this.process(obj._embedded, requestHref);
}
const normalizedObj = serializer.deserialize(obj);
if (isNotEmpty(processed)) {
const linksOnly = {};
Object.keys(processed).forEach((key) => {
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
});
Object.assign(normalizedObj, linksOnly);
}
this.addToObjectCache(normalizedObj, requestHref);
return [normalizedObj];
} else {
// TODO: move check to Validator?
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
return [];
}
} else {
// TODO: move check to Validator
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
return [];
}
}
protected addToObjectCache(co: CacheableObject, requestHref: string): void {
if (hasNoValue(co) || hasNoValue(co.self)) {
throw new Error('The server returned an invalid object');
}
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
}
protected processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
} else {
return undefined;
}
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -4,7 +4,6 @@ import { Store } from '@ngrx/store';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { NormalizedItem } from '../cache/models/normalized-item.model'; import { NormalizedItem } from '../cache/models/normalized-item.model';
@@ -14,11 +13,10 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable() @Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> { export class ItemDataService extends DataService<NormalizedItem, Item> {
protected resourceEndpoint = '/core/items'; protected linkName = 'items';
protected browseEndpoint = '/discover/browses/dateissued/items'; protected browseEndpoint = '/discover/browses/dateissued/items';
constructor( constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,

View File

@@ -0,0 +1,7 @@
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { RestRequest } from './request.models';
import { RestResponse } from '../cache/response-cache.models';
export interface ResponseParsingService {
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse;
}

View File

@@ -14,7 +14,7 @@ export enum RemoteDataState {
*/ */
export class RemoteData<T> { export class RemoteData<T> {
constructor( constructor(
public self: string, public self: Observable<string>,
private requestPending: Observable<boolean>, private requestPending: Observable<boolean>,
private responsePending: Observable<boolean>, private responsePending: Observable<boolean>,
private isSuccessFul: Observable<boolean>, private isSuccessFul: Observable<boolean>,

View File

@@ -1,7 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
import { CacheableObject } from '../cache/object-cache.reducer'; import { RestRequest } from './request.models';
import { Request } from './request.models';
/** /**
* The list of RequestAction type definitions * The list of RequestAction type definitions
@@ -15,10 +14,10 @@ export const RequestActionTypes = {
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class RequestConfigureAction implements Action { export class RequestConfigureAction implements Action {
type = RequestActionTypes.CONFIGURE; type = RequestActionTypes.CONFIGURE;
payload: Request<CacheableObject>; payload: RestRequest;
constructor( constructor(
request: Request<CacheableObject> request: RestRequest
) { ) {
this.payload = request; this.payload = request;
} }

View File

@@ -1,48 +1,18 @@
import { Injectable, Inject } from '@angular/core'; import { Inject, Injectable, Injector } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect } from '@ngrx/effects';
// tslint:disable-next-line:import-blacklist // tslint:disable-next-line:import-blacklist
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { CacheableObject } from '../cache/object-cache.reducer';
import { Response, SuccessResponse, ErrorResponse } from '../cache/response-cache.models';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RequestEntry } from './request.reducer';
import { RequestActionTypes, RequestExecuteAction, RequestCompleteAction } from './request.actions';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from './request.service'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { ResourceType } from '../shared/resource-type'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions';
import { RequestError } from './request.models'; import { RequestError } from './request.models';
import { PageInfo } from '../shared/page-info.model'; import { RequestEntry } from './request.reducer';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { RequestService } from './request.service';
import { GlobalConfig, GLOBAL_CONFIG } from '../../../config';
function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
}
function isPaginatedResponse(halObj: any) {
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
}
function flattenSingleKeyObject(obj: any): any {
const keys = Object.keys(obj);
if (keys.length !== 1) {
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
}
return obj[keys[0]];
}
/* tslint:disable:max-classes-per-file */
class ProcessRequestDTO {
[key: string]: NormalizedObject[]
}
@Injectable() @Injectable()
export class RequestEffects { export class RequestEffects {
@@ -55,121 +25,23 @@ export class RequestEffects {
}) })
.flatMap((entry: RequestEntry) => { .flatMap((entry: RequestEntry) => {
return this.restApi.get(entry.request.href) return this.restApi.get(entry.request.href)
.map((data: DSpaceRESTV2Response) => { .map((data: DSpaceRESTV2Response) =>
const processRequestDTO = this.process(data.payload, entry.request.href); this.injector.get(entry.request.getResponseParser()).parse(entry.request, data))
const uuids = flattenSingleKeyObject(processRequestDTO).map((no) => no.uuid); .do((response: RestResponse) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
return new SuccessResponse(uuids, data.statusCode, this.processPageInfo(data.payload.page)) .map((response: RestResponse) => new RequestCompleteAction(entry.request.href))
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href))
.catch((error: RequestError) => Observable.of(new ErrorResponse(error)) .catch((error: RequestError) => Observable.of(new ErrorResponse(error))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) .do((response: RestResponse) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href))); .map((response: RestResponse) => new RequestCompleteAction(entry.request.href)));
}); });
constructor( constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions, private actions$: Actions,
private restApi: DSpaceRESTv2Service, private restApi: DSpaceRESTv2Service,
private objectCache: ObjectCacheService, private injector: Injector,
private responseCache: ResponseCacheService, private responseCache: ResponseCacheService,
protected requestService: RequestService protected requestService: RequestService
) { } ) { }
protected process(data: any, requestHref: string): ProcessRequestDTO {
if (isNotEmpty(data)) {
if (isPaginatedResponse(data)) {
return this.process(data._embedded, requestHref);
} else if (isObjectLevel(data)) {
return { topLevel: this.deserializeAndCache(data, requestHref) };
} else {
const result = new ProcessRequestDTO();
if (Array.isArray(data)) {
result.topLevel = [];
data.forEach((datum) => {
if (isPaginatedResponse(datum)) {
const obj = this.process(datum, requestHref);
result.topLevel = [...result.topLevel, ...flattenSingleKeyObject(obj)];
} else {
result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)];
}
});
} else {
Object.keys(data)
.filter((property) => data.hasOwnProperty(property))
.filter((property) => hasValue(data[property]))
.forEach((property) => {
if (isPaginatedResponse(data[property])) {
const obj = this.process(data[property], requestHref);
result[property] = flattenSingleKeyObject(obj);
} else {
result[property] = this.deserializeAndCache(data[property], requestHref);
}
});
}
return result;
}
}
}
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
if (Array.isArray(obj)) {
let result = [];
obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)])
return result;
}
const type: ResourceType = obj.type;
if (hasValue(type)) {
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
if (hasValue(normObjConstructor)) {
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
let processed;
if (isNotEmpty(obj._embedded)) {
processed = this.process(obj._embedded, requestHref);
}
const normalizedObj = serializer.deserialize(obj);
if (isNotEmpty(processed)) {
const linksOnly = {};
Object.keys(processed).forEach((key) => {
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
});
Object.assign(normalizedObj, linksOnly);
}
this.addToObjectCache(normalizedObj, requestHref);
return [normalizedObj];
} else {
// TODO: move check to Validator?
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
return [];
}
} else {
// TODO: move check to Validator
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
return [];
}
}
protected addToObjectCache(co: CacheableObject, requestHref: string): void {
if (hasNoValue(co) || hasNoValue(co.uuid)) {
throw new Error('The server returned an invalid object');
}
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
}
protected processPageInfo(pageObj: any): PageInfo {
if (isNotEmpty(pageObj)) {
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
} else {
return undefined;
}
}
} }
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */

View File

@@ -1,15 +1,23 @@
import { SortOptions } from '../cache/models/sort-options.model'; import { SortOptions } from '../cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { GlobalConfig } from '../../../config/global-config.interface';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RootResponseParsingService } from './root-response-parsing.service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class Request<T> { export class RestRequest {
constructor( constructor(
public href: string, public href: string,
) { } ) { }
getResponseParser(): GenericConstructor<ResponseParsingService> {
return DSOResponseParsingService;
}
} }
export class FindByIDRequest<T> extends Request<T> { export class FindByIDRequest extends RestRequest {
constructor( constructor(
href: string, href: string,
public resourceID: string public resourceID: string
@@ -25,7 +33,7 @@ export class FindAllOptions {
sort?: SortOptions; sort?: SortOptions;
} }
export class FindAllRequest<T> extends Request<T> { export class FindAllRequest extends RestRequest {
constructor( constructor(
href: string, href: string,
public options?: FindAllOptions, public options?: FindAllOptions,
@@ -34,6 +42,17 @@ export class FindAllRequest<T> extends Request<T> {
} }
} }
export class RootEndpointRequest extends RestRequest {
constructor(EnvConfig: GlobalConfig) {
const href = new RESTURLCombiner(EnvConfig, '/').toString();
super(href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return RootResponseParsingService;
}
}
export class RequestError extends Error { export class RequestError extends Error {
statusText: string; statusText: string;
} }

View File

@@ -1,12 +1,11 @@
import { CacheableObject } from '../cache/object-cache.reducer';
import { import {
RequestActionTypes, RequestAction, RequestConfigureAction, RequestActionTypes, RequestAction, RequestConfigureAction,
RequestExecuteAction, RequestCompleteAction RequestExecuteAction, RequestCompleteAction
} from './request.actions'; } from './request.actions';
import { Request } from './request.models'; import { RestRequest } from './request.models';
export class RequestEntry { export class RequestEntry {
request: Request<CacheableObject>; request: RestRequest;
requestPending: boolean; requestPending: boolean;
responsePending: boolean; responsePending: boolean;
completed: boolean; completed: boolean;

View File

@@ -3,18 +3,18 @@ import { Injectable } from '@angular/core';
import { MemoizedSelector, Store } from '@ngrx/store'; import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RequestEntry } from './request.reducer';
import { Request } from './request.models';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import { ResponseCacheService } from '../cache/response-cache.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOSuccessResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { SuccessResponse } from '../cache/response-cache.models'; import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { keySelector } from '../shared/selectors';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import { RestRequest } from './request.models';
import { RequestEntry } from './request.reducer';
function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> { function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> {
return keySelector<RequestEntry>('data/request', href); return keySelector<RequestEntry>('data/request', href);
@@ -45,18 +45,25 @@ export class RequestService {
return this.store.select(entryFromHrefSelector(href)); return this.store.select(entryFromHrefSelector(href));
} }
configure<T extends CacheableObject>(request: Request<T>): void { configure<T extends CacheableObject>(request: RestRequest): void {
let isCached = this.objectCache.hasBySelfLink(request.href); let isCached = this.objectCache.hasBySelfLink(request.href);
if (!isCached && this.responseCache.has(request.href)) { if (!isCached && this.responseCache.has(request.href)) {
// if it isn't cached it may be a list endpoint, if so verify const [dsoSuccessResponse, otherSuccessResponse] = this.responseCache.get(request.href)
// every object included in the response is still cached
this.responseCache.get(request.href)
.take(1) .take(1)
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (entry.response as SuccessResponse).resourceUUIDs) .map((entry: ResponseCacheEntry) => entry.response)
.map((resourceUUIDs: string[]) => resourceUUIDs.every((uuid) => this.objectCache.has(uuid))) .share()
.subscribe((c) => isCached = c); .partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
Observable.merge(
otherSuccessResponse.map(() => true),
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)
.map((resourceSelfLinks: string[]) => resourceSelfLinks
.every((selfLink) => this.objectCache.hasBySelfLink(selfLink))
)
).subscribe((c) => isCached = c);
} }
const isPending = this.isPending(request.href); const isPending = this.isPending(request.href);

View File

@@ -0,0 +1,39 @@
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ErrorResponse, RestResponse, RootSuccessResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
@Injectable()
export class RootResponseParsingService implements ResponseParsingService {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
) {
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const links = data.payload._links;
for (const link of Object.keys(links)) {
let href = links[link].href;
// TODO temporary workaround as these endpoint paths are relative, but should be absolute
if (isNotEmpty(href) && !href.startsWith('http')) {
href = new RESTURLCombiner(this.EnvConfig, href.substring(this.EnvConfig.rest.nameSpace.length)).toString();
}
links[link] = href;
}
return new RootSuccessResponse(links, data.statusCode);
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from root endpoint'),
{ statusText: data.statusCode }
)
);
}
}
}

View File

@@ -1,60 +0,0 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
/**
* The list of HrefIndexAction type definitions
*/
export const HrefIndexActionTypes = {
ADD: type('dspace/core/index/href/ADD'),
REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add an href to the index
*/
export class AddToHrefIndexAction implements Action {
type = HrefIndexActionTypes.ADD;
payload: {
href: string;
uuid: string;
};
/**
* Create a new AddToHrefIndexAction
*
* @param href
* the href to add
* @param uuid
* the uuid of the resource the href links to
*/
constructor(href: string, uuid: string) {
this.payload = { href, uuid };
}
}
/**
* An ngrx action to remove an href from the index
*/
export class RemoveUUIDFromHrefIndexAction implements Action {
type = HrefIndexActionTypes.REMOVE_UUID;
payload: string;
/**
* Create a new RemoveUUIDFromHrefIndexAction
*
* @param uuid
* the uuid to remove all hrefs for
*/
constructor(uuid: string) {
this.payload = uuid;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all HrefIndexActions
*/
export type HrefIndexAction = AddToHrefIndexAction | RemoveUUIDFromHrefIndexAction;

View File

@@ -1,46 +0,0 @@
import {
HrefIndexAction,
HrefIndexActionTypes,
AddToHrefIndexAction,
RemoveUUIDFromHrefIndexAction
} from './href-index.actions';
export interface HrefIndexState {
[href: string]: string
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: HrefIndexState = Object.create(null);
export function hrefIndexReducer(state = initialState, action: HrefIndexAction): HrefIndexState {
switch (action.type) {
case HrefIndexActionTypes.ADD: {
return addToHrefIndex(state, action as AddToHrefIndexAction);
}
case HrefIndexActionTypes.REMOVE_UUID: {
return removeUUIDFromHrefIndex(state, action as RemoveUUIDFromHrefIndexAction)
}
default: {
return state;
}
}
}
function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
return Object.assign({}, state, {
[action.payload.href]: action.payload.uuid
});
}
function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
const newState = Object.create(null);
for (const href in state) {
if (state[href] !== action.payload) {
newState[href] = state[href];
}
}
return newState;
}

View File

@@ -0,0 +1,60 @@
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;

View File

@@ -5,26 +5,26 @@ import {
ObjectCacheActionTypes, AddToObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction RemoveFromObjectCacheAction
} from '../cache/object-cache.actions'; } from '../cache/object-cache.actions';
import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from './href-index.actions'; import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
@Injectable() @Injectable()
export class HrefIndexEffects { export class UUIDIndexEffects {
@Effect() add$ = this.actions$ @Effect() add$ = this.actions$
.ofType(ObjectCacheActionTypes.ADD) .ofType(ObjectCacheActionTypes.ADD)
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self)) .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid))
.map((action: AddToObjectCacheAction) => { .map((action: AddToObjectCacheAction) => {
return new AddToHrefIndexAction( return new AddToUUIDIndexAction(
action.payload.objectToCache.self, action.payload.objectToCache.uuid,
action.payload.objectToCache.uuid action.payload.objectToCache.self
); );
}); });
@Effect() remove$ = this.actions$ @Effect() remove$ = this.actions$
.ofType(ObjectCacheActionTypes.REMOVE) .ofType(ObjectCacheActionTypes.REMOVE)
.map((action: RemoveFromObjectCacheAction) => { .map((action: RemoveFromObjectCacheAction) => {
return new RemoveUUIDFromHrefIndexAction(action.payload); return new RemoveHrefFromUUIDIndexAction(action.payload);
}); });
constructor(private actions$: Actions) { constructor(private actions$: Actions) {

View File

@@ -0,0 +1,46 @@
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;
}

View File

@@ -103,7 +103,7 @@ describe('Item', () => {
}); });
function createRemoteDataObject(object: any) { function createRemoteDataObject(object: any) {
const self = ''; const self = Observable.of('');
const requestPending = Observable.of(false); const requestPending = Observable.of(false);
const responsePending = Observable.of(false); const responsePending = Observable.of(false);
const isSuccessful = Observable.of(true); const isSuccessful = Observable.of(true);

View File

@@ -102,7 +102,7 @@ export class SearchService {
}); });
return new RemoteData( return new RemoteData(
self, Observable.of(self),
requestPending, requestPending,
responsePending, responsePending,
itemsRD.hasSucceeded, itemsRD.hasSucceeded,