Refactored Data Services

This commit is contained in:
Art Lowel
2017-02-21 18:33:16 +01:00
parent de23b1aaeb
commit 9364c32ae2
28 changed files with 515 additions and 651 deletions

View File

@@ -103,6 +103,7 @@
"ng2-translate": "4.2.0",
"preboot": "4.5.2",
"rxjs": "5.0.0-beta.12",
"ts-md5": "^1.2.0",
"webfontloader": "1.6.27",
"zone.js": "0.6.26"
},

View File

@@ -1,6 +1,6 @@
import { EffectsModule } from "@ngrx/effects";
import { CollectionDataEffects } from "./data-services/collection/collection-data.effects";
import { ItemDataEffects } from "./data-services/item/item-data.effects";
import { CollectionDataEffects } from "./data-services/collection-data.effects";
import { ItemDataEffects } from "./data-services/item-data.effects";
export const coreEffects = [
EffectsModule.run(CollectionDataEffects),

View File

@@ -4,9 +4,9 @@ import { SharedModule } from "../shared/shared.module";
import { isNotEmpty } from "../shared/empty.util";
import { FooterComponent } from "./footer/footer.component";
import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
import { CollectionDataService } from "./data-services/collection/collection-data.service";
import { CacheService } from "./data-services/cache/cache.service";
import { ItemDataService } from "./data-services/item/item-data.service";
import { CollectionDataService } from "./data-services/collection-data.service";
import { ItemDataService } from "./data-services/item-data.service";
const IMPORTS = [
CommonModule,

View File

@@ -1,20 +1,14 @@
import { combineReducers } from "@ngrx/store";
import {
CollectionDataState,
collectionDataReducer
} from "./data-services/collection/collection-data.reducer";
import { CacheState, cacheReducer } from "./data-services/cache/cache.reducer";
import { ItemDataState, itemDataReducer } from "./data-services/item/item-data.reducer";
import { dataReducer, DataState } from "./data-services/data.reducer";
export interface CoreState {
collectionData: CollectionDataState,
itemData: ItemDataState,
data: DataState,
cache: CacheState
}
export const reducers = {
collectionData: collectionDataReducer,
itemData: itemDataReducer,
data: dataReducer,
cache: cacheReducer
};

View File

@@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { DataEffects } from "./data.effects";
import { Serializer } from "../serializer";
import { Collection } from "../shared/collection.model";
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
import { CacheService } from "./cache/cache.service";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { Actions, Effect } from "@ngrx/effects";
import { DataFindAllRequestAction, DataFindByIDRequestAction } from "./data.actions";
import { CollectionDataService } from "./collection-data.service";
@Injectable()
export class CollectionDataEffects extends DataEffects<Collection> {
constructor(
actions$: Actions,
restApi: DSpaceRESTv2Service,
cache: CacheService,
dataService: CollectionDataService
) {
super(actions$, restApi, cache, dataService);
}
protected getFindAllEndpoint(action: DataFindAllRequestAction): string {
return '/collections';
}
protected getFindByIdEndpoint(action: DataFindByIDRequestAction): string {
return `/collections/${action.payload.resourceID}`;
}
protected getSerializer(): Serializer<Collection> {
return new DSpaceRESTv2Serializer(Collection);
}
@Effect() findAll$ = this.findAll;
@Effect() findById$ = this.findById;
}

View File

@@ -0,0 +1,19 @@
import { Injectable, OpaqueToken } from "@angular/core";
import { Store } from "@ngrx/store";
import { DataService } from "./data.service";
import { Collection } from "../shared/collection.model";
import { CacheService } from "./cache/cache.service";
import { DataState } from "./data.reducer";
@Injectable()
export class CollectionDataService extends DataService<Collection> {
name = new OpaqueToken('CollectionDataService');
constructor(
store: Store<DataState>,
cache: CacheService
) {
super(store, cache);
}
}

View File

@@ -1,60 +0,0 @@
import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { Collection } from "../../shared/collection.model";
import { Observable } from "rxjs";
import {
CollectionFindMultipleActionTypes,
CollectionFindMultipleSuccessAction,
CollectionFindMultipleErrorAction
} from "./collection-find-multiple.actions";
import {
CollectionFindSingleActionTypes,
CollectionFindByIdSuccessAction,
CollectionFindByIdErrorAction
} from "./collection-find-single.actions";
import { DSpaceRESTV2Response } from "../../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Serializer } from "../../dspace-rest-v2/dspace-rest-v2.serializer";
import { DSpaceRESTv2Service } from "../../dspace-rest-v2/dspace-rest-v2.service";
import { CacheService } from "../cache/cache.service";
import { GlobalConfig } from "../../../../config";
@Injectable()
export class CollectionDataEffects {
constructor(
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private cache: CacheService
) {}
// TODO, results of a findall aren't retrieved from cache for now,
// because currently the cache is more of an object store. We need to move
// more towards memoization for things like this.
@Effect() findAll$ = this.actions$
.ofType(CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST)
.switchMap(() => {
return this.restApi.get('/collections')
.map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Collection).deserializeArray(data))
.do((collections: Collection[]) => {
collections.forEach((collection) => {
this.cache.add(collection, GlobalConfig.cache.msToLive);
});
})
.map((collections: Array<Collection>) => collections.map(collection => collection.uuid))
.map((uuids: Array<string>) => new CollectionFindMultipleSuccessAction(uuids))
.catch((errorMsg: string) => Observable.of(new CollectionFindMultipleErrorAction(errorMsg)));
});
@Effect() findById$ = this.actions$
.ofType(CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST)
.switchMap(action => {
return this.restApi.get(`/collections/${action.payload}`)
.map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Collection).deserialize(data))
.do((collection: Collection) => {
this.cache.add(collection, GlobalConfig.cache.msToLive);
})
.map((collection: Collection) => new CollectionFindByIdSuccessAction(collection.uuid))
.catch((errorMsg: string) => Observable.of(new CollectionFindByIdErrorAction(errorMsg)));
});
}

