diff --git a/package.json b/package.json index 8715037a40..4d318ebbd5 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 3c8dc9735f..cc9835dd2d 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -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), diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index d70eb6e0b9..847f6f8287 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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, diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index b872b48af7..d39039c499 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -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 }; diff --git a/src/app/core/data-services/collection-data.effects.ts b/src/app/core/data-services/collection-data.effects.ts new file mode 100644 index 0000000000..9daacf999d --- /dev/null +++ b/src/app/core/data-services/collection-data.effects.ts @@ -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 { + 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 { + return new DSpaceRESTv2Serializer(Collection); + } + + @Effect() findAll$ = this.findAll; + + @Effect() findById$ = this.findById; +} diff --git a/src/app/core/data-services/collection-data.service.ts b/src/app/core/data-services/collection-data.service.ts new file mode 100644 index 0000000000..9f4e2ed902 --- /dev/null +++ b/src/app/core/data-services/collection-data.service.ts @@ -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 { + name = new OpaqueToken('CollectionDataService'); + + constructor( + store: Store, + cache: CacheService + ) { + super(store, cache); + } + +} diff --git a/src/app/core/data-services/collection/collection-data.effects.ts b/src/app/core/data-services/collection/collection-data.effects.ts deleted file mode 100644 index 9a8651944e..0000000000 --- a/src/app/core/data-services/collection/collection-data.effects.ts +++ /dev/null @@ -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) => collections.map(collection => collection.uuid)) - .map((uuids: Array) => 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))); - }); - -} diff --git a/src/app/core/data-services/collection/collection-data.reducer.ts b/src/app/core/data-services/collection/collection-data.reducer.ts deleted file mode 100644 index bb9adc95cc..0000000000 --- a/src/app/core/data-services/collection/collection-data.reducer.ts +++ /dev/null @@ -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); -} diff --git a/src/app/core/data-services/collection/collection-data.service.ts b/src/app/core/data-services/collection/collection-data.service.ts deleted file mode 100644 index 262fcbc357..0000000000 --- a/src/app/core/data-services/collection/collection-data.service.ts +++ /dev/null @@ -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, - private cache: CacheService - ) { } - - findAll(scopeID?: string): Observable { - this.store.dispatch(new CollectionFindMultipleRequestAction(scopeID)); - //get an observable of the IDs from the collectionData store - return this.store.select>('core', 'collectionData', 'findMultiple', 'collectionUUIDs') - .flatMap((collectionUUIDs: Array) => { - // use those IDs to fetch the actual collection objects from the cache - return this.cache.getList(collectionUUIDs); - }); - } - - findById(id: string): Observable { - this.store.dispatch(new CollectionFindByIdRequestAction(id)); - return this.store.select('core', 'collectionData', 'findSingle', 'collectionUUID') - .flatMap((collectionUUID: string) => { - return this.cache.get(collectionUUID); - }); - } - -} diff --git a/src/app/core/data-services/collection/collection-find-multiple.actions.ts b/src/app/core/data-services/collection/collection-find-multiple.actions.ts deleted file mode 100644 index dd79b274c0..0000000000 --- a/src/app/core/data-services/collection/collection-find-multiple.actions.ts +++ /dev/null @@ -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; - - constructor(collectionUUIDs: Array) { - 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; diff --git a/src/app/core/data-services/collection/collection-find-multiple.reducer.ts b/src/app/core/data-services/collection/collection-find-multiple.reducer.ts deleted file mode 100644 index e64ce5011b..0000000000 --- a/src/app/core/data-services/collection/collection-find-multiple.reducer.ts +++ /dev/null @@ -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; - 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; - } - } -}; diff --git a/src/app/core/data-services/collection/collection-find-single.actions.ts b/src/app/core/data-services/collection/collection-find-single.actions.ts deleted file mode 100644 index 392dbe3482..0000000000 --- a/src/app/core/data-services/collection/collection-find-single.actions.ts +++ /dev/null @@ -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; - diff --git a/src/app/core/data-services/collection/collection-find-single.reducer.ts b/src/app/core/data-services/collection/collection-find-single.reducer.ts deleted file mode 100644 index 49c330cdfb..0000000000 --- a/src/app/core/data-services/collection/collection-find-single.reducer.ts +++ /dev/null @@ -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; - } - } -}; diff --git a/src/app/core/data-services/data.actions.ts b/src/app/core/data-services/data.actions.ts new file mode 100644 index 0000000000..caf37331b0 --- /dev/null +++ b/src/app/core/data-services/data.actions.ts @@ -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 + }; + + constructor(key: string, resourceUUIDs: Array) { + 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; diff --git a/src/app/core/data-services/data.effects.ts b/src/app/core/data-services/data.effects.ts new file mode 100644 index 0000000000..362471bdfb --- /dev/null +++ b/src/app/core/data-services/data.effects.ts @@ -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 { + protected abstract getFindAllEndpoint(action: DataFindAllRequestAction): string; + protected abstract getFindByIdEndpoint(action: DataFindByIDRequestAction): string; + protected abstract getSerializer(): Serializer; + + constructor( + private actions$: Actions, + private restApi: DSpaceRESTv2Service, + private cache: CacheService, + private dataService: DataService + ) {} + + // 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) => ts.map(t => t.uuid)) + .map((ids: Array) => 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))); + }); + +} diff --git a/src/app/core/data-services/data.reducer.ts b/src/app/core/data-services/data.reducer.ts new file mode 100644 index 0000000000..3641e790c7 --- /dev/null +++ b/src/app/core/data-services/data.reducer.ts @@ -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; + 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, action); + } + + case DataActionTypes.FIND_BY_ID_REQUEST: { + return findByIDRequest(state, action); + } + + case DataActionTypes.SUCCESS: { + return success(state, action); + } + + case DataActionTypes.ERROR: { + return error(state, 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 + }) + }); +} + + diff --git a/src/app/core/data-services/data.service.ts b/src/app/core/data-services/data.service.ts new file mode 100644 index 0000000000..26a9e4e377 --- /dev/null +++ b/src/app/core/data-services/data.service.ts @@ -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 { + abstract name: OpaqueToken; + + constructor( + private store: Store, + private cache: CacheService + ) { } + + findAll(scopeID?: string): Observable> { + 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>('core', 'data', key, 'resourceUUIDs') + .flatMap((resourceUUIDs: Array) => { + // use those IDs to fetch the actual objects from the cache + return this.cache.getList(resourceUUIDs); + }); + } + + findById(id: string): Observable { + const key = new ParamHash(this.name, 'findById', id).toString(); + this.store.dispatch(new DataFindByIDRequestAction(key, this.name, id)); + return this.store.select>('core', 'data', key, 'resourceUUIDs') + .flatMap((resourceUUIDs: Array) => { + if(isNotEmpty(resourceUUIDs)) { + return this.cache.get(resourceUUIDs[0]); + } + else { + return Observable.of(undefined); + } + }); + } + +} diff --git a/src/app/core/data-services/item-data.effects.ts b/src/app/core/data-services/item-data.effects.ts new file mode 100644 index 0000000000..3976691e48 --- /dev/null +++ b/src/app/core/data-services/item-data.effects.ts @@ -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 { + 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 { + return new DSpaceRESTv2Serializer(Item); + } + + @Effect() findAll$ = this.findAll; + + @Effect() findById$ = this.findById; +} diff --git a/src/app/core/data-services/item-data.service.ts b/src/app/core/data-services/item-data.service.ts new file mode 100644 index 0000000000..2d7128289a --- /dev/null +++ b/src/app/core/data-services/item-data.service.ts @@ -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 { + name = new OpaqueToken('ItemDataService'); + + constructor( + store: Store, + cache: CacheService + ) { + super(store, cache); + } + +} diff --git a/src/app/core/data-services/item/item-data.effects.ts b/src/app/core/data-services/item/item-data.effects.ts deleted file mode 100644 index a8ccf9f223..0000000000 --- a/src/app/core/data-services/item/item-data.effects.ts +++ /dev/null @@ -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) => items.map(item => item.uuid)) - .map((uuids: Array) => 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))); - }); - -} diff --git a/src/app/core/data-services/item/item-data.reducer.ts b/src/app/core/data-services/item/item-data.reducer.ts deleted file mode 100644 index 20c0d805e5..0000000000 --- a/src/app/core/data-services/item/item-data.reducer.ts +++ /dev/null @@ -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); -} diff --git a/src/app/core/data-services/item/item-data.service.ts b/src/app/core/data-services/item/item-data.service.ts deleted file mode 100644 index 36fa5a91db..0000000000 --- a/src/app/core/data-services/item/item-data.service.ts +++ /dev/null @@ -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, - private cache: CacheService - ) { } - - findAll(scopeID?: string): Observable { - this.store.dispatch(new ItemFindMultipleRequestAction(scopeID)); - //get an observable of the IDs from the itemData store - return this.store.select>('core', 'itemData', 'findMultiple', 'itemUUIDs') - .flatMap((itemUUIDs: Array) => { - // use those IDs to fetch the actual item objects from the cache - return this.cache.getList(itemUUIDs); - }); - } - - findById(id: string): Observable { - this.store.dispatch(new ItemFindByIdRequestAction(id)); - return this.store.select('core', 'itemData', 'findSingle', 'itemUUID') - .flatMap((itemUUID: string) => { - return this.cache.get(itemUUID); - }); - } - -} diff --git a/src/app/core/data-services/item/item-find-multiple.actions.ts b/src/app/core/data-services/item/item-find-multiple.actions.ts deleted file mode 100644 index 72649dba85..0000000000 --- a/src/app/core/data-services/item/item-find-multiple.actions.ts +++ /dev/null @@ -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; - - constructor(itemUUIDs: Array) { - 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; diff --git a/src/app/core/data-services/item/item-find-multiple.reducer.ts b/src/app/core/data-services/item/item-find-multiple.reducer.ts deleted file mode 100644 index bb3431b1fc..0000000000 --- a/src/app/core/data-services/item/item-find-multiple.reducer.ts +++ /dev/null @@ -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; - 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; - } - } -}; diff --git a/src/app/core/data-services/item/item-find-single.actions.ts b/src/app/core/data-services/item/item-find-single.actions.ts deleted file mode 100644 index f514d83474..0000000000 --- a/src/app/core/data-services/item/item-find-single.actions.ts +++ /dev/null @@ -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; - diff --git a/src/app/core/data-services/item/item-find-single.reducer.ts b/src/app/core/data-services/item/item-find-single.reducer.ts deleted file mode 100644 index 83382728de..0000000000 --- a/src/app/core/data-services/item/item-find-single.reducer.ts +++ /dev/null @@ -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; - } - } -}; diff --git a/src/app/core/shared/param-hash.spec.ts b/src/app/core/shared/param-hash.spec.ts new file mode 100644 index 0000000000..f532c15235 --- /dev/null +++ b/src/app/core/shared/param-hash.spec.ts @@ -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); + }); +}); diff --git a/src/app/core/shared/param-hash.ts b/src/app/core/shared/param-hash.ts new file mode 100644 index 0000000000..9d07819ce5 --- /dev/null +++ b/src/app/core/shared/param-hash.ts @@ -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; + + 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(); + } +}