From 80c2aef0d0e959d5c6a68d0f488b38fd9db59b8d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 17 Jan 2020 15:03:33 +0100 Subject: [PATCH] MOBILE-2853 core: Let app DB tables define a scheme to handle updates --- src/classes/site.ts | 2 +- .../emulator/providers/local-notifications.ts | 100 ++-- .../providers/pushnotifications.ts | 239 +++++----- src/core/sharedfiles/providers/sharedfiles.ts | 53 ++- src/providers/app.ts | 95 +++- src/providers/config.ts | 60 ++- src/providers/cron.ts | 52 ++- src/providers/filepool.ts | 227 ++++----- src/providers/local-notifications.ts | 163 ++++--- src/providers/sites.ts | 432 ++++++++++-------- 10 files changed, 842 insertions(+), 581 deletions(-) diff --git a/src/classes/site.ts b/src/classes/site.ts index dcf06ef29..18c8127ff 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -170,7 +170,7 @@ interface RequestQueueItem { /** * Class that represents a site (combination of site + user). * It will have all the site data and provide utility functions regarding a site. - * To add tables to the site's database, please use CoreSitesProvider.createTablesFromSchema. This will make sure that + * To add tables to the site's database, please use CoreSitesProvider.registerSiteSchema. This will make sure that * the tables are created in all the sites, not just the current one. */ export class CoreSite { diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index d3234bcd8..b5cbcd1eb 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -14,10 +14,10 @@ import { Injectable } from '@angular/core'; import { LocalNotifications, ILocalNotification, ILocalNotificationAction } from '@ionic-native/local-notifications'; -import { CoreAppProvider } from '@providers/app'; +import { CoreAppProvider, CoreAppSchema } from '@providers/app'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; import * as moment from 'moment'; @@ -43,41 +43,48 @@ export class LocalNotificationsMock extends LocalNotifications { // Variables for database. protected DESKTOP_NOTIFS_TABLE = 'desktop_local_notifications'; - protected tableSchema: SQLiteDBTableSchema = { - name: this.DESKTOP_NOTIFS_TABLE, - columns: [ + protected tableSchema: CoreAppSchema = { + name: 'LocalNotificationsMock', + version: 1, + tables: [ { - name: 'id', - type: 'INTEGER', - primaryKey: true + name: this.DESKTOP_NOTIFS_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'title', + type: 'TEXT' + }, + { + name: 'text', + type: 'TEXT' + }, + { + name: 'at', + type: 'INTEGER' + }, + { + name: 'data', + type: 'TEXT' + }, + { + name: 'triggered', + type: 'INTEGER' + } + ], }, - { - name: 'title', - type: 'TEXT' - }, - { - name: 'text', - type: 'TEXT' - }, - { - name: 'at', - type: 'INTEGER' - }, - { - name: 'data', - type: 'TEXT' - }, - { - name: 'triggered', - type: 'INTEGER' - } - ] + ], }; protected appDB: SQLiteDB; protected scheduled: { [i: number]: any } = {}; protected triggered: { [i: number]: any } = {}; protected observers: {[event: string]: Subject}; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected defaults = { actions : [], attachments : [], @@ -117,7 +124,9 @@ export class LocalNotificationsMock extends LocalNotifications { super(); this.appDB = appProvider.getDB(); - this.appDB.createTableFromSchema(this.tableSchema); + this.dbReady = appProvider.createTablesFromSchema(this.tableSchema).catch(() => { + // Ignore errors. + }); // Initialize observers. this.observers = { @@ -550,20 +559,21 @@ export class LocalNotificationsMock extends LocalNotifications { * * @return Promise resolved with the notifications. */ - protected getAllNotifications(): Promise { - return this.appDB.getAllRecords(this.DESKTOP_NOTIFS_TABLE).then((notifications) => { - notifications.forEach((notification) => { - notification.trigger = { - at: new Date(notification.at) - }; - notification.data = this.textUtils.parseJSON(notification.data); - notification.triggered = !!notification.triggered; + protected async getAllNotifications(): Promise { + await this.dbReady; - this.mergeWithDefaults(notification); - }); + const notifications = await this.appDB.getAllRecords(this.DESKTOP_NOTIFS_TABLE); + notifications.forEach((notification) => { + notification.trigger = { + at: new Date(notification.at) + }; + notification.data = this.textUtils.parseJSON(notification.data); + notification.triggered = !!notification.triggered; - return notifications; + this.mergeWithDefaults(notification); }); + + return notifications; } /** @@ -889,7 +899,9 @@ export class LocalNotificationsMock extends LocalNotifications { * @param id ID of the notification. * @return Promise resolved when done. */ - protected removeNotification(id: number): Promise { + protected async removeNotification(id: number): Promise { + await this.dbReady; + return this.appDB.deleteRecords(this.DESKTOP_NOTIFS_TABLE, { id: id }); } @@ -979,7 +991,9 @@ export class LocalNotificationsMock extends LocalNotifications { * @param triggered Whether the notification has been triggered. * @return Promise resolved when stored. */ - protected storeNotification(notification: ILocalNotification, triggered: boolean): Promise { + protected async storeNotification(notification: ILocalNotification, triggered: boolean): Promise { + await this.dbReady; + // Only store some of the properties. const entry = { id : notification.id, diff --git a/src/core/pushnotifications/providers/pushnotifications.ts b/src/core/pushnotifications/providers/pushnotifications.ts index 755300520..7540c08f1 100644 --- a/src/core/pushnotifications/providers/pushnotifications.ts +++ b/src/core/pushnotifications/providers/pushnotifications.ts @@ -18,7 +18,7 @@ import { Badge } from '@ionic-native/badge'; import { Push, PushObject, PushOptions } from '@ionic-native/push'; import { Device } from '@ionic-native/device'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from '@providers/app'; +import { CoreAppProvider, CoreAppSchema } from '@providers/app'; import { CoreInitDelegate } from '@providers/init'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; @@ -31,7 +31,7 @@ import { CoreConfigProvider } from '@providers/config'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../../../configconstants'; import { ILocalNotification } from '@ionic-native/local-notifications'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreSite } from '@classes/site'; import { CoreFilterProvider } from '@core/filter/providers/filter'; import { CoreFilterDelegate } from '@core/filter/providers/delegate'; @@ -84,54 +84,59 @@ export class CorePushNotificationsProvider { protected logger; protected pushID: string; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. static COMPONENT = 'CorePushNotificationsProvider'; // Variables for database. The name still contains the name "addon" for backwards compatibility. static BADGE_TABLE = 'addon_pushnotifications_badge'; static PENDING_UNREGISTER_TABLE = 'addon_pushnotifications_pending_unregister'; static REGISTERED_DEVICES_TABLE = 'addon_pushnotifications_registered_devices'; - protected appTablesSchema: SQLiteDBTableSchema[] = [ - { - name: CorePushNotificationsProvider.BADGE_TABLE, - columns: [ - { - name: 'siteid', - type: 'TEXT' - }, - { - name: 'addon', - type: 'TEXT' - }, - { - name: 'number', - type: 'INTEGER' - } - ], - primaryKeys: ['siteid', 'addon'] - }, - { - name: CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, - columns: [ - { - name: 'siteid', - type: 'TEXT', - primaryKey: true - }, - { - name: 'siteurl', - type: 'TEXT' - }, - { - name: 'token', - type: 'TEXT' - }, - { - name: 'info', - type: 'TEXT' - } - ] - } - ]; + protected appTablesSchema: CoreAppSchema = { + name: 'CorePushNotificationsProvider', + version: 1, + tables: [ + { + name: CorePushNotificationsProvider.BADGE_TABLE, + columns: [ + { + name: 'siteid', + type: 'TEXT' + }, + { + name: 'addon', + type: 'TEXT' + }, + { + name: 'number', + type: 'INTEGER' + } + ], + primaryKeys: ['siteid', 'addon'] + }, + { + name: CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, + columns: [ + { + name: 'siteid', + type: 'TEXT', + primaryKey: true + }, + { + name: 'siteurl', + type: 'TEXT' + }, + { + name: 'token', + type: 'TEXT' + }, + { + name: 'info', + type: 'TEXT' + } + ] + } + ] + }; protected siteSchema: CoreSiteSchema = { name: 'AddonPushNotificationsProvider', // The name still contains "Addon" for backwards compatibility. version: 1, @@ -182,7 +187,9 @@ export class CorePushNotificationsProvider { private filterProvider: CoreFilterProvider, private filterDelegate: CoreFilterDelegate) { this.logger = logger.getInstance('CorePushNotificationsProvider'); this.appDB = appProvider.getDB(); - this.appDB.createTablesFromSchema(this.appTablesSchema); + this.dbReady = appProvider.createTablesFromSchema(this.appTablesSchema).catch(() => { + // Ignore errors. + }); this.sitesProvider.registerSiteSchema(this.siteSchema); platform.ready().then(() => { @@ -211,10 +218,14 @@ export class CorePushNotificationsProvider { * @param siteId Site ID. * @return Resolved when done. */ - cleanSiteCounters(siteId: string): Promise { - return this.appDB.deleteRecords(CorePushNotificationsProvider.BADGE_TABLE, {siteid: siteId} ).finally(() => { + async cleanSiteCounters(siteId: string): Promise { + await this.dbReady; + + try { + await this.appDB.deleteRecords(CorePushNotificationsProvider.BADGE_TABLE, {siteid: siteId} ); + } finally { this.updateAppCounter(); - }); + } } /** @@ -532,11 +543,13 @@ export class CorePushNotificationsProvider { * @param site Site to unregister from. * @return Promise resolved when device is unregistered. */ - unregisterDeviceOnMoodle(site: CoreSite): Promise { + async unregisterDeviceOnMoodle(site: CoreSite): Promise { if (!site || !this.appProvider.isMobile()) { return Promise.reject(null); } + await this.dbReady; + this.logger.debug(`Unregister device on Moodle: '${site.id}'`); const data = { @@ -544,9 +557,11 @@ export class CorePushNotificationsProvider { uuid: this.device.uuid }; - return site.write('core_user_remove_user_device', data).then((response) => { + try { + const response = await site.write('core_user_remove_user_device', data); + if (!response || !response.removed) { - return Promise.reject(null); + throw null; } const promises = []; @@ -561,22 +576,19 @@ export class CorePushNotificationsProvider { return Promise.all(promises).catch(() => { // Ignore errors. }); - }).catch((error) => { - if (this.utils.isWebServiceError(error)) { - // It's a WebService error, can't unregister. - return Promise.reject(error); + } catch (error) { + if (!this.utils.isWebServiceError(error)) { + // Store the pending unregister so it's retried again later. + await this.appDB.insertRecord(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, { + siteid: site.id, + siteurl: site.getURL(), + token: site.getToken(), + info: JSON.stringify(site.getInfo()) + }); } - // Store the pending unregister so it's retried again later. - return this.appDB.insertRecord(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, { - siteid: site.id, - siteurl: site.getURL(), - token: site.getToken(), - info: JSON.stringify(site.getInfo()) - }).then(() => { - return Promise.reject(error); - }); - }); + throw error; + } } /** @@ -724,52 +736,53 @@ export class CorePushNotificationsProvider { * @param forceUnregister Whether to force unregister and register. * @return Promise resolved when device is registered. */ - registerDeviceOnMoodle(siteId?: string, forceUnregister?: boolean): Promise { + async registerDeviceOnMoodle(siteId?: string, forceUnregister?: boolean): Promise { this.logger.debug('Register device on Moodle.'); if (!this.canRegisterOnMoodle()) { return Promise.reject(null); } - const data = this.getRegisterData(); - let result, - site: CoreSite; + await this.dbReady; - return this.sitesProvider.getSite(siteId).then((s) => { - site = s; + const data = this.getRegisterData(); + let result; + + const site = await this.sitesProvider.getSite(siteId); + + try { if (forceUnregister) { - return {unregister: true, register: true}; + result = {unregister: true, register: true}; } else { // Check if the device is already registered. - return this.shouldRegister(data, site); + result = await this.shouldRegister(data, site); } - }).then((res) => { - result = res; if (result.unregister) { // Unregister the device first. - return this.unregisterDeviceOnMoodle(site).catch(() => { + await this.unregisterDeviceOnMoodle(site).catch(() => { // Ignore errors. }); } - }).then(() => { + if (result.register) { // Now register the device. - return site.write('core_user_add_user_device', this.utils.clone(data)).then((response) => { - // Insert the device in the local DB. - return site.getDb().insertRecord(CorePushNotificationsProvider.REGISTERED_DEVICES_TABLE, data) - .catch((error) => { - // Ignore errors. - }); - }); + await site.write('core_user_add_user_device', this.utils.clone(data)); + + // Insert the device in the local DB. + try { + await site.getDb().insertRecord(CorePushNotificationsProvider.REGISTERED_DEVICES_TABLE, data); + } catch (err) { + // Ignore errors. + } } - }).finally(() => { + } finally { // Remove pending unregisters for this site. - this.appDB.deleteRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: site.id}).catch(() => { + await this.appDB.deleteRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: site.id}).catch(() => { // Ignore errors. }); - }); + } } /** @@ -779,12 +792,16 @@ export class CorePushNotificationsProvider { * @param addon Registered addon name. If not defined it will store the site total. * @return Promise resolved with the stored badge counter for the addon or site or 0 if none. */ - protected getAddonBadge(siteId?: string, addon: string = 'site'): Promise { - return this.appDB.getRecord(CorePushNotificationsProvider.BADGE_TABLE, {siteid: siteId, addon: addon}).then((entry) => { - return (entry && entry.number) || 0; - }).catch(() => { + protected async getAddonBadge(siteId?: string, addon: string = 'site'): Promise { + await this.dbReady; + + try { + const entry = await this.appDB.getRecord(CorePushNotificationsProvider.BADGE_TABLE, {siteid: siteId, addon: addon}); + + return (entry && entry.number) || 0; + } catch (err) { return 0; - }); + } } /** @@ -793,30 +810,26 @@ export class CorePushNotificationsProvider { * @param siteId If defined, retry only for that site if needed. Otherwise, retry all pending unregisters. * @return Promise resolved when done. */ - retryUnregisters(siteId?: string): Promise { - let promise; + async retryUnregisters(siteId?: string): Promise { + await this.dbReady; + + let results; if (siteId) { // Check if the site has a pending unregister. - promise = this.appDB.getRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: siteId}); + results = await this.appDB.getRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE, {siteid: siteId}); } else { // Get all pending unregisters. - promise = this.appDB.getAllRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE); + results = await this.appDB.getAllRecords(CorePushNotificationsProvider.PENDING_UNREGISTER_TABLE); } - return promise.then((results) => { - const promises = []; + return Promise.all(results.map((result) => { + // Create a temporary site to unregister. + const tmpSite = this.sitesFactory.makeSite(result.siteid, result.siteurl, result.token, + this.textUtils.parseJSON(result.info, {})); - results.forEach((result) => { - // Create a temporary site to unregister. - const tmpSite = this.sitesFactory.makeSite(result.siteid, result.siteurl, result.token, - this.textUtils.parseJSON(result.info, {})); - - promises.push(this.unregisterDeviceOnMoodle(tmpSite)); - }); - - return Promise.all(promises); - }); + return this.unregisterDeviceOnMoodle(tmpSite); + })); } /** @@ -827,7 +840,9 @@ export class CorePushNotificationsProvider { * @param addon Registered addon name. If not defined it will store the site total. * @return Promise resolved with the stored badge counter for the addon or site. */ - protected saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { + protected async saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { + await this.dbReady; + siteId = siteId || this.sitesProvider.getCurrentSiteId(); const entry = { @@ -836,9 +851,9 @@ export class CorePushNotificationsProvider { number: value }; - return this.appDB.insertRecord(CorePushNotificationsProvider.BADGE_TABLE, entry).then(() => { - return value; - }); + await this.appDB.insertRecord(CorePushNotificationsProvider.BADGE_TABLE, entry); + + return value; } /** diff --git a/src/core/sharedfiles/providers/sharedfiles.ts b/src/core/sharedfiles/providers/sharedfiles.ts index f95c1dc29..91f971436 100644 --- a/src/core/sharedfiles/providers/sharedfiles.ts +++ b/src/core/sharedfiles/providers/sharedfiles.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from '@providers/app'; +import { CoreAppProvider, CoreAppSchema } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; @@ -21,7 +21,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { Md5 } from 'ts-md5/dist/md5'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Service to share files with the app. @@ -32,19 +32,26 @@ export class CoreSharedFilesProvider { // Variables for the database. protected SHARED_FILES_TABLE = 'shared_files'; - protected tableSchema: SQLiteDBTableSchema = { - name: this.SHARED_FILES_TABLE, - columns: [ + protected tableSchema: CoreAppSchema = { + name: 'CoreSharedFilesProvider', + version: 1, + tables: [ { - name: 'id', - type: 'TEXT', - primaryKey: true - } - ] + name: this.SHARED_FILES_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + }, + ], + }, + ], }; protected logger; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. constructor(logger: CoreLoggerProvider, private fileProvider: CoreFileProvider, appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider, private mimeUtils: CoreMimetypeUtilsProvider, @@ -52,7 +59,9 @@ export class CoreSharedFilesProvider { this.logger = logger.getInstance('CoreSharedFilesProvider'); this.appDB = appProvider.getDB(); - this.appDB.createTableFromSchema(this.tableSchema); + this.dbReady = appProvider.createTablesFromSchema(this.tableSchema).catch(() => { + // Ignore errors. + }); } /** @@ -189,7 +198,9 @@ export class CoreSharedFilesProvider { * @param fileId File ID. * @return Resolved if treated, rejected otherwise. */ - protected isFileTreated(fileId: string): Promise { + protected async isFileTreated(fileId: string): Promise { + await this.dbReady; + return this.appDB.getRecord(this.SHARED_FILES_TABLE, { id: fileId }); } @@ -199,12 +210,16 @@ export class CoreSharedFilesProvider { * @param fileId File ID. * @return Promise resolved when marked. */ - protected markAsTreated(fileId: string): Promise { - // Check if it's already marked. - return this.isFileTreated(fileId).catch(() => { + protected async markAsTreated(fileId: string): Promise { + await this.dbReady; + + try { + // Check if it's already marked. + await this.isFileTreated(fileId); + } catch (err) { // Doesn't exist, insert it. - return this.appDB.insertRecord(this.SHARED_FILES_TABLE, { id: fileId }); - }); + await this.appDB.insertRecord(this.SHARED_FILES_TABLE, { id: fileId }); + } } /** @@ -243,7 +258,9 @@ export class CoreSharedFilesProvider { * @param fileId File ID. * @return Resolved when unmarked. */ - protected unmarkAsTreated(fileId: string): Promise { + protected async unmarkAsTreated(fileId: string): Promise { + await this.dbReady; + return this.appDB.deleteRecords(this.SHARED_FILES_TABLE, { id: fileId }); } } diff --git a/src/providers/app.ts b/src/providers/app.ts index f2071b076..32745f52a 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -21,7 +21,7 @@ import { StatusBar } from '@ionic-native/status-bar'; import { CoreDbProvider } from './db'; import { CoreLoggerProvider } from './logger'; import { CoreEventsProvider } from './events'; -import { SQLiteDB } from '@classes/sqlitedb'; +import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { CoreConfigConstants } from '../configconstants'; /** @@ -49,6 +49,37 @@ export interface CoreRedirectData { timemodified?: number; } +/** + * App DB schema and migration function. + */ +export interface CoreAppSchema { + /** + * Name of the schema. + */ + name: string; + + /** + * Latest version of the schema (integer greater than 0). + */ + version: number; + + /** + * Tables to create when installing or upgrading the schema. + */ + tables?: SQLiteDBTableSchema[]; + + /** + * Migrates the schema to the latest version. + * + * Called when installing and upgrading the schema, after creating the defined tables. + * + * @param db The affected DB. + * @param oldVersion Old version of the schema or 0 if not installed. + * @return Promise resolved when done. + */ + migrate?(db: SQLiteDB, oldVersion: number): Promise; +} + /** * Factory to provide some global functionalities, like access to the global app database. * @description @@ -71,12 +102,33 @@ export class CoreAppProvider { protected mainMenuOpen: number; protected forceOffline = false; + // Variables for DB. + protected createVersionsTableReady: Promise; + protected SCHEMA_VERSIONS_TABLE = 'schema_versions'; + protected versionsTableSchema: SQLiteDBTableSchema = { + name: this.SCHEMA_VERSIONS_TABLE, + columns: [ + { + name: 'name', + type: 'TEXT', + primaryKey: true, + }, + { + name: 'version', + type: 'INTEGER' + } + ] + }; + constructor(dbProvider: CoreDbProvider, private platform: Platform, private keyboard: Keyboard, private appCtrl: App, private network: Network, logger: CoreLoggerProvider, private events: CoreEventsProvider, zone: NgZone, private menuCtrl: MenuController, private statusBar: StatusBar) { this.logger = logger.getInstance('CoreAppProvider'); this.db = dbProvider.getDB(this.DBNAME); + // Create the schema versions table. + this.createVersionsTableReady = this.db.createTableFromSchema(this.versionsTableSchema); + this.keyboard.onKeyboardShow().subscribe((data) => { // Execute the callback in the Angular zone, so change detection doesn't stop working. zone.run(() => { @@ -133,6 +185,47 @@ export class CoreAppProvider { } } + /** + * Install and upgrade a certain schema. + * + * @param schema The schema to create. + * @return Promise resolved when done. + */ + async createTablesFromSchema(schema: CoreAppSchema): Promise { + this.logger.debug(`Apply schema to app DB: ${schema.name}`); + + let oldVersion; + + try { + // Wait for the schema versions table to be created. + await this.createVersionsTableReady; + + // Fetch installed version of the schema. + const entry = await this.db.getRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name}); + oldVersion = entry.version; + } catch (error) { + // No installed version yet. + oldVersion = 0; + } + + if (oldVersion >= schema.version) { + // Version already installed, nothing else to do. + return; + } + + this.logger.debug(`Migrating schema '${schema.name}' of app DB from version ${oldVersion} to ${schema.version}`); + + if (schema.tables) { + await this.db.createTablesFromSchema(schema.tables); + } + if (schema.migrate) { + await schema.migrate(this.db, oldVersion); + } + + // Set installed version. + await this.db.insertRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name, version: schema.version}); + } + /** * Get the application global database. * diff --git a/src/providers/config.ts b/src/providers/config.ts index 38039bff6..d74b0fbf7 100644 --- a/src/providers/config.ts +++ b/src/providers/config.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from './app'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { CoreAppProvider, CoreAppSchema } from './app'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Factory to provide access to dynamic and permanent config and settings. @@ -24,24 +24,34 @@ import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; export class CoreConfigProvider { protected appDB: SQLiteDB; protected TABLE_NAME = 'core_config'; - protected tableSchema: SQLiteDBTableSchema = { - name: this.TABLE_NAME, - columns: [ + protected tableSchema: CoreAppSchema = { + name: 'CoreConfigProvider', + version: 1, + tables: [ { - name: 'name', - type: 'TEXT', - unique: true, - notNull: true + name: this.TABLE_NAME, + columns: [ + { + name: 'name', + type: 'TEXT', + unique: true, + notNull: true + }, + { + name: 'value' + } + ] }, - { - name: 'value' - } - ] + ], }; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. + constructor(appProvider: CoreAppProvider) { this.appDB = appProvider.getDB(); - this.appDB.createTableFromSchema(this.tableSchema); + this.dbReady = appProvider.createTablesFromSchema(this.tableSchema).catch(() => { + // Ignore errors. + }); } /** @@ -50,7 +60,9 @@ export class CoreConfigProvider { * @param name The config name. * @return Promise resolved when done. */ - delete(name: string): Promise { + async delete(name: string): Promise { + await this.dbReady; + return this.appDB.deleteRecords(this.TABLE_NAME, { name: name }); } @@ -61,16 +73,20 @@ export class CoreConfigProvider { * @param defaultValue Default value to use if the entry is not found. * @return Resolves upon success along with the config data. Reject on failure. */ - get(name: string, defaultValue?: any): Promise { - return this.appDB.getRecord(this.TABLE_NAME, { name: name }).then((entry) => { + async get(name: string, defaultValue?: any): Promise { + await this.dbReady; + + try { + const entry = await this.appDB.getRecord(this.TABLE_NAME, { name: name }); + return entry.value; - }).catch((error) => { + } catch (error) { if (typeof defaultValue != 'undefined') { return defaultValue; } - return Promise.reject(error); - }); + throw error; + } } /** @@ -80,7 +96,9 @@ export class CoreConfigProvider { * @param value The config value. Can only store number or strings. * @return Promise resolved when done. */ - set(name: string, value: number | string): Promise { + async set(name: string, value: number | string): Promise { + await this.dbReady; + return this.appDB.insertRecord(this.TABLE_NAME, { name: name, value: value }); } } diff --git a/src/providers/cron.ts b/src/providers/cron.ts index dbf9ce3e1..2d5930041 100644 --- a/src/providers/cron.ts +++ b/src/providers/cron.ts @@ -14,12 +14,12 @@ import { Injectable, NgZone } from '@angular/core'; import { Network } from '@ionic-native/network'; -import { CoreAppProvider } from './app'; +import { CoreAppProvider, CoreAppSchema } from './app'; import { CoreConfigProvider } from './config'; import { CoreLoggerProvider } from './logger'; import { CoreUtilsProvider } from './utils/utils'; import { CoreConstants } from '@core/constants'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Interface that all cron handlers must implement. @@ -92,23 +92,30 @@ export class CoreCronDelegate { // Variables for database. protected CRON_TABLE = 'cron'; - protected tableSchema: SQLiteDBTableSchema = { - name: this.CRON_TABLE, - columns: [ + protected tableSchema: CoreAppSchema = { + name: 'CoreCronDelegate', + version: 1, + tables: [ { - name: 'id', - type: 'TEXT', - primaryKey: true + name: this.CRON_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + }, + { + name: 'value', + type: 'INTEGER' + } + ] }, - { - name: 'value', - type: 'INTEGER' - } - ] + ], }; protected logger; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected handlers: { [s: string]: CoreCronHandler } = {}; protected queuePromise = Promise.resolve(); @@ -117,7 +124,9 @@ export class CoreCronDelegate { this.logger = logger.getInstance('CoreCronDelegate'); this.appDB = this.appProvider.getDB(); - this.appDB.createTableFromSchema(this.tableSchema); + this.dbReady = appProvider.createTablesFromSchema(this.tableSchema).catch(() => { + // Ignore errors. + }); // When the app is re-connected, start network handlers that were stopped. network.onConnect().subscribe(() => { @@ -306,16 +315,19 @@ export class CoreCronDelegate { * @param name Handler's name. * @return Promise resolved with the handler's last execution time. */ - protected getHandlerLastExecutionTime(name: string): Promise { + protected async getHandlerLastExecutionTime(name: string): Promise { + await this.dbReady; + const id = this.getHandlerLastExecutionId(name); - return this.appDB.getRecord(this.CRON_TABLE, { id: id }).then((entry) => { + try { + const entry = await this.appDB.getRecord(this.CRON_TABLE, { id: id }); const time = parseInt(entry.value, 10); return isNaN(time) ? 0 : time; - }).catch(() => { + } catch (err) { return 0; // Not set, return 0. - }); + } } /** @@ -471,7 +483,9 @@ export class CoreCronDelegate { * @param time Time to set. * @return Promise resolved when the execution time is saved. */ - protected setHandlerLastExecutionTime(name: string, time: number): Promise { + protected async setHandlerLastExecutionTime(name: string, time: number): Promise { + await this.dbReady; + const id = this.getHandlerLastExecutionId(name), entry = { id: id, diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index 9e8fa5790..8b7db5467 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -14,7 +14,7 @@ import { Injectable, NgZone } from '@angular/core'; import { Network } from '@ionic-native/network'; -import { CoreAppProvider } from './app'; +import { CoreAppProvider, CoreAppSchema } from './app'; import { CoreEventsProvider } from './events'; import { CoreFileProvider } from './file'; import { CoreInitDelegate } from './init'; @@ -28,7 +28,7 @@ import { CoreTextUtilsProvider } from './utils/text'; import { CoreTimeUtilsProvider } from './utils/time'; import { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; import { Md5 } from 'ts-md5/dist/md5'; @@ -224,58 +224,62 @@ export class CoreFilepoolProvider { protected FILES_TABLE = 'filepool_files'; // Downloaded files. protected LINKS_TABLE = 'filepool_files_links'; // Links between downloaded files and components. protected PACKAGES_TABLE = 'filepool_packages'; // Downloaded packages (sets of files). - protected appTablesSchema: SQLiteDBTableSchema[] = [ - { - name: this.QUEUE_TABLE, - columns: [ - { - name: 'siteId', - type: 'TEXT' - }, - { - name: 'fileId', - type: 'TEXT' - }, - { - name: 'added', - type: 'INTEGER' - }, - { - name: 'priority', - type: 'INTEGER' - }, - { - name: 'url', - type: 'TEXT' - }, - { - name: 'revision', - type: 'INTEGER' - }, - { - name: 'timemodified', - type: 'INTEGER' - }, - { - name: 'isexternalfile', - type: 'INTEGER' - }, - { - name: 'repositorytype', - type: 'TEXT' - }, - { - name: 'path', - type: 'TEXT' - }, - { - name: 'links', - type: 'TEXT' - } - ], - primaryKeys: ['siteId', 'fileId'] - } - ]; + protected appTablesSchema: CoreAppSchema = { + name: 'CoreFilepoolProvider', + version: 1, + tables: [ + { + name: this.QUEUE_TABLE, + columns: [ + { + name: 'siteId', + type: 'TEXT' + }, + { + name: 'fileId', + type: 'TEXT' + }, + { + name: 'added', + type: 'INTEGER' + }, + { + name: 'priority', + type: 'INTEGER' + }, + { + name: 'url', + type: 'TEXT' + }, + { + name: 'revision', + type: 'INTEGER' + }, + { + name: 'timemodified', + type: 'INTEGER' + }, + { + name: 'isexternalfile', + type: 'INTEGER' + }, + { + name: 'repositorytype', + type: 'TEXT' + }, + { + name: 'path', + type: 'TEXT' + }, + { + name: 'links', + type: 'TEXT' + } + ], + primaryKeys: ['siteId', 'fileId'] + } + ] + }; protected siteSchema: CoreSiteSchema = { name: 'CoreFilepoolProvider', version: 1, @@ -392,6 +396,7 @@ export class CoreFilepoolProvider { protected logger; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected tokenRegex = new RegExp('(\\?|&)token=([A-Za-z0-9]*)'); protected queueState: string; protected urlAttributes = [ @@ -415,7 +420,9 @@ export class CoreFilepoolProvider { this.logger = logger.getInstance('CoreFilepoolProvider'); this.appDB = this.appProvider.getDB(); - this.appDB.createTablesFromSchema(this.appTablesSchema); + this.dbReady = appProvider.createTablesFromSchema(this.appTablesSchema).catch(() => { + // Ignore errors. + }); this.sitesProvider.registerSiteSchema(this.siteSchema); @@ -567,11 +574,13 @@ export class CoreFilepoolProvider { * @param link The link to add for the file. * @return Promise resolved when the file is downloaded. */ - protected addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, timemodified: number, - filePath: string, onProgress?: (event: any) => any, options: any = {}, link?: any): Promise { + protected async addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, + timemodified: number, filePath: string, onProgress?: (event: any) => any, options: any = {}, link?: any): Promise { + await this.dbReady; + this.logger.debug(`Adding ${fileId} to the queue`); - return this.appDB.insertRecord(this.QUEUE_TABLE, { + await this.appDB.insertRecord(this.QUEUE_TABLE, { siteId: siteId, fileId: fileId, url: url, @@ -583,13 +592,13 @@ export class CoreFilepoolProvider { repositorytype: options.repositorytype, links: JSON.stringify(link ? [link] : []), added: Date.now() - }).then(() => { - // Check if the queue is running. - this.checkQueueProcessing(); - this.notifyFileDownloading(siteId, fileId); - - return this.getQueuePromise(siteId, fileId, true, onProgress); }); + + // Check if the queue is running. + this.checkQueueProcessing(); + this.notifyFileDownloading(siteId, fileId); + + return this.getQueuePromise(siteId, fileId, true, onProgress); } /** @@ -608,9 +617,11 @@ export class CoreFilepoolProvider { * @param alreadyFixed Whether the URL has already been fixed. * @return Resolved on success. */ - addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, timemodified: number = 0, - filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, revision?: number, - alreadyFixed?: boolean): Promise { + async addToQueueByUrl(siteId: string, fileUrl: string, component?: string, componentId?: string | number, + timemodified: number = 0, filePath?: string, onProgress?: (event: any) => any, priority: number = 0, options: any = {}, + revision?: number, alreadyFixed?: boolean): Promise { + await this.dbReady; + let fileId, link, queueDeferred; @@ -2309,16 +2320,17 @@ export class CoreFilepoolProvider { * @param fileUrl The file URL. * @return Resolved with file object from DB on success, rejected otherwise. */ - protected hasFileInQueue(siteId: string, fileId: string): Promise { - return this.appDB.getRecord(this.QUEUE_TABLE, { siteId: siteId, fileId: fileId }).then((entry) => { - if (typeof entry === 'undefined') { - return Promise.reject(null); - } - // Convert the links to an object. - entry.links = this.textUtils.parseJSON(entry.links, []); + protected async hasFileInQueue(siteId: string, fileId: string): Promise { + await this.dbReady; - return entry; - }); + const entry = await this.appDB.getRecord(this.QUEUE_TABLE, { siteId: siteId, fileId: fileId }); + if (typeof entry === 'undefined') { + throw null; + } + // Convert the links to an object. + entry.links = this.textUtils.parseJSON(entry.links, []); + + return entry; } /** @@ -2546,19 +2558,25 @@ export class CoreFilepoolProvider { * * @return Resolved on success. Rejected on failure. */ - protected processImportantQueueItem(): Promise { - return this.appDB.getRecords(this.QUEUE_TABLE, undefined, 'priority DESC, added ASC', undefined, 0, 1).then((items) => { - const item = items.pop(); - if (!item) { - return Promise.reject(this.ERR_QUEUE_IS_EMPTY); - } - // Convert the links to an object. - item.links = this.textUtils.parseJSON(item.links, []); + protected async processImportantQueueItem(): Promise { + await this.dbReady; - return this.processQueueItem(item); - }, () => { - return Promise.reject(this.ERR_QUEUE_IS_EMPTY); - }); + let items; + + try { + items = await this.appDB.getRecords(this.QUEUE_TABLE, undefined, 'priority DESC, added ASC', undefined, 0, 1); + } catch (err) { + throw this.ERR_QUEUE_IS_EMPTY; + } + + const item = items.pop(); + if (!item) { + throw this.ERR_QUEUE_IS_EMPTY; + } + // Convert the links to an object. + item.links = this.textUtils.parseJSON(item.links, []); + + return this.processQueueItem(item); } /** @@ -2685,7 +2703,9 @@ export class CoreFilepoolProvider { * @param fileId The file ID. * @return Resolved on success. Rejected on failure. It is advised to silently ignore failures. */ - protected removeFromQueue(siteId: string, fileId: string): Promise { + protected async removeFromQueue(siteId: string, fileId: string): Promise { + await this.dbReady; + return this.appDB.deleteRecords(this.QUEUE_TABLE, { siteId: siteId, fileId: fileId }); } @@ -3003,27 +3023,26 @@ export class CoreFilepoolProvider { * * @return Promise resolved when done. */ - treatExtensionInQueue(): Promise { + async treatExtensionInQueue(): Promise { + await this.dbReady; + this.logger.debug('Treat extensions in queue'); - return this.appDB.getAllRecords(this.QUEUE_TABLE).then((entries) => { - const promises = []; - entries.forEach((entry) => { + const entries = await this.appDB.getAllRecords(this.QUEUE_TABLE); - // For files in the queue, we only need to remove the extension from the fileId. - // After downloading, additional info will be added. - const fileId = entry.fileId; - entry.fileId = this.mimeUtils.removeExtension(fileId); + return Promise.all(entries.map((entry) => { - if (fileId == entry.fileId) { - return; - } + // For files in the queue, we only need to remove the extension from the fileId. + // After downloading, additional info will be added. + const fileId = entry.fileId; + entry.fileId = this.mimeUtils.removeExtension(fileId); - promises.push(this.appDB.updateRecords(this.QUEUE_TABLE, { fileId: entry.fileId }, { fileId: fileId })); - }); + if (fileId == entry.fileId) { + return; + } - return Promise.all(promises); - }); + return this.appDB.updateRecords(this.QUEUE_TABLE, { fileId: entry.fileId }, { fileId: fileId }); + })); } /** diff --git a/src/providers/local-notifications.ts b/src/providers/local-notifications.ts index 3967f8cfe..ab564c322 100644 --- a/src/providers/local-notifications.ts +++ b/src/providers/local-notifications.ts @@ -17,13 +17,13 @@ import { Platform, Alert, AlertController } from 'ionic-angular'; import { LocalNotifications, ILocalNotification } from '@ionic-native/local-notifications'; import { Push } from '@ionic-native/push'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from './app'; +import { CoreAppProvider, CoreAppSchema } from './app'; import { CoreConfigProvider } from './config'; import { CoreEventsProvider } from './events'; import { CoreLoggerProvider } from './logger'; import { CoreTextUtilsProvider } from './utils/text'; import { CoreUtilsProvider } from './utils/utils'; -import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConstants } from '@core/constants'; import { CoreConfigConstants } from '../configconstants'; import { Subject, Subscription } from 'rxjs'; @@ -40,56 +40,61 @@ export class CoreLocalNotificationsProvider { protected SITES_TABLE = 'notification_sites'; // Store to asigne unique codes to each site. protected COMPONENTS_TABLE = 'notification_components'; // Store to asigne unique codes to each component. protected TRIGGERED_TABLE = 'notifications_triggered'; // Store to prevent re-triggering notifications. - protected tablesSchema: SQLiteDBTableSchema[] = [ - { - name: this.SITES_TABLE, - columns: [ - { - name: 'id', - type: 'TEXT', - primaryKey: true - }, - { - name: 'code', - type: 'INTEGER', - notNull: true - } - ] - }, - { - name: this.COMPONENTS_TABLE, - columns: [ - { - name: 'id', - type: 'TEXT', - primaryKey: true - }, - { - name: 'code', - type: 'INTEGER', - notNull: true - } - ] - }, - { - name: this.TRIGGERED_TABLE, - columns: [ - { - name: 'id', - type: 'INTEGER', - primaryKey: true - }, - { - name: 'at', - type: 'INTEGER', - notNull: true - } - ] - } - ]; + protected tablesSchema: CoreAppSchema = { + name: 'CoreLocalNotificationsProvider', + version: 1, + tables: [ + { + name: this.SITES_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + }, + { + name: 'code', + type: 'INTEGER', + notNull: true + } + ] + }, + { + name: this.COMPONENTS_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + }, + { + name: 'code', + type: 'INTEGER', + notNull: true + } + ] + }, + { + name: this.TRIGGERED_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'at', + type: 'INTEGER', + notNull: true + } + ] + } + ] + }; protected logger; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected codes: { [s: string]: number } = {}; protected codeRequestsQueue = {}; protected observables = {}; @@ -114,7 +119,9 @@ export class CoreLocalNotificationsProvider { this.logger = logger.getInstance('CoreLocalNotificationsProvider'); this.appDB = appProvider.getDB(); - this.appDB.createTablesFromSchema(this.tablesSchema); + this.dbReady = appProvider.createTablesFromSchema(this.tablesSchema).catch(() => { + // Ignore errors. + }); platform.ready().then(() => { // Listen to events. @@ -242,34 +249,35 @@ export class CoreLocalNotificationsProvider { * @param id ID of the element to get its code. * @return Promise resolved when the code is retrieved. */ - protected getCode(table: string, id: string): Promise { + protected async getCode(table: string, id: string): Promise { + await this.dbReady; + const key = table + '#' + id; // Check if the code is already in memory. if (typeof this.codes[key] != 'undefined') { - return Promise.resolve(this.codes[key]); + return this.codes[key]; } - // Check if we already have a code stored for that ID. - return this.appDB.getRecord(table, { id: id }).then((entry) => { + try { + // Check if we already have a code stored for that ID. + const entry = await this.appDB.getRecord(table, { id: id }); this.codes[key] = entry.code; return entry.code; - }).catch(() => { + } catch (err) { // No code stored for that ID. Create a new code for it. - return this.appDB.getRecords(table, undefined, 'code DESC').then((entries) => { - let newCode = 0; - if (entries.length > 0) { - newCode = entries[0].code + 1; - } + const entries = await this.appDB.getRecords(table, undefined, 'code DESC'); + let newCode = 0; + if (entries.length > 0) { + newCode = entries[0].code + 1; + } - return this.appDB.insertRecord(table, { id: id, code: newCode }).then(() => { - this.codes[key] = newCode; + await this.appDB.insertRecord(table, { id: id, code: newCode }); + this.codes[key] = newCode; - return newCode; - }); - }); - }); + return newCode; + } } /** @@ -352,8 +360,11 @@ export class CoreLocalNotificationsProvider { * @param notification Notification to check. * @return Promise resolved with a boolean indicating if promise is triggered (true) or not. */ - isTriggered(notification: ILocalNotification): Promise { - return this.appDB.getRecord(this.TRIGGERED_TABLE, { id: notification.id }).then((stored) => { + async isTriggered(notification: ILocalNotification): Promise { + await this.dbReady; + + try { + const stored = await this.appDB.getRecord(this.TRIGGERED_TABLE, { id: notification.id }); let triggered = (notification.trigger && notification.trigger.at) || 0; if (typeof triggered != 'number') { @@ -361,9 +372,9 @@ export class CoreLocalNotificationsProvider { } return stored.at === triggered; - }).catch(() => { + } catch (err) { return this.localNotifications.isTriggered(notification.id); - }); + } } /** @@ -477,7 +488,9 @@ export class CoreLocalNotificationsProvider { * @param id Notification ID. * @return Promise resolved when it is removed. */ - removeTriggered(id: number): Promise { + async removeTriggered(id: number): Promise { + await this.dbReady; + return this.appDB.deleteRecords(this.TRIGGERED_TABLE, { id: id }); } @@ -714,7 +727,9 @@ export class CoreLocalNotificationsProvider { * @param notification Triggered notification. * @return Promise resolved when stored, rejected otherwise. */ - trigger(notification: ILocalNotification): Promise { + async trigger(notification: ILocalNotification): Promise { + await this.dbReady; + const entry = { id: notification.id, at: notification.trigger && notification.trigger.at ? notification.trigger.at : Date.now() @@ -730,7 +745,9 @@ export class CoreLocalNotificationsProvider { * @param newName The new name. * @return Promise resolved when done. */ - updateComponentName(oldName: string, newName: string): Promise { + async updateComponentName(oldName: string, newName: string): Promise { + await this.dbReady; + const oldId = this.COMPONENTS_TABLE + '#' + oldName, newId = this.COMPONENTS_TABLE + '#' + newName; diff --git a/src/providers/sites.ts b/src/providers/sites.ts index adcd01681..b35aca68e 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -15,7 +15,7 @@ import { Injectable, Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; -import { CoreAppProvider } from './app'; +import { CoreAppProvider, CoreAppSchema } from './app'; import { CoreEventsProvider } from './events'; import { CoreLoggerProvider } from './logger'; import { CoreSitesFactoryProvider } from './sites-factory'; @@ -170,7 +170,7 @@ export const enum CoreSitesReadingStrategy { * their own database tables. Example: * * constructor(sitesProvider: CoreSitesProvider) { - * this.sitesProvider.createTableFromSchema(this.tableSchema); + * this.sitesProvider.registerSiteSchema(this.siteSchema); * * This provider will automatically create the tables in the databases of all the instantiated sites, and also to the * databases of sites instantiated from now on. @@ -181,59 +181,63 @@ export class CoreSitesProvider { protected SITES_TABLE = 'sites'; protected CURRENT_SITE_TABLE = 'current_site'; protected SCHEMA_VERSIONS_TABLE = 'schema_versions'; - protected appTablesSchema: SQLiteDBTableSchema[] = [ - { - name: this.SITES_TABLE, - columns: [ - { - name: 'id', - type: 'TEXT', - primaryKey: true - }, - { - name: 'siteUrl', - type: 'TEXT', - notNull: true - }, - { - name: 'token', - type: 'TEXT' - }, - { - name: 'info', - type: 'TEXT' - }, - { - name: 'privateToken', - type: 'TEXT' - }, - { - name: 'config', - type: 'TEXT' - }, - { - name: 'loggedOut', - type: 'INTEGER' - } - ] - }, - { - name: this.CURRENT_SITE_TABLE, - columns: [ - { - name: 'id', - type: 'INTEGER', - primaryKey: true - }, - { - name: 'siteId', - type: 'TEXT', - notNull: true, - unique: true - } - ] - } - ]; + protected appTablesSchema: CoreAppSchema = { + name: 'CoreSitesProvider', + version: 1, + tables: [ + { + name: this.SITES_TABLE, + columns: [ + { + name: 'id', + type: 'TEXT', + primaryKey: true + }, + { + name: 'siteUrl', + type: 'TEXT', + notNull: true + }, + { + name: 'token', + type: 'TEXT' + }, + { + name: 'info', + type: 'TEXT' + }, + { + name: 'privateToken', + type: 'TEXT' + }, + { + name: 'config', + type: 'TEXT' + }, + { + name: 'loggedOut', + type: 'INTEGER' + } + ] + }, + { + name: this.CURRENT_SITE_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true + }, + { + name: 'siteId', + type: 'TEXT', + notNull: true, + unique: true + } + ] + } + ] + }; // Constants to validate a site version. protected WORKPLACE_APP = 3; @@ -249,6 +253,7 @@ export class CoreSitesProvider { protected currentSite: CoreSite; protected sites: { [s: string]: CoreSite } = {}; protected appDB: SQLiteDB; + protected dbReady: Promise; // Promise resolved when the app DB is initialized. protected siteSchemasMigration: { [siteId: string]: Promise } = {}; // Schemas for site tables. Other providers can add schemas in here. @@ -323,7 +328,9 @@ export class CoreSitesProvider { this.logger = logger.getInstance('CoreSitesProvider'); this.appDB = appProvider.getDB(); - this.appDB.createTablesFromSchema(this.appTablesSchema); + this.dbReady = appProvider.createTablesFromSchema(this.appTablesSchema).catch(() => { + // Ignore errors. + }); this.registerSiteSchema(this.siteSchema); } @@ -859,7 +866,9 @@ export class CoreSitesProvider { * @param config Site config (from tool_mobile_get_config). * @return Promise resolved when done. */ - addSite(id: string, siteUrl: string, token: string, info: any, privateToken: string = '', config?: any): Promise { + async addSite(id: string, siteUrl: string, token: string, info: any, privateToken: string = '', config?: any): Promise { + await this.dbReady; + const entry = { id: id, siteUrl: siteUrl, @@ -1070,29 +1079,32 @@ export class CoreSitesProvider { * @param siteId ID of the site to delete. * @return Promise to be resolved when the site is deleted. */ - deleteSite(siteId: string): Promise { + async deleteSite(siteId: string): Promise { + await this.dbReady; + this.logger.debug(`Delete site ${siteId}`); if (typeof this.currentSite != 'undefined' && this.currentSite.id == siteId) { this.logout(); } - return this.getSite(siteId).then((site: CoreSite) => { - return site.deleteDB().then(() => { - // Site DB deleted, now delete the app from the list of sites. - delete this.sites[siteId]; + const site = await this.getSite(siteId); - return this.appDB.deleteRecords(this.SITES_TABLE, { id: siteId }).then(() => { - // Site deleted from sites list, now delete the folder. - return site.deleteFolder(); - }, () => { - // DB remove shouldn't fail, but we'll go ahead even if it does. - return site.deleteFolder(); - }).then(() => { - this.eventsProvider.trigger(CoreEventsProvider.SITE_DELETED, site, siteId); - }); - }); - }); + await site.deleteDB(); + + // Site DB deleted, now delete the app from the list of sites. + delete this.sites[siteId]; + + try { + await this.appDB.deleteRecords(this.SITES_TABLE, { id: siteId }); + } catch (err) { + // DB remove shouldn't fail, but we'll go ahead even if it does. + } + + // Site deleted from sites list, now delete the folder. + await site.deleteFolder(); + + this.eventsProvider.trigger(CoreEventsProvider.SITE_DELETED, site, siteId); } /** @@ -1100,10 +1112,12 @@ export class CoreSitesProvider { * * @return Promise resolved with true if there are sites and false if there aren't. */ - hasSites(): Promise { - return this.appDB.countRecords(this.SITES_TABLE).then((count) => { - return count > 0; - }); + async hasSites(): Promise { + await this.dbReady; + + const count = await this.appDB.countRecords(this.SITES_TABLE); + + return count > 0; } /** @@ -1112,18 +1126,24 @@ export class CoreSitesProvider { * @param siteId The site ID. If not defined, current site (if available). * @return Promise resolved with the site. */ - getSite(siteId?: string): Promise { + async getSite(siteId?: string): Promise { + await this.dbReady; + if (!siteId) { - return this.currentSite ? Promise.resolve(this.currentSite) : Promise.reject(null); + if (this.currentSite) { + return this.currentSite; + } + + throw null; } else if (this.currentSite && this.currentSite.getId() == siteId) { - return Promise.resolve(this.currentSite); + return this.currentSite; } else if (typeof this.sites[siteId] != 'undefined') { - return Promise.resolve(this.sites[siteId]); + return this.sites[siteId]; } else { // Retrieve and create the site. - return this.appDB.getRecord(this.SITES_TABLE, { id: siteId }).then((data) => { - return this.makeSiteFromSiteListEntry(data); - }); + const data = await this.appDB.getRecord(this.SITES_TABLE, { id: siteId }); + + return this.makeSiteFromSiteListEntry(data); } } @@ -1199,27 +1219,29 @@ export class CoreSitesProvider { * @param ids IDs of the sites to get. If not defined, return all sites. * @return Promise resolved when the sites are retrieved. */ - getSites(ids?: string[]): Promise { - return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => { - const formattedSites = []; - sites.forEach((site) => { - if (!ids || ids.indexOf(site.id) > -1) { - // Parse info. - const siteInfo = site.info ? this.textUtils.parseJSON(site.info) : site.info, - basicInfo: CoreSiteBasicInfo = { - id: site.id, - siteUrl: site.siteUrl, - fullName: siteInfo && siteInfo.fullname, - siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename, - avatar: siteInfo && siteInfo.userpictureurl, - siteHomeId: siteInfo && siteInfo.siteid || 1 - }; - formattedSites.push(basicInfo); - } - }); + async getSites(ids?: string[]): Promise { + await this.dbReady; - return formattedSites; + const sites = await this.appDB.getAllRecords(this.SITES_TABLE); + + const formattedSites = []; + sites.forEach((site) => { + if (!ids || ids.indexOf(site.id) > -1) { + // Parse info. + const siteInfo = site.info ? this.textUtils.parseJSON(site.info) : site.info, + basicInfo: CoreSiteBasicInfo = { + id: site.id, + siteUrl: site.siteUrl, + fullName: siteInfo && siteInfo.fullname, + siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename, + avatar: siteInfo && siteInfo.userpictureurl, + siteHomeId: siteInfo && siteInfo.siteid || 1 + }; + formattedSites.push(basicInfo); + } }); + + return formattedSites; } /** @@ -1257,11 +1279,13 @@ export class CoreSitesProvider { * * @return Promise resolved when the sites IDs are retrieved. */ - getLoggedInSitesIds(): Promise { - return this.appDB.getRecords(this.SITES_TABLE, {loggedOut : 0}).then((sites) => { - return sites.map((site) => { - return site.id; - }); + async getLoggedInSitesIds(): Promise { + await this.dbReady; + + const sites = await this.appDB.getRecords(this.SITES_TABLE, {loggedOut : 0}); + + return sites.map((site) => { + return site.id; }); } @@ -1270,11 +1294,13 @@ export class CoreSitesProvider { * * @return Promise resolved when the sites IDs are retrieved. */ - getSitesIds(): Promise { - return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => { - return sites.map((site) => { - return site.id; - }); + async getSitesIds(): Promise { + await this.dbReady; + + const sites = await this.appDB.getAllRecords(this.SITES_TABLE); + + return sites.map((site) => { + return site.id; }); } @@ -1284,15 +1310,17 @@ export class CoreSitesProvider { * @param siteid ID of the site the user is accessing. * @return Promise resolved when current site is stored. */ - login(siteId: string): Promise { + async login(siteId: string): Promise { + await this.dbReady; + const entry = { id: 1, siteId: siteId }; - return this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry).then(() => { - this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId); - }); + await this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry); + + this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId); } /** @@ -1300,7 +1328,9 @@ export class CoreSitesProvider { * * @return Promise resolved when the user is logged out. */ - logout(): Promise { + async logout(): Promise { + await this.dbReady; + let siteId; const promises = []; @@ -1317,9 +1347,11 @@ export class CoreSitesProvider { promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, { id: 1 })); } - return Promise.all(promises).finally(() => { + try { + await Promise.all(promises); + } finally { this.eventsProvider.trigger(CoreEventsProvider.LOGOUT, {}, siteId); - }); + } } /** @@ -1327,21 +1359,24 @@ export class CoreSitesProvider { * * @return Promise resolved if a session is restored. */ - restoreSession(): Promise { + async restoreSession(): Promise { if (this.sessionRestored) { return Promise.reject(null); } + await this.dbReady; + this.sessionRestored = true; - return this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }).then((currentSite) => { + try { + const currentSite = await this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }); const siteId = currentSite.siteId; this.logger.debug(`Restore session in site ${siteId}`); return this.loadSite(siteId); - }).catch(() => { + } catch (err) { // No current session. - }); + } } /** @@ -1351,17 +1386,18 @@ export class CoreSitesProvider { * @param loggedOut True to set the site as logged out, false otherwise. * @return Promise resolved when done. */ - setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { - return this.getSite(siteId).then((site) => { - const newValues = { - token: '', // Erase the token for security. - loggedOut: loggedOut ? 1 : 0 - }; + async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise { + await this.dbReady; - site.setLoggedOut(loggedOut); + const site = await this.getSite(siteId); + const newValues = { + token: '', // Erase the token for security. + loggedOut: loggedOut ? 1 : 0 + }; - return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }); - }); + site.setLoggedOut(loggedOut); + + return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }); } /** @@ -1396,20 +1432,21 @@ export class CoreSitesProvider { * @param privateToken User's private token. * @return A promise resolved when the site is updated. */ - updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { - return this.getSite(siteId).then((site) => { - const newValues = { - token: token, - privateToken: privateToken, - loggedOut: 0 - }; + async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { + await this.dbReady; - site.token = token; - site.privateToken = privateToken; - site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. + const site = await this.getSite(siteId); + const newValues = { + token: token, + privateToken: privateToken, + loggedOut: 0 + }; - return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }); - }); + site.token = token; + site.privateToken = privateToken; + site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. + + return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }); } /** @@ -1418,39 +1455,49 @@ export class CoreSitesProvider { * @param siteid Site's ID. * @return A promise resolved when the site is updated. */ - updateSiteInfo(siteId: string): Promise { - return this.getSite(siteId).then((site) => { - return site.fetchSiteInfo().then((info) => { - site.setInfo(info); + async updateSiteInfo(siteId: string): Promise { + await this.dbReady; - const versionCheck = this.isValidMoodleVersion(info); - if (versionCheck != this.VALID_VERSION) { - // The Moodle version is not supported, reject. - return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId()); - } + const site = await this.getSite(siteId); - // Try to get the site config. - return this.getSiteConfig(site).catch(() => { - // Error getting config, keep the current one. - }).then((config) => { - const newValues: any = { - info: JSON.stringify(info), - loggedOut: site.isLoggedOut() ? 1 : 0 - }; + try { - if (typeof config != 'undefined') { - site.setConfig(config); - newValues.config = JSON.stringify(config); - } + const info = await site.fetchSiteInfo(); + site.setInfo(info); - return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }).finally(() => { - this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, info, siteId); - }); - }); - }).catch((error) => { - // Ignore that we cannot fetch site info. Probably the auth token is invalid. - }); - }); + const versionCheck = this.isValidMoodleVersion(info); + if (versionCheck != this.VALID_VERSION) { + // The Moodle version is not supported, reject. + return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId()); + } + + // Try to get the site config. + let config; + + try { + config = await this.getSiteConfig(site); + } catch (error) { + // Error getting config, keep the current one. + } + + const newValues: any = { + info: JSON.stringify(info), + loggedOut: site.isLoggedOut() ? 1 : 0 + }; + + if (typeof config != 'undefined') { + site.setConfig(config); + newValues.config = JSON.stringify(config); + } + + try { + await this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }); + } finally { + this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, info, siteId); + } + } catch (error) { + // Ignore that we cannot fetch site info. Probably the auth token is invalid. + } } /** @@ -1476,11 +1523,13 @@ export class CoreSitesProvider { * @param username If set, it will return only the sites where the current user has this username. * @return Promise resolved with the site IDs (array). */ - getSiteIdsFromUrl(url: string, prioritize?: boolean, username?: string): Promise { + async getSiteIdsFromUrl(url: string, prioritize?: boolean, username?: string): Promise { + await this.dbReady; + // If prioritize is true, check current site first. if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) { if (!username || this.currentSite.getInfo().username == username) { - return Promise.resolve([this.currentSite.getId()]); + return [this.currentSite.getId()]; } } @@ -1489,18 +1538,19 @@ export class CoreSitesProvider { // URL doesn't have http(s) protocol. Check if it has any protocol. if (this.urlUtils.isAbsoluteURL(url)) { // It has some protocol. Return empty array. - return Promise.resolve([]); + return []; } else { // No protocol, probably a relative URL. Return current site. if (this.currentSite) { - return Promise.resolve([this.currentSite.getId()]); + return [this.currentSite.getId()]; } else { - return Promise.resolve([]); + return []; } } } - return this.appDB.getAllRecords(this.SITES_TABLE).then((siteEntries) => { + try { + const siteEntries = await this.appDB.getAllRecords(this.SITES_TABLE); const ids = []; const promises = []; @@ -1516,13 +1566,13 @@ export class CoreSitesProvider { } }); - return Promise.all(promises).then(() => { - return ids; - }); - }).catch(() => { + await Promise.all(promises); + + return ids; + } catch (error) { // Shouldn't happen. return []; - }); + } } /** @@ -1530,10 +1580,12 @@ export class CoreSitesProvider { * * @return Promise resolved with the site ID. */ - getStoredCurrentSiteId(): Promise { - return this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }).then((currentSite) => { - return currentSite.siteId; - }); + async getStoredCurrentSiteId(): Promise { + await this.dbReady; + + const currentSite = await this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }); + + return currentSite.siteId; } /** @@ -1580,6 +1632,7 @@ export class CoreSitesProvider { * Create a table in all the sites databases. * * @param table Table schema. + * @deprecated. Please use registerSiteSchema instead. */ createTableFromSchema(table: SQLiteDBTableSchema): void { this.createTablesFromSchema([table]); @@ -1589,6 +1642,7 @@ export class CoreSitesProvider { * Create several tables in all the sites databases. * * @param tables List of tables schema. + * @deprecated. Please use registerSiteSchema instead. */ createTablesFromSchema(tables: SQLiteDBTableSchema[]): void { // Add the tables to the list of schemas. This list is to create all the tables in new sites.