View File

@@ -1,17 +0,0 @@
import { combineReducers } from "@ngrx/store";
import { CollectionFindMultipleState, findMultipleReducer } from "./collection-find-multiple.reducer";
import { CollectionFindSingleState, findSingleReducer } from "./collection-find-single.reducer";
export interface CollectionDataState {
findMultiple: CollectionFindMultipleState,
findSingle: CollectionFindSingleState
}
const reducers = {
findMultiple: findMultipleReducer,
findSingle: findSingleReducer
};
export function collectionDataReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -1,36 +0,0 @@
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CollectionDataState } from "./collection-data.reducer";
import { Store } from "@ngrx/store";
import { Collection } from "../../shared/collection.model";
import { CollectionFindMultipleRequestAction } from "./collection-find-multiple.actions";
import { CollectionFindByIdRequestAction } from "./collection-find-single.actions";
import { CacheService } from "../cache/cache.service";
import 'rxjs/add/observable/forkJoin';
@Injectable()
export class CollectionDataService {
constructor(
private store: Store<CollectionDataState>,
private cache: CacheService
) { }
findAll(scopeID?: string): Observable<Collection[]> {
this.store.dispatch(new CollectionFindMultipleRequestAction(scopeID));
//get an observable of the IDs from the collectionData store
return this.store.select<Array<string>>('core', 'collectionData', 'findMultiple', 'collectionUUIDs')
.flatMap((collectionUUIDs: Array<string>) => {
// use those IDs to fetch the actual collection objects from the cache
return this.cache.getList<Collection>(collectionUUIDs);
});
}
findById(id: string): Observable<Collection> {
this.store.dispatch(new CollectionFindByIdRequestAction(id));
return this.store.select<string>('core', 'collectionData', 'findSingle', 'collectionUUID')
.flatMap((collectionUUID: string) => {
return this.cache.get<Collection>(collectionUUID);
});
}
}

View File

@@ -1,54 +0,0 @@
import { Action } from "@ngrx/store";
import { type } from "../../../shared/ngrx/type";
import { PaginationOptions } from "../../shared/pagination-options.model";
import { SortOptions } from "../../shared/sort-options.model";
export const CollectionFindMultipleActionTypes = {
FIND_MULTI_REQUEST: type('dspace/core/data/collection/FIND_MULTI_REQUEST'),
FIND_MULTI_SUCCESS: type('dspace/core/data/collection/FIND_MULTI_SUCCESS'),
FIND_MULTI_ERROR: type('dspace/core/data/collection/FIND_MULTI_ERROR')
};
export class CollectionFindMultipleRequestAction implements Action {
type = CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST;
payload: {
scopeID: string,
paginationOptions: PaginationOptions,
sortOptions: SortOptions
};
constructor(
scopeID?: string,
paginationOptions: PaginationOptions = new PaginationOptions(),
sortOptions: SortOptions = new SortOptions()
) {
this.payload = {
scopeID,
paginationOptions,
sortOptions
}
}
}
export class CollectionFindMultipleSuccessAction implements Action {
type = CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS;
payload: Array<string>;
constructor(collectionUUIDs: Array<string>) {
this.payload = collectionUUIDs;
}
}
export class CollectionFindMultipleErrorAction implements Action {
type = CollectionFindMultipleActionTypes.FIND_MULTI_ERROR;
payload: string;
constructor(errorMessage: string) {
this.payload = errorMessage;
}
}
export type CollectionFindMultipleAction
= CollectionFindMultipleRequestAction
| CollectionFindMultipleSuccessAction
| CollectionFindMultipleErrorAction;

