feat(data): add basic methods of data provider

Related to #1
This commit is contained in:
Jovan Krunić
2019-02-01 15:13:13 +01:00
parent c819d15386
commit ffe05e4548
3 changed files with 351 additions and 4 deletions

View File

@@ -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,

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SCThings> = {
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<SCThing>;
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']);
});
});

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SCSaveableThing<SCThings>>;
/**
* Provides a thing from the backend
*/
async get(uid: string, scope: DataScope.Remote): Promise<SCThings | SCSaveableThing<SCThings>>;
/**
* Provides a thing from both local database and backend
*/
async get(uid: string): Promise<Map<DataScope, SCThings | SCSaveableThing<SCThings>>>;
/**
* 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<SCThings | SCSaveableThing<SCThings> | Map<DataScope, SCThings | SCSaveableThing<SCThings>>> {
if (scope === DataScope.Local) {
return this.storageProvider.get<SCSaveableThing<SCThings>>(this.getDataKey(uid));
}
if (scope === DataScope.Remote) {
return this.client.getThing(uid);
}
const map: Map<DataScope, SCThings | SCSaveableThing<SCThings>> = 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<Map<string, SCSaveableThing<SCThings>>> {
return this.storageProvider.search<SCSaveableThing<SCThings>>(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<SCSaveableThing<SCThings>> {
const saveableItem: SCSaveableThing<SCThings> = {
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<SCSaveableThing<SCThings>>(this.getDataKey(item.uid), saveableItem));
}
/**
* Delete a data item
*
* @param uid Unique identifier of the saved data item
*/
async delete(uid: string): Promise<void> {
return this.storageProvider.delete(this.getDataKey(uid));
}
/**
* Delete all the previously saved data items
*/
async deleteAll(): Promise<void> {
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<SCSearchResponse> {
return (await this.client.search(query));
}
}