/* * Copyright (C) 2022 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 {Injectable} from '@angular/core'; import {Client} from '@openstapps/api/lib/client'; import { SCFacet, SCIndexableThings, SCMultiSearchRequest, SCMultiSearchResponse, SCSearchRequest, SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingsField, SCThingType, SCSaveableThing, SCFeedbackRequest, SCFeedbackResponse, SCUuid, } from '@openstapps/core'; import {environment} from '../../../environments/environment'; import {StorageProvider} from '../storage/storage.provider'; import {StAppsWebHttpClient} from './stapps-web-http-client.provider'; import {chunk} from '../../_helpers/collections/chunk'; export enum DataScope { Local = 'local', Remote = 'remote', } interface CacheItem { data: Promise; timestamp: number; } /** * Generated class for the DataProvider provider. * * See https://angular.io/guide/dependency-injection for more info on providers * and Angular DI. */ @Injectable({ providedIn: 'root', }) export class DataProvider { /** * TODO */ get storagePrefix(): string { return this._storagePrefix; } /** * TODO */ set storagePrefix(storagePrefix) { this._storagePrefix = storagePrefix; } /** * TODO */ private _storagePrefix = 'stapps.data'; readonly cache: Record | undefined> = {}; private maxCacheAge = 3600; /** * Version of the app (used for the header in communication with the backend) */ appVersion = environment.backend_version; /** * Maximum number of sub-queries in a multi-query allowed by the backend */ backendQueriesLimit = 5; /** * TODO */ backendUrl = environment.backend_url; /** * TODO */ client: Client; /** * TODO */ storageProvider: StorageProvider; /** * Simplify creation of a value filter * * @param field Database field for apply the filter to * @param value Value to match with */ static createValueFilter(field: SCThingsField, value: string): SCSearchValueFilter { return { type: 'value', arguments: { field: field, value: value, }, }; } /** * Create a facet from data * * @param items Data to generate facet for * @param field Field for which to generate facet */ static facetForField(items: SCThing[], field: SCThingsField): SCFacet { const bucketMap = new Map(); const facet: SCFacet = {buckets: [], field: field}; for (const item of items) { const value = typeof bucketMap.get(item.type) === 'undefined' ? 1 : (bucketMap.get(item.type) as number) + 1; bucketMap.set(item.type, value); } for (const [key, value] of bucketMap.entries()) { facet.buckets.push({key: key, count: value}); } return facet; } /** * TODO * * @param stAppsWebHttpClient TODO * @param storageProvider TODO */ constructor(stAppsWebHttpClient: StAppsWebHttpClient, storageProvider: StorageProvider) { this.client = new Client(stAppsWebHttpClient, this.backendUrl, this.appVersion); this.storageProvider = storageProvider; } /** * Create savable thing from an indexable thing * * @param item An indexable to create savable thing from * @param type The type (falls back to the type of the indexable thing) */ static createSaveable(item: SCIndexableThings, type?: SCThingType): SCSaveableThing { return { data: item, name: item.name, origin: { created: new Date().toISOString(), type: SCThingOriginType.User, }, type: typeof type === 'undefined' ? item.type : type, uid: item.uid, }; } /** * 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 = [...(await this.getAll()).keys()]; return this.storageProvider.delete(...keys); } /** * Provides a savable 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> { if (scope === DataScope.Local) { return this.storageProvider.get(this.getDataKey(uid)); } if (scope === DataScope.Remote) { const timestamp = Date.now(); const cacheItem = this.cache[uid]; if (cacheItem && timestamp - cacheItem.timestamp < this.maxCacheAge) { return cacheItem.data; } const item = this.client.getThing(uid); this.cache[uid] = { data: item, timestamp: timestamp, }; return item; } 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); } /** * 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 information if something with an UID is saved as a data item * * @param uid Unique identifier of the saved data item */ async isSaved(uid: string): Promise { return this.storageProvider.has(this.getDataKey(uid)); } /** * Performs multiple searches at once and returns their responses * * @param query - query to send to the backend (auto-splits according to the backend limit) */ async multiSearch(query: SCMultiSearchRequest): Promise { // partition object into chunks, process those requests in parallel, then merge their responses again return Object.assign( {}, ...(await Promise.all( chunk(Object.entries(query), this.backendQueriesLimit).map(request => this.client.multiSearch(Object.fromEntries(request)), ), )), ); } /** * Save a data item * * @param item An item that needs to be saved */ async put(item: SCIndexableThings): Promise { return this.storageProvider.put( this.getDataKey(item.uid), DataProvider.createSaveable(item, item.type), ); } /** * Send a feedback message (request) * * @param feedback Feedback message to be sent to the backend */ async sendFeedback(feedback: SCFeedbackRequest) { return this.client.invokePlugin('feedback', undefined, feedback); } /** * Searches the backend using the provided query and returns response * * @param query - query to send to the backend */ async search(query: SCSearchRequest): Promise { return this.client.search(query); } }