View File

@@ -1,59 +0,0 @@
import { PaginationOptions } from "../../shared/pagination-options.model";
import { SortOptions } from "../../shared/sort-options.model";
import {
CollectionFindMultipleAction,
CollectionFindMultipleActionTypes
} from "./collection-find-multiple.actions";
export interface CollectionFindMultipleState {
scopeID: string;
collectionUUIDs: Array<String>;
isLoading: boolean;
errorMessage: string;
paginationOptions: PaginationOptions;
sortOptions: SortOptions;
}
const initialState: CollectionFindMultipleState = {
scopeID: undefined,
collectionUUIDs: [],
isLoading: false,
errorMessage: undefined,
paginationOptions: undefined,
sortOptions: undefined
};
export const findMultipleReducer = (state = initialState, action: CollectionFindMultipleAction): CollectionFindMultipleState => {
switch (action.type) {
case CollectionFindMultipleActionTypes.FIND_MULTI_REQUEST: {
return Object.assign({}, state, {
scopeID: action.payload.scopeID,
collectionUUIDs: [],
isLoading: true,
errorMessage: undefined,
paginationOptions: action.payload.paginationOptions,
sortOptions: action.payload.sortOptions
});
}
case CollectionFindMultipleActionTypes.FIND_MULTI_SUCCESS: {
return Object.assign({}, state, {
isLoading: false,
collectionUUIDs: action.payload,
errorMessage: undefined
});
}
case CollectionFindMultipleActionTypes.FIND_MULTI_ERROR: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: action.payload
});
}
default: {
return state;
}
}
};

View File

@@ -1,42 +0,0 @@
import { Action } from "@ngrx/store";
import { type } from "../../../shared/ngrx/type";
import { Collection } from "../../shared/collection.model";
export const CollectionFindSingleActionTypes = {
FIND_BY_ID_REQUEST: type('dspace/core/data/collection/FIND_BY_ID_REQUEST'),
FIND_BY_ID_SUCCESS: type('dspace/core/data/collection/FIND_BY_ID_SUCCESS'),
FIND_BY_ID_ERROR: type('dspace/core/data/collection/FIND_BY_ID_ERROR')
};
export class CollectionFindByIdRequestAction implements Action {
type = CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST;
payload: string;
constructor(requestID: string) {
this.payload = requestID;
}
}
export class CollectionFindByIdSuccessAction implements Action {
type = CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS;
payload: string;
constructor(collectionUUID: string) {
this.payload = collectionUUID;
}
}
export class CollectionFindByIdErrorAction implements Action {
type = CollectionFindSingleActionTypes.FIND_BY_ID_ERROR;
payload: string;
constructor(errorMessage: string) {
this.payload = errorMessage;
}
}
export type CollectionFindSingleAction
= CollectionFindByIdRequestAction
| CollectionFindByIdSuccessAction
| CollectionFindByIdErrorAction;

View File

@@ -1,51 +0,0 @@
import { Collection } from "../../shared/collection.model";
import {
CollectionFindSingleAction,
CollectionFindSingleActionTypes
} from "./collection-find-single.actions";
export interface CollectionFindSingleState {
isLoading: boolean;
errorMessage: string;
requestedID: string;
collectionUUID: string;
}
const initialState: CollectionFindSingleState = {
isLoading: false,
errorMessage: undefined,
requestedID: undefined,
collectionUUID: undefined
};
export const findSingleReducer = (state = initialState, action: CollectionFindSingleAction): CollectionFindSingleState => {
switch (action.type) {
case CollectionFindSingleActionTypes.FIND_BY_ID_REQUEST: {
return Object.assign({}, state, {
isLoading: true,
errorMessage: undefined,
requestedID: action.payload
});
}
case CollectionFindSingleActionTypes.FIND_BY_ID_SUCCESS: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: undefined,
collectionUUID: action.payload
});
}
case CollectionFindSingleActionTypes.FIND_BY_ID_ERROR: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: action.payload
});
}
default: {
return state;
}
}
};

View File

