diff --git a/src/app/modules/data/data.module.ts b/src/app/modules/data/data.module.ts index 25c46bb7..95d75741 100644 --- a/src/app/modules/data/data.module.ts +++ b/src/app/modules/data/data.module.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 StApps + * Copyright (C) 2018, 2019 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -17,6 +17,7 @@ import {HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {IonicModule} from '@ionic/angular'; +import {StorageModule} from '../storage/storage.module'; import {DataRoutingModule} from './data-routing.module'; import {DataProvider} from './data.provider'; import {DataDetailComponent} from './detail/data-detail.component'; @@ -48,6 +49,7 @@ import {EventListItemComponent} from './types/event/event-list-item.component'; FormsModule, DataRoutingModule, HttpClientModule, + StorageModule, ], providers: [ DataProvider, diff --git a/src/app/modules/data/data.provider.spec.ts b/src/app/modules/data/data.provider.spec.ts new file mode 100644 index 00000000..3f0826a5 --- /dev/null +++ b/src/app/modules/data/data.provider.spec.ts @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2018, 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {TestBed} from '@angular/core/testing'; +import {Client} from '@openstapps/api/lib/client'; +import {SCMessage, SCSaveableThing, SCSearchQuery, SCSearchResponse, + SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core'; +import {StorageProvider} from '../storage/storage.provider'; +import {DataModule} from './data.module'; +import {DataProvider, DataScope} from './data.provider'; +import {StAppsWebHttpClient} from './stapps-web-http-client.provider'; + +describe('DataProvider', () => { + let dataProvider: DataProvider; + let storageProvider: StorageProvider; + const sampleResponse: SCSearchResponse = { + data: [ + { + categories: ['main dish'], + name: 'foo dish', + origin: { + indexed: '12345', + name: 'bar', + type: SCThingOriginType.Remote, + }, + type: SCThingType.Dish, + uid: '123', + }, + { + categories: ['dessert'], + name: 'foo dessert', + origin: { + indexed: '12345', + name: 'bar', + type: SCThingOriginType.Remote, + }, + type: SCThingType.Dish, + uid: '123', + }, + ], + facets: [ + { + buckets: [], + field: 'foo', + }, + ], + pagination: { + count: 0, + offset: 0, + total: 0, + }, + stats: { + time: 123, + }, + }; + const sampleFilter: SCSearchValueFilter = { + arguments: { + field: 'type', + value: 'dish', + }, + type: 'value', + }; + const sampleQuery: SCSearchQuery = { + filter: sampleFilter, + }; + + const sampleThing: SCMessage = { + audiences: ['students'], + message: 'Foo Message', + name: 'foo', + origin: { + indexed: 'SOME-DATE', + name: 'some name', + type: SCThingOriginType.Remote, + }, + type: SCThingType.Message, + uid: '123', + }; + const sampleSavable: SCSaveableThing = { + data: sampleThing, + name: sampleThing.name, + origin: { + created: new Date().toISOString(), + type: SCThingOriginType.User, + }, + type: SCThingType.Message, + uid: sampleThing.uid, + }; + const otherSampleThing: SCMessage = {...sampleThing, uid: '456', name: 'bar'}; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [DataModule], + providers: [ + DataProvider, + StAppsWebHttpClient, + ], + }); + storageProvider = TestBed.get(StorageProvider); + dataProvider = TestBed.get(DataProvider); + await storageProvider.deleteAll(); + }); + + it('should generate data key', async () => { + dataProvider.storagePrefix = 'foo.data'; + expect(dataProvider.getDataKey('123')).toBe('foo.data.123'); + }); + + it('should provide backend data items using search query', async () => { + spyOn(Client.prototype, 'search').and.callFake(() => { + return { + then: (callback: any) => { + return callback(sampleResponse); + }, + }; + }); + const response = await dataProvider.search(sampleQuery); + expect(response).toEqual(sampleResponse); + }); + + it('should put an data item into the local database (storage)', async () => { + let providedThing: SCSaveableThing; + spyOn(storageProvider, 'put').and.callFake((_id: any, thing: any) => { + providedThing = thing; + providedThing.origin.created = sampleSavable.origin.created; + }); + expect(storageProvider.put).not.toHaveBeenCalled(); + expect(providedThing!).not.toBeDefined(); + await dataProvider.put(sampleThing); + expect(providedThing!).toBeDefined(); + expect(providedThing!).toEqual(sampleSavable); + }); + + it('should correctly set and get single data item from the local database (storage)', async () => { + await dataProvider.put(sampleThing); + spyOn(storageProvider, 'get').and.callThrough(); + expect(storageProvider.get).not.toHaveBeenCalled(); + const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Local); + providedThing.origin.created = sampleSavable.origin.created; + expect(storageProvider.get).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid)); + expect(providedThing).toEqual(sampleSavable); + }); + + it('should provide all data items from the local database (storage)', async () => { + await dataProvider.put(sampleThing); + await dataProvider.put(otherSampleThing); + const result = await dataProvider.getAll(); + expect(Array.from(result.keys()).sort()).toEqual([ + dataProvider.getDataKey(sampleThing.uid), dataProvider.getDataKey(otherSampleThing.uid), + ]); + expect(result.get(dataProvider.getDataKey(sampleThing.uid))!.data).toEqual(sampleThing); + expect(result.get(dataProvider.getDataKey(otherSampleThing.uid))!.data).toEqual(otherSampleThing); + }); + + it('should provide single data from the backend', async () => { + spyOn(Client.prototype, 'getThing').and.callFake(() => { + return { + then: (callback: any) => { + return callback(sampleThing); + }, + }; + }); + expect(Client.prototype.getThing).not.toHaveBeenCalled(); + const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Remote); + expect(Client.prototype.getThing).toHaveBeenCalledWith(sampleThing.uid); + expect(providedThing).toBe(sampleThing); + }); + + it('should get an item from both local and remote database', async () => { + spyOn(Client.prototype, 'getThing').and.callFake(() => { + return { + then: (callback: any) => { + return callback(sampleThing); + }, + }; + }); + spyOn(storageProvider, 'get').and.callFake(() => { + return { + then: (callback: any) => { + return callback(sampleSavable); + }, + }; + }); + const result = await dataProvider.get(sampleThing.uid); + expect(result.get(DataScope.Local)).toEqual(sampleSavable); + expect(result.get(DataScope.Remote)).toEqual(sampleThing); + }); + + it('should properly delete a data item from the local database (storage)', async () => { + spyOn(storageProvider, 'delete').and.callThrough(); + await dataProvider.put(sampleThing); + expect(await storageProvider.length()).toBe(1); + await dataProvider.delete(sampleThing.uid); + expect(storageProvider.delete).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid)); + expect(await storageProvider.length()).toBe(0); + }); + + it('should properly delete all the data items from the local database (storage)', async () => { + spyOn(storageProvider, 'delete').and.callThrough(); + await dataProvider.put(sampleThing); + await dataProvider.put(otherSampleThing); + await storageProvider.put('some-uid', {some: 'thing'}); + expect(await storageProvider.length()).toBe(3); + await dataProvider.deleteAll(); + expect(storageProvider.delete).toHaveBeenCalledWith( + dataProvider.getDataKey(sampleThing.uid), + dataProvider.getDataKey(otherSampleThing.uid), + ); + const result = await storageProvider.getAll(); + expect(Array.from(result.keys())).toEqual(['some-uid']); + }); +}); diff --git a/src/app/modules/data/data.provider.ts b/src/app/modules/data/data.provider.ts index e461a7d8..e8518a4f 100644 --- a/src/app/modules/data/data.provider.ts +++ b/src/app/modules/data/data.provider.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 StApps + * Copyright (C) 2018, 2019 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -13,6 +13,16 @@ * this program. If not, see . */ import {Injectable} from '@angular/core'; +import {Client} from '@openstapps/api/lib/client'; +import {SCSearchQuery, SCSearchResponse, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core'; +import {SCSaveableThing} from '@openstapps/core'; +import {StorageProvider} from '../storage/storage.provider'; +import {StAppsWebHttpClient} from './stapps-web-http-client.provider'; + +export enum DataScope { + Local = 'local', + Remote = 'remote', +} /* Generated class for the DataProvider provider. @@ -22,7 +32,119 @@ import {Injectable} from '@angular/core'; */ @Injectable() export class DataProvider { - constructor() { - // @TODO + private _storagePrefix: string = 'stapps.data'; + // @TODO: get backendUrl and appVersion and storagePrefix from config (module) + appVersion: string = '1.0.6'; + backendUrl: string = 'https://stappsbe01.innocampus.tu-berlin.de'; + client: Client; + storageProvider: StorageProvider; + + constructor(stAppsWebHttpClient: StAppsWebHttpClient, storageProvider: StorageProvider) { + this.client = new Client(stAppsWebHttpClient, this.backendUrl, this.appVersion); + this.storageProvider = storageProvider; + } + + get storagePrefix(): string { + return this._storagePrefix; + } + + set storagePrefix(storagePrefix) { + this._storagePrefix = storagePrefix; + } + + /** + * Provides key for storing data into the local database + * + * @param uid Unique identifier of a resource + */ + getDataKey(uid: string): string { + return `${this.storagePrefix}.${uid}`; + } + + /** + * Provides a saveable thing from the local database using the provided UID + */ + async get(uid: string, scope: DataScope.Local): Promise>; + /** + * Provides a thing from the backend + */ + async get(uid: string, scope: DataScope.Remote): Promise>; + /** + * Provides a thing from both local database and backend + */ + async get(uid: string): Promise>>; + + /** + * Provides a thing from the local database only, backend only or both, depending on the scope + * + * @param uid Unique identifier of a thing + * @param scope From where data should be provided + */ + async get(uid: string, scope?: DataScope): + Promise | Map>> { + if (scope === DataScope.Local) { + return this.storageProvider.get>(this.getDataKey(uid)); + } + if (scope === DataScope.Remote) { + return this.client.getThing(uid); + } + const map: Map> = new Map(); + map.set(DataScope.Local, await this.get(uid, DataScope.Local)); + map.set(DataScope.Remote, await this.get(uid, DataScope.Remote)); + return map; + } + + /** + * Provides all things saved in the local database + */ + async getAll(): Promise>> { + return this.storageProvider.search>(this.storagePrefix); + } + + /** + * Save a data item + * + * @param item Data item that needs to be saved + * @param [type] Saveable type (e.g. 'favorite'); if nothing is provided then type of the thing is used + */ + async put(item: SCThings, type?: SCThingType): Promise> { + const saveableItem: SCSaveableThing = { + data: item, + name: item.name, + origin: { + created: new Date().toISOString(), + type: SCThingOriginType.User, + }, + type: (typeof type === 'undefined') ? item.type : type, + uid: item.uid, + }; + // @TODO: Implementation for saving item into the backend (user's account) + return (await this.storageProvider.put>(this.getDataKey(item.uid), saveableItem)); + } + + /** + * Delete a data item + * + * @param uid Unique identifier of the saved data item + */ + async delete(uid: string): Promise { + return this.storageProvider.delete(this.getDataKey(uid)); + } + + /** + * Delete all the previously saved data items + */ + async deleteAll(): Promise { + const keys = Array.from((await this.getAll()).keys()); + return this.storageProvider.delete(...keys); + } + + /** + * Searches the backend using the provided query and returns response + * + * @param query - query to send to the backend + */ + async search(query: SCSearchQuery): Promise { + return (await this.client.search(query)); } }