mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-11 20:13:07 +00:00
resolved merge conflicts
This commit is contained in:
@@ -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,
|
||||||
|
46
src/app/core/cache/object-cache.reducer.spec.ts
vendored
46
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -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);
|
||||||
});
|
});
|
||||||
|
12
src/app/core/cache/object-cache.reducer.ts
vendored
12
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -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,
|
||||||
|
57
src/app/core/cache/object-cache.service.spec.ts
vendored
57
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
68
src/app/core/cache/object-cache.service.ts
vendored
68
src/app/core/cache/object-cache.service.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
6
src/app/core/cache/response-cache.actions.ts
vendored
6
src/app/core/cache/response-cache.actions.ts
vendored
@@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
src/app/core/cache/response-cache.models.ts
vendored
21
src/app/core/cache/response-cache.models.ts
vendored
@@ -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) {
|
||||||
|
4
src/app/core/cache/response-cache.reducer.ts
vendored
4
src/app/core/cache/response-cache.reducer.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
4
src/app/core/cache/response-cache.service.ts
vendored
4
src/app/core/cache/response-cache.service.ts
vendored
@@ -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));
|
||||||
|
@@ -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,
|
||||||
];
|
];
|
||||||
|
@@ -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 }
|
||||||
];
|
];
|
||||||
|
@@ -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');
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
149
src/app/core/data/dso-response-parsing.service.ts
Normal file
149
src/app/core/data/dso-response-parsing.service.ts
Normal 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 */
|
@@ -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,
|
||||||
|
7
src/app/core/data/parsing.service.ts
Normal file
7
src/app/core/data/parsing.service.ts
Normal 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;
|
||||||
|
}
|
@@ -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>,
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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 */
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
39
src/app/core/data/root-response-parsing.service.ts
Normal file
39
src/app/core/data/root-response-parsing.service.ts
Normal 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 }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
|
@@ -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;
|
|
||||||
}
|
|
60
src/app/core/index/uuid-index.actions.ts
Normal file
60
src/app/core/index/uuid-index.actions.ts
Normal 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;
|
@@ -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) {
|
46
src/app/core/index/uuid-index.reducer.ts
Normal file
46
src/app/core/index/uuid-index.reducer.ts
Normal 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;
|
||||||
|
}
|
@@ -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);
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user