@@ -0,0 +1,96 @@
import { OpaqueToken } from "@angular/core";
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { PaginationOptions } from "../shared/pagination-options.model";
import { SortOptions } from "../shared/sort-options.model";
export const DataActionTypes = {
FIND_BY_ID_REQUEST: type('dspace/core/data/FIND_BY_ID_REQUEST'),
FIND_ALL_REQUEST: type('dspace/core/data/FIND_ALL_REQUEST'),
SUCCESS: type('dspace/core/data/SUCCESS'),
ERROR: type('dspace/core/data/ERROR')
};
export class DataFindAllRequestAction implements Action {
type = DataActionTypes.FIND_ALL_REQUEST;
payload: {
key: string,
service: OpaqueToken,
scopeID: string,
paginationOptions: PaginationOptions,
sortOptions: SortOptions
};
constructor(
key: string,
service: OpaqueToken,
scopeID?: string,
paginationOptions: PaginationOptions = new PaginationOptions(),
sortOptions: SortOptions = new SortOptions()
) {
this.payload = {
key,
service,
scopeID,
paginationOptions,
sortOptions
}
}
}
export class DataFindByIDRequestAction implements Action {
type = DataActionTypes.FIND_BY_ID_REQUEST;
payload: {
key: string,
service: OpaqueToken,
resourceID: string
};
constructor(
key: string,
service: OpaqueToken,
resourceID: string
) {
this.payload = {
key,
service,
resourceID
}
}
}
export class DataSuccessAction implements Action {
type = DataActionTypes.SUCCESS;
payload: {
key: string,
resourceUUIDs: Array<string>
};
constructor(key: string, resourceUUIDs: Array<string>) {
this.payload = {
key,
resourceUUIDs
};
}
}
export class DataErrorAction implements Action {
type = DataActionTypes.ERROR;
payload: {
key: string,
errorMessage: string
};
constructor(key: string, errorMessage: string) {
this.payload = {
key,
errorMessage
};
}
}
export type DataAction
= DataFindAllRequestAction
| DataFindByIDRequestAction
| DataSuccessAction
| DataErrorAction;

View File

@@ -0,0 +1,60 @@
import { Actions, Effect } from "@ngrx/effects";
import { Observable } from "rxjs";
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { CacheService } from "./cache/cache.service";
import { GlobalConfig } from "../../../config";
import { CacheableObject } from "./cache/cache.reducer";
import { Serializer } from "../serializer";
import {
DataActionTypes, DataFindAllRequestAction, DataSuccessAction,
DataErrorAction, DataFindByIDRequestAction, DataAction
} from "./data.actions";
import { DataService } from "./data.service";
export abstract class DataEffects<T extends CacheableObject> {
protected abstract getFindAllEndpoint(action: DataFindAllRequestAction): string;
protected abstract getFindByIdEndpoint(action: DataFindByIDRequestAction): string;
protected abstract getSerializer(): Serializer<T>;
constructor(
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private cache: CacheService,
private dataService: DataService<T>
) {}
// TODO, results of a findall aren't retrieved from cache for now,
// because currently the cache is more of an object store. We need to move
// more towards memoization for things like this.
protected findAll = this.actions$
.ofType(DataActionTypes.FIND_ALL_REQUEST)
.filter((action: DataFindAllRequestAction) => action.payload.service === this.dataService.name)
.switchMap((action: DataFindAllRequestAction) => {
//TODO scope, pagination, sorting -> when we know how that works in rest
return this.restApi.get(this.getFindAllEndpoint(action))
.map((data: DSpaceRESTV2Response) => this.getSerializer().deserializeArray(data))
.do((ts: T[]) => {
ts.forEach((t) => {
this.cache.add(t, GlobalConfig.cache.msToLive);
});
})
.map((ts: Array<T>) => ts.map(t => t.uuid))
.map((ids: Array<string>) => new DataSuccessAction(action.payload.key, ids))
.catch((errorMsg: string) => Observable.of(new DataErrorAction(action.payload.key, errorMsg)));
});
protected findById = this.actions$
.ofType(DataActionTypes.FIND_BY_ID_REQUEST)
.filter((action: DataFindAllRequestAction) => action.payload.service === this.dataService.name)
.switchMap((action: DataFindByIDRequestAction) => {
return this.restApi.get(this.getFindByIdEndpoint(action))
.map((data: DSpaceRESTV2Response) => this.getSerializer().deserialize(data))
.do((t: T) => {
this.cache.add(t, GlobalConfig.cache.msToLive);
})
.map((t: T) => new DataSuccessAction(action.payload.key, [t.uuid]))
.catch((errorMsg: string) => Observable.of(new DataErrorAction(action.payload.key, errorMsg)));
});
}

