diff --git a/src/core/initializers/initialize-databases.ts b/src/core/initializers/initialize-databases.ts index 4785c9455..1a0eb20ee 100644 --- a/src/core/initializers/initialize-databases.ts +++ b/src/core/initializers/initialize-databases.ts @@ -18,7 +18,11 @@ import { CoreCronDelegate } from '@services/cron'; import { CoreFilepool } from '@services/filepool'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreSites } from '@services/sites'; +import { CoreStorage } from '@services/storage'; +/** + * Init databases instances. + */ export default async function(): Promise { await Promise.all([ CoreApp.initializeDatabase(), @@ -27,5 +31,6 @@ export default async function(): Promise { CoreFilepool.initializeDatabase(), CoreLocalNotifications.initializeDatabase(), CoreSites.initializeDatabase(), + CoreStorage.initializeDatabase(), ]); } diff --git a/src/core/services/database/index.ts b/src/core/services/database/index.ts index ab8d9bd68..e55368ffe 100644 --- a/src/core/services/database/index.ts +++ b/src/core/services/database/index.ts @@ -18,7 +18,13 @@ import { CORE_SITE_SCHEMAS } from '@services/sites'; import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './filepool'; import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './sites'; import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './sync'; +import { SITE_SCHEMA as STORAGE_SITE_SCHEMA } from './storage'; +/** + * Give database providers. + * + * @returns database providers + */ export function getDatabaseProviders(): Provider[] { return [{ provide: CORE_SITE_SCHEMAS, @@ -26,6 +32,7 @@ export function getDatabaseProviders(): Provider[] { FILEPOOL_SITE_SCHEMA, SITES_SITE_SCHEMA, SYNC_SITE_SCHEMA, + STORAGE_SITE_SCHEMA, ], multi: true, }]; diff --git a/src/core/services/database/storage.ts b/src/core/services/database/storage.ts new file mode 100644 index 000000000..6744b673d --- /dev/null +++ b/src/core/services/database/storage.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { CoreAppSchema } from '@services/app'; +import { CoreSiteSchema } from '@services/sites'; + +export const TABLE_NAME = 'core_storage'; + +export const TABLE_SCHEMA: SQLiteDBTableSchema = { + name: TABLE_NAME, + columns: [ + { + name: 'key', + type: 'TEXT', + primaryKey: true, + }, + { + name: 'value', + type: 'TEXT', + notNull: true, + }, + ], +}; + +export const APP_SCHEMA: CoreAppSchema = { + name: 'CoreStorageService', + version: 1, + tables: [TABLE_SCHEMA], +}; + +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreStorageService', + version: 1, + tables: [TABLE_SCHEMA], +}; + +/** + * Storage table record type. + */ +export type CoreStorageRecord = { + key: string; + value: string; +}; diff --git a/src/core/services/storage.ts b/src/core/services/storage.ts new file mode 100644 index 000000000..a2fb8fc38 --- /dev/null +++ b/src/core/services/storage.ts @@ -0,0 +1,222 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Inject, Injectable, Optional } from '@angular/core'; + +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { CoreApp } from '@services/app'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { makeSingleton } from '@singletons'; +import { SQLiteDB } from '@classes/sqlitedb'; + +import { APP_SCHEMA, CoreStorageRecord, TABLE_NAME } from './database/storage'; +import { CoreSites } from './sites'; +import { CoreSite } from '@classes/site'; + +/** + * Service to store data using key-value pairs. + * + * The data can be scoped to a single site using CoreStorage.forSite(site), and it will be automatically cleared + * when the site is deleted. + * + * For tabular data, use CoreAppProvider.getDB() or CoreSite.getDb(). + */ +@Injectable({ providedIn: 'root' }) +export class CoreStorageService { + + table: AsyncInstance; + + constructor(@Optional() @Inject(null) lazyTableConstructor?: () => Promise) { + this.table = asyncInstance(lazyTableConstructor); + } + + /** + * Initialize database. + */ + async initializeDatabase(): Promise { + try { + await CoreApp.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + await this.initializeTable(CoreApp.getDB()); + } + + /** + * Initialize table. + * + * @param database Database. + */ + async initializeTable(database: SQLiteDB): Promise { + const table = await getStorageTable(database); + + this.table.setInstance(table); + } + + /** + * Get value. + * + * @param key Data key. + * @param defaultValue Value to return if the key wasn't found. + * @returns Data value. + */ + async get(key: string): Promise; + async get(key: string, defaultValue: T): Promise; + async get(key: string, defaultValue: T | null = null): Promise { + try { + const { value } = await this.table.getOneByPrimaryKey({ key }); + + return JSON.parse(value); + } catch (error) { + return defaultValue; + } + } + + /** + * Get value directly from the database, without using any optimizations.. + * + * @param key Data key. + * @param defaultValue Value to return if the key wasn't found. + * @returns Data value. + */ + async getFromDB(key: string): Promise; + async getFromDB(key: string, defaultValue: T): Promise; + async getFromDB(key: string, defaultValue: T | null = null): Promise { + try { + const db = CoreApp.getDB(); + const { value } = await db.getRecord(TABLE_NAME, { key }); + + return JSON.parse(value); + } catch (error) { + return defaultValue; + } + } + + /** + * Set value. + * + * @param key Data key. + * @param value Data value. + */ + async set(key: string, value: unknown): Promise { + await this.table.insert({ key, value: JSON.stringify(value) }); + } + + /** + * Check if value exists. + * + * @param key Data key. + * @returns Whether key exists or not. + */ + async has(key: string): Promise { + return this.table.hasAny({ key }); + } + + /** + * Remove value. + * + * @param key Data key. + */ + async remove(key: string): Promise { + await this.table.deleteByPrimaryKey({ key }); + } + + /** + * Get the core_storage table of the current site. + * + * @returns CoreStorageService instance with the core_storage table. + */ + forCurrentSite(): AsyncInstance> { + return asyncInstance(async () => { + const siteId = await CoreSites.getStoredCurrentSiteId(); + const site = await CoreSites.getSite(siteId); + + if (!(siteId in SERVICE_INSTANCES)) { + SERVICE_INSTANCES[siteId] = asyncInstance(async () => { + const instance = new CoreStorageService(); + await instance.initializeTable(site.getDb()); + + return instance; + }); + } + + return await SERVICE_INSTANCES[siteId].getInstance(); + }); + } + + /** + * Get the core_storage table for the provided site. + * + * @param site Site from which we will obtain the storage. + * @returns CoreStorageService instance with the core_storage table. + */ + forSite(site: CoreSite): AsyncInstance> { + const siteId = site.getId(); + + return asyncInstance(async () => { + if (!(siteId in SERVICE_INSTANCES)) { + const instance = new CoreStorageService(); + await instance.initializeTable(site.getDb()); + + SERVICE_INSTANCES[siteId] = asyncInstance(() => instance); + } + + return await SERVICE_INSTANCES[siteId].getInstance(); + }); + } + +} + +export const CoreStorage = makeSingleton(CoreStorageService); + +const SERVICE_INSTANCES: Record> = {}; +const TABLE_INSTANCES: WeakMap> = new WeakMap(); + +/** + * Helper function to get a storage table for the given database. + * + * @param database Database. + * @returns Storage table. + */ +function getStorageTable(database: SQLiteDB): Promise { + const existingTable = TABLE_INSTANCES.get(database); + + if (existingTable) { + return existingTable; + } + + const table = new Promise((resolve, reject) => { + const tableProxy = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + database, + TABLE_NAME, + ['key'], + ); + + tableProxy.initialize() + .then(() => resolve(tableProxy)) + .catch(reject); + }); + + TABLE_INSTANCES.set(database, table); + + return table; +} + +/** + * Storage table. + */ +type CoreStorageTable = CoreDatabaseTable;