View File

@@ -0,0 +1,100 @@
import { PaginationOptions } from "../shared/pagination-options.model";
import { SortOptions } from "../shared/sort-options.model";
import {
DataAction, DataActionTypes, DataFindAllRequestAction,
DataSuccessAction, DataErrorAction, DataFindByIDRequestAction
} from "./data.actions";
import { OpaqueToken } from "@angular/core";
export interface DataRequestState {
service: OpaqueToken
scopeID: string;
resourceID: string;
resourceUUIDs: Array<String>;
resourceType: String;
isLoading: boolean;
errorMessage: string;
paginationOptions: PaginationOptions;
sortOptions: SortOptions;
timeAdded: number;
msToLive: number;
}
export interface DataState {
[key: string]: DataRequestState
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
export const dataReducer = (state = initialState, action: DataAction): DataState => {
switch (action.type) {
case DataActionTypes.FIND_ALL_REQUEST: {
return findAllRequest(state, <DataFindAllRequestAction> action);
}
case DataActionTypes.FIND_BY_ID_REQUEST: {
return findByIDRequest(state, <DataFindByIDRequestAction> action);
}
case DataActionTypes.SUCCESS: {
return success(state, <DataSuccessAction> action);
}
case DataActionTypes.ERROR: {
return error(state, <DataErrorAction> action);
}
default: {
return state;
}
}
};
function findAllRequest(state: DataState, action: DataFindAllRequestAction): DataState {
return Object.assign({}, state, {
[action.payload.key]: {
service: action.payload.service,
scopeID: action.payload.scopeID,
resourceUUIDs: [],
isLoading: true,
errorMessage: undefined,
paginationOptions: action.payload.paginationOptions,
sortOptions: action.payload.sortOptions
}
});
}
function findByIDRequest(state: DataState, action: DataFindByIDRequestAction): DataState {
return Object.assign({}, state, {
[action.payload.key]: {
service: action.payload.service,
resourceID: action.payload.resourceID,
resourceUUIDs: [],
isLoading: true,
errorMessage: undefined,
}
});
}
function success(state: DataState, action: DataSuccessAction): DataState {
return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], {
isLoading: false,
resourceUUIDs: action.payload.resourceUUIDs,
errorMessage: undefined
})
});
}
function error(state: DataState, action: DataErrorAction): DataState {
return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], {
isLoading: false,
errorMessage: action.payload.errorMessage
})
});
}

View File

@@ -0,0 +1,44 @@
import { OpaqueToken } from "@angular/core";
import { Observable } from "rxjs";
import { Store } from "@ngrx/store";
import { CacheService } from "./cache/cache.service";
import { CacheableObject } from "./cache/cache.reducer";
import { DataState } from "./data.reducer";
import { DataFindAllRequestAction, DataFindByIDRequestAction } from "./data.actions";
import { ParamHash } from "../shared/param-hash";
import { isNotEmpty } from "../../shared/empty.util";
export abstract class DataService<T extends CacheableObject> {
abstract name: OpaqueToken;
constructor(
private store: Store<DataState>,
private cache: CacheService
) { }
findAll(scopeID?: string): Observable<Array<T>> {
const key = new ParamHash(this.name, 'findAll', scopeID).toString();
this.store.dispatch(new DataFindAllRequestAction(key, this.name, scopeID));
//get an observable of the IDs from the store
return this.store.select<Array<string>>('core', 'data', key, 'resourceUUIDs')
.flatMap((resourceUUIDs: Array<string>) => {
// use those IDs to fetch the actual objects from the cache
return this.cache.getList<T>(resourceUUIDs);
});
}
findById(id: string): Observable<T> {
const key = new ParamHash(this.name, 'findById', id).toString();
this.store.dispatch(new DataFindByIDRequestAction(key, this.name, id));
return this.store.select<Array<string>>('core', 'data', key, 'resourceUUIDs')
.flatMap((resourceUUIDs: Array<string>) => {
if(isNotEmpty(resourceUUIDs)) {
return this.cache.get<T>(resourceUUIDs[0]);
}
else {
return Observable.of(undefined);
}
});
}
}

View File

@@ -0,0 +1,38 @@
import { Injectable } from "@angular/core";
import { DataEffects } from "./data.effects";
import { Serializer } from "../serializer";
import { Item } from "../shared/item.model";
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
import { CacheService } from "./cache/cache.service";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { Actions, Effect } from "@ngrx/effects";
import { DataFindAllRequestAction, DataFindByIDRequestAction } from "./data.actions";
import { ItemDataService } from "./item-data.service";
@Injectable()
export class ItemDataEffects extends DataEffects<Item> {
constructor(
actions$: Actions,
restApi: DSpaceRESTv2Service,
cache: CacheService,
dataService: ItemDataService
) {
super(actions$, restApi, cache, dataService);
}
protected getFindAllEndpoint(action: DataFindAllRequestAction): string {
return '/items';
}
protected getFindByIdEndpoint(action: DataFindByIDRequestAction): string {
return `/items/${action.payload.resourceID}`;
}
protected getSerializer(): Serializer<Item> {
return new DSpaceRESTv2Serializer(Item);
}
@Effect() findAll$ = this.findAll;
@Effect() findById$ = this.findById;
}

View File

@@ -0,0 +1,19 @@
import { Injectable, OpaqueToken } from "@angular/core";
import { Store } from "@ngrx/store";
import { DataService } from "./data.service";
import { Item } from "../shared/item.model";
import { CacheService } from "./cache/cache.service";
import { DataState } from "./data.reducer";
@Injectable()
export class ItemDataService extends DataService<Item> {
name = new OpaqueToken('ItemDataService');
constructor(
store: Store<DataState>,
cache: CacheService
) {
super(store, cache);
}
}

View File

@@ -1,60 +0,0 @@
import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { Item } from "../../shared/item.model";
import { Observable } from "rxjs";
import {
ItemFindMultipleActionTypes,
ItemFindMultipleSuccessAction,
ItemFindMultipleErrorAction
} from "./item-find-multiple.actions";
import {
ItemFindSingleActionTypes,
ItemFindByIdSuccessAction,
ItemFindByIdErrorAction
} from "./item-find-single.actions";
import { DSpaceRESTV2Response } from "../../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Serializer } from "../../dspace-rest-v2/dspace-rest-v2.serializer";
import { DSpaceRESTv2Service } from "../../dspace-rest-v2/dspace-rest-v2.service";
import { CacheService } from "../cache/cache.service";
import { GlobalConfig } from "../../../../config";
@Injectable()
export class ItemDataEffects {
constructor(
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private cache: CacheService
) {}
// TODO, results of a findall aren't retrieved from cache for now,
// because currently the cache is more of an object store. We need to move
// more towards memoization for things like this.
@Effect() findAll$ = this.actions$
.ofType(ItemFindMultipleActionTypes.FIND_MULTI_REQUEST)
.switchMap(() => {
return this.restApi.get('/items')
.map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Item).deserializeArray(data))
.do((items: Item[]) => {
items.forEach((item) => {
this.cache.add(item, GlobalConfig.cache.msToLive);
});
})
.map((items: Array<Item>) => items.map(item => item.uuid))
.map((uuids: Array<string>) => new ItemFindMultipleSuccessAction(uuids))
.catch((errorMsg: string) => Observable.of(new ItemFindMultipleErrorAction(errorMsg)));
});
@Effect() findById$ = this.actions$
.ofType(ItemFindSingleActionTypes.FIND_BY_ID_REQUEST)
.switchMap(action => {
return this.restApi.get(`/items/${action.payload}`)
.map((data: DSpaceRESTV2Response) => new DSpaceRESTv2Serializer(Item).deserialize(data))
.do((item: Item) => {
this.cache.add(item, GlobalConfig.cache.msToLive);
})
.map((item: Item) => new ItemFindByIdSuccessAction(item.uuid))
.catch((errorMsg: string) => Observable.of(new ItemFindByIdErrorAction(errorMsg)));
});
}

View File

@@ -1,17 +0,0 @@
import { combineReducers } from "@ngrx/store";
import { ItemFindMultipleState, findMultipleReducer } from "./item-find-multiple.reducer";
import { ItemFindSingleState, findSingleReducer } from "./item-find-single.reducer";
export interface ItemDataState {
findMultiple: ItemFindMultipleState,
findSingle: ItemFindSingleState
}
const reducers = {
findMultiple: findMultipleReducer,
findSingle: findSingleReducer
};
export function itemDataReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -1,36 +0,0 @@
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { ItemDataState } from "./item-data.reducer";
import { Store } from "@ngrx/store";
import { Item } from "../../shared/item.model";
import { ItemFindMultipleRequestAction } from "./item-find-multiple.actions";
import { ItemFindByIdRequestAction } from "./item-find-single.actions";
import { CacheService } from "../cache/cache.service";
import 'rxjs/add/observable/forkJoin';
@Injectable()
export class ItemDataService {
constructor(
private store: Store<ItemDataState>,
private cache: CacheService
) { }
findAll(scopeID?: string): Observable<Item[]> {
this.store.dispatch(new ItemFindMultipleRequestAction(scopeID));
//get an observable of the IDs from the itemData store
return this.store.select<Array<string>>('core', 'itemData', 'findMultiple', 'itemUUIDs')
.flatMap((itemUUIDs: Array<string>) => {
// use those IDs to fetch the actual item objects from the cache
return this.cache.getList<Item>(itemUUIDs);
});
}
findById(id: string): Observable<Item> {
this.store.dispatch(new ItemFindByIdRequestAction(id));
return this.store.select<string>('core', 'itemData', 'findSingle', 'itemUUID')
.flatMap((itemUUID: string) => {
return this.cache.get<Item>(itemUUID);
});
}
}

View File

@@ -1,54 +0,0 @@
import { Action } from "@ngrx/store";
import { type } from "../../../shared/ngrx/type";
import { PaginationOptions } from "../../shared/pagination-options.model";
import { SortOptions } from "../../shared/sort-options.model";
export const ItemFindMultipleActionTypes = {
FIND_MULTI_REQUEST: type('dspace/core/data/item/FIND_MULTI_REQUEST'),
FIND_MULTI_SUCCESS: type('dspace/core/data/item/FIND_MULTI_SUCCESS'),
FIND_MULTI_ERROR: type('dspace/core/data/item/FIND_MULTI_ERROR')
};
export class ItemFindMultipleRequestAction implements Action {
type = ItemFindMultipleActionTypes.FIND_MULTI_REQUEST;
payload: {
scopeID: string,
paginationOptions: PaginationOptions,
sortOptions: SortOptions
};
constructor(
scopeID?: string,
paginationOptions: PaginationOptions = new PaginationOptions(),
sortOptions: SortOptions = new SortOptions()
) {
this.payload = {
scopeID,
paginationOptions,
sortOptions
}
}
}
export class ItemFindMultipleSuccessAction implements Action {
type = ItemFindMultipleActionTypes.FIND_MULTI_SUCCESS;
payload: Array<string>;
constructor(itemUUIDs: Array<string>) {
this.payload = itemUUIDs;
}
}
export class ItemFindMultipleErrorAction implements Action {
type = ItemFindMultipleActionTypes.FIND_MULTI_ERROR;
payload: string;
constructor(errorMessage: string) {
this.payload = errorMessage;
}
}
export type ItemFindMultipleAction
= ItemFindMultipleRequestAction
| ItemFindMultipleSuccessAction
| ItemFindMultipleErrorAction;

View File

@@ -1,59 +0,0 @@
import { PaginationOptions } from "../../shared/pagination-options.model";
import { SortOptions } from "../../shared/sort-options.model";
import {
ItemFindMultipleAction,
ItemFindMultipleActionTypes
} from "./item-find-multiple.actions";
export interface ItemFindMultipleState {
scopeID: string;
itemUUIDs: Array<String>;
isLoading: boolean;
errorMessage: string;
paginationOptions: PaginationOptions;
sortOptions: SortOptions;
}
const initialState: ItemFindMultipleState = {
scopeID: undefined,
itemUUIDs: [],
isLoading: false,
errorMessage: undefined,
paginationOptions: undefined,
sortOptions: undefined
};
export const findMultipleReducer = (state = initialState, action: ItemFindMultipleAction): ItemFindMultipleState => {
switch (action.type) {
case ItemFindMultipleActionTypes.FIND_MULTI_REQUEST: {
return Object.assign({}, state, {
scopeID: action.payload.scopeID,
itemUUIDs: [],
isLoading: true,
errorMessage: undefined,
paginationOptions: action.payload.paginationOptions,
sortOptions: action.payload.sortOptions
});
}
case ItemFindMultipleActionTypes.FIND_MULTI_SUCCESS: {
return Object.assign({}, state, {
isLoading: false,
itemUUIDs: action.payload,
errorMessage: undefined
});
}
case ItemFindMultipleActionTypes.FIND_MULTI_ERROR: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: action.payload
});
}
default: {
return state;
}
}
};

View File

@@ -1,42 +0,0 @@
import { Action } from "@ngrx/store";
import { type } from "../../../shared/ngrx/type";
import { Item } from "../../shared/item.model";
export const ItemFindSingleActionTypes = {
FIND_BY_ID_REQUEST: type('dspace/core/data/item/FIND_BY_ID_REQUEST'),
FIND_BY_ID_SUCCESS: type('dspace/core/data/item/FIND_BY_ID_SUCCESS'),
FIND_BY_ID_ERROR: type('dspace/core/data/item/FIND_BY_ID_ERROR')
};
export class ItemFindByIdRequestAction implements Action {
type = ItemFindSingleActionTypes.FIND_BY_ID_REQUEST;
payload: string;
constructor(requestID: string) {
this.payload = requestID;
}
}
export class ItemFindByIdSuccessAction implements Action {
type = ItemFindSingleActionTypes.FIND_BY_ID_SUCCESS;
payload: string;
constructor(itemUUID: string) {
this.payload = itemUUID;
}
}
export class ItemFindByIdErrorAction implements Action {
type = ItemFindSingleActionTypes.FIND_BY_ID_ERROR;
payload: string;
constructor(errorMessage: string) {
this.payload = errorMessage;
}
}
export type ItemFindSingleAction
= ItemFindByIdRequestAction
| ItemFindByIdSuccessAction
| ItemFindByIdErrorAction;

View File

@@ -1,51 +0,0 @@
import { Item } from "../../shared/item.model";
import {
ItemFindSingleAction,
ItemFindSingleActionTypes
} from "./item-find-single.actions";
export interface ItemFindSingleState {
isLoading: boolean;
errorMessage: string;
requestedID: string;
itemUUID: string;
}
const initialState: ItemFindSingleState = {
isLoading: false,
errorMessage: undefined,
requestedID: undefined,
itemUUID: undefined
};
export const findSingleReducer = (state = initialState, action: ItemFindSingleAction): ItemFindSingleState => {
switch (action.type) {
case ItemFindSingleActionTypes.FIND_BY_ID_REQUEST: {
return Object.assign({}, state, {
isLoading: true,
errorMessage: undefined,
requestedID: action.payload
});
}
case ItemFindSingleActionTypes.FIND_BY_ID_SUCCESS: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: undefined,
itemUUID: action.payload
});
}
case ItemFindSingleActionTypes.FIND_BY_ID_ERROR: {
return Object.assign({}, state, {
isLoading: false,
errorMessage: action.payload
});
}
default: {
return state;
}
}
};

View File

@@ -0,0 +1,58 @@
import { ParamHash } from "./param-hash";
describe("ParamHash", () => {
it("should return a hash for a set of parameters", () => {
const hash = new ParamHash('azerty', true, 23).toString();
expect(hash).not.toBeNull();
expect(hash).not.toBe('');
});
it("should work with both simple and complex objects as parameters", () => {
const hash = new ParamHash('azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }).toString();
expect(hash).not.toBeNull();
expect(hash).not.toBe('');
});
it("should work with null or undefined as parameters", () => {
const hash1 = new ParamHash(undefined).toString();
const hash2 = new ParamHash(null).toString();
const hash3 = new ParamHash(undefined, null).toString();
expect(hash1).not.toBeNull();
expect(hash1).not.toBe('');
expect(hash2).not.toBeNull();
expect(hash2).not.toBe('');
expect(hash3).not.toBeNull();
expect(hash3).not.toBe('');
expect(hash1).not.toEqual(hash2);
expect(hash1).not.toEqual(hash3);
expect(hash2).not.toEqual(hash3);
});
it("should work if created without parameters", () => {
const hash1 = new ParamHash().toString();
const hash2 = new ParamHash().toString();
expect(hash1).not.toBeNull();
expect(hash1).not.toBe('');
expect(hash1).toEqual(hash2);
});
it("should create the same hash if created with the same set of parameters in the same order", () => {
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
const hash1 = new ParamHash(...params).toString();
const hash2 = new ParamHash(...params).toString();
expect(hash1).toEqual(hash2);
});
it("should create a different hash if created with the same set of parameters in a different order", () => {
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
const hash1 = new ParamHash(...params).toString();
const hash2 = new ParamHash(...params.reverse()).toString();
expect(hash1).not.toEqual(hash2);
});
});

View File

@@ -0,0 +1,35 @@
import { Md5 } from "ts-md5/dist/md5";
/**
* Creates a hash of a set of parameters
*/
export class ParamHash {
private params: Array<any>;
constructor(...params) {
this.params = params;
}
/**
* Returns an md5 hash based on the
* params passed to the constructor
*
* If you hash the same set of params in the
* same order the hashes will be identical
*
* @return {string}
* an md5 hash
*/
toString(): string {
let hash = new Md5();
this.params.forEach((param) => {
if (param === Object(param)) {
hash.appendStr(JSON.stringify(param));
}
else {
hash.appendStr('' + param);
}
});
return hash.end().toString();
}
}