diff --git a/angular.json b/angular.json index cbffd58ac..07faf01a0 100644 --- a/angular.json +++ b/angular.json @@ -95,7 +95,11 @@ "options": { "disableHostCheck": true, "port": 8100, - "buildTarget": "app:build" + "buildTarget": "app:build", + "headers": { + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" + } }, "configurations": { "production": { diff --git a/package-lock.json b/package-lock.json index 41ee2d719..18ad94365 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@sqlite.org/sqlite-wasm": "^3.45.0-build1", "@types/chart.js": "^2.9.31", "@types/cordova": "0.0.34", "@types/dom-mediacapture-record": "1.0.7", @@ -8997,6 +8998,14 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.45.0-build1", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.45.0-build1.tgz", + "integrity": "sha512-QAwE4n16t82g8kbhpuBzy6pzh7bm5VKziNKwQHmIPmtCBUk2AlUndsGS5qL8pAfOrrafXq9xILa0LdZkPFetgA==", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@stencil/core": { "version": "4.10.0", "license": "MIT", diff --git a/package.json b/package.json index 440f6af04..a04ed7d4b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ], "scripts": { "ng": "ng", - "start": "ionic serve --browser=$MOODLE_APP_BROWSER", + "start": "ionic serve --browser=$MOODLE_APP_BROWSER --ssl", "serve:test": "NODE_ENV=testing ionic serve --no-open", "build": "ionic build", "build:prod": "NODE_ENV=production ionic build --prod", @@ -89,6 +89,7 @@ "@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", + "@sqlite.org/sqlite-wasm": "^3.45.0-build1", "@types/chart.js": "^2.9.31", "@types/cordova": "0.0.34", "@types/dom-mediacapture-record": "1.0.7", diff --git a/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch new file mode 100644 index 000000000..793eae2e0 --- /dev/null +++ b/patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch @@ -0,0 +1,31 @@ +diff --git a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +index b86a0aa..a9bf793 100644 +--- a/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs ++++ b/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +@@ -533,7 +533,7 @@ var sqlite3InitModule = (() => { + wasmBinaryFile = locateFile(wasmBinaryFile); + } + } else { +- wasmBinaryFile = new URL('sqlite3.wasm', import.meta.url).href; ++ wasmBinaryFile = '/assets/lib/sqlite3/sqlite3.wasm'; + } + + function getBinary(file) { +@@ -12522,7 +12522,7 @@ var sqlite3InitModule = (() => { + return promiseResolve_(sqlite3); + }; + const W = new Worker( +- new URL('sqlite3-opfs-async-proxy.js', import.meta.url), ++ '/assets/lib/sqlite3/sqlite3-opfs-async-proxy.js', + ); + setTimeout(() => { + if (undefined === promiseWasRejected) { +@@ -13445,7 +13445,7 @@ var sqlite3InitModule = (() => { + }); + return thePromise; + }; +- installOpfsVfs.defaultProxyUri = 'sqlite3-opfs-async-proxy.js'; ++ installOpfsVfs.defaultProxyUri = '/assets/lib/sqlite3/sqlite3-opfs-async-proxy.js'; + globalThis.sqlite3ApiBootstrap.initializersAsync.push( + async (sqlite3) => { + try { diff --git a/scripts/copy-assets.js b/scripts/copy-assets.js index 72274160b..5e9bf9dd2 100644 --- a/scripts/copy-assets.js +++ b/scripts/copy-assets.js @@ -31,6 +31,8 @@ const ASSETS = { '/src/core/features/h5p/assets': '/lib/h5p', '/node_modules/ogv/dist': '/lib/ogv', '/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css', + '/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm': '/lib/sqlite3/sqlite3.wasm', + '/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js': '/lib/sqlite3/sqlite3-opfs-async-proxy.js', }; module.exports = function(ctx) { diff --git a/src/core/classes/sqlitedb.ts b/src/core/classes/sqlitedb.ts index 2e7637d0a..5bb9c40ec 100644 --- a/src/core/classes/sqlitedb.ts +++ b/src/core/classes/sqlitedb.ts @@ -14,10 +14,7 @@ import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; -import { SQLite } from '@singletons'; import { CoreError } from '@classes/errors/error'; -import { CoreDB } from '@services/db'; -import { CorePlatform } from '@services/platform'; type SQLiteDBColumnType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'; @@ -137,17 +134,13 @@ export interface SQLiteDBForeignKeySchema { */ export class SQLiteDB { - db?: SQLiteObject; - promise!: Promise; - /** * Create and open the database. * * @param name Database name. + * @param db Database connection. */ - constructor(public name: string) { - this.init(); - } + constructor(public name: string, private db: SQLiteObject) {} /** * Add a column to an existing table. @@ -277,9 +270,7 @@ export class SQLiteDB { * @returns Promise resolved when done. */ async close(): Promise { - await this.ready(); - - await this.db?.close(); + await this.db.close(); } /** @@ -455,9 +446,7 @@ export class SQLiteDB { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise { - await this.ready(); - - return this.db?.executeSql(sql, params); + return this.db.executeSql(sql, params); } /** @@ -470,9 +459,7 @@ export class SQLiteDB { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async executeBatch(sqlStatements: (string | string[] | any)[]): Promise { - await this.ready(); - - await this.db?.sqlBatch(sqlStatements); + await this.db.sqlBatch(sqlStatements); } /** @@ -753,25 +740,6 @@ export class SQLiteDB { }; } - /** - * Initialize the database. - */ - init(): void { - this.promise = this.createDatabase().then(db => { - if (CoreDB.loggingEnabled()) { - const spies = this.getDatabaseSpies(db); - - db = new Proxy(db, { - get: (target, property, receiver) => spies[property] ?? Reflect.get(target, property, receiver), - }); - } - - this.db = db; - - return; - }); - } - /** * Insert a record into a table and return the "rowId" field. * @@ -898,18 +866,7 @@ export class SQLiteDB { * @returns Promise resolved when open. */ async open(): Promise { - await this.ready(); - - await this.db?.open(); - } - - /** - * Wait for the DB to be ready. - * - * @returns Promise resolved when ready. - */ - ready(): Promise { - return this.promise; + await this.db.open(); } /** @@ -1094,83 +1051,6 @@ export class SQLiteDB { return { sql, params }; } - /** - * Open a database connection. - * - * @returns Database. - */ - protected async createDatabase(): Promise { - await CorePlatform.ready(); - - return SQLite.create({ name: this.name, location: 'default' }); - } - - /** - * Get database spy methods to intercept database calls and track logging information. - * - * @param db Database to spy. - * @returns Spy methods. - */ - protected getDatabaseSpies(db: SQLiteObject): Partial { - const dbName = this.name; - - return { - async executeSql(statement, params) { - const start = performance.now(); - - try { - const result = await db.executeSql(statement, params); - - CoreDB.logQuery({ - params, - sql: statement, - duration: performance.now() - start, - dbName, - }); - - return result; - } catch (error) { - CoreDB.logQuery({ - params, - error, - sql: statement, - duration: performance.now() - start, - dbName, - }); - - throw error; - } - }, - async sqlBatch(statements) { - const start = performance.now(); - const sql = Array.isArray(statements) - ? statements.join(' | ') - : String(statements); - - try { - const result = await db.sqlBatch(statements); - - CoreDB.logQuery({ - sql, - duration: performance.now() - start, - dbName, - }); - - return result; - } catch (error) { - CoreDB.logQuery({ - sql, - error, - duration: performance.now() - start, - dbName, - }); - - throw error; - } - }, - }; - } - } export type SQLiteDBRecordValues = { diff --git a/src/core/features/emulator/classes/sqlitedb.ts b/src/core/features/emulator/classes/sqlitedb.ts deleted file mode 100644 index f0243ba6f..000000000 --- a/src/core/features/emulator/classes/sqlitedb.ts +++ /dev/null @@ -1,219 +0,0 @@ -// (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 { SQLiteDB } from '@classes/sqlitedb'; -import { DbTransaction, SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; -import { CoreDB } from '@services/db'; - -/** - * Class to mock the interaction with the SQLite database. - */ -export class SQLiteDBMock extends SQLiteDB { - - /** - * Create and open the database. - * - * @param name Database name. - */ - constructor(public name: string) { - super(name); - } - - /** - * Close the database. - * - * @returns Promise resolved when done. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - close(): Promise { - // WebSQL databases aren't closed. - return Promise.resolve(); - } - - /** - * Drop all the data in the database. - * - * @returns Promise resolved when done. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async emptyDatabase(): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - this.db?.transaction((tx) => { - // Query all tables from sqlite_master that we have created and can modify. - const args = []; - const query = `SELECT * FROM sqlite_master - WHERE name NOT LIKE 'sqlite\\_%' escape '\\' AND name NOT LIKE '\\_%' escape '\\'`; - - tx.executeSql(query, args, (tx, result) => { - if (result.rows.length <= 0) { - // No tables to delete, stop. - resolve(null); - - return; - } - - // Drop all the tables. - const promises: Promise[] = []; - - for (let i = 0; i < result.rows.length; i++) { - promises.push(new Promise((resolve, reject): void => { - // Drop the table. - const name = JSON.stringify(result.rows.item(i).name); - tx.executeSql('DROP TABLE ' + name, [], resolve, reject); - })); - } - - Promise.all(promises).then(resolve).catch(reject); - }, reject); - }); - }); - } - - /** - * Execute a SQL query. - * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that - * these query will be run in SQLite (Mobile) and Web SQL (desktop), so your query should work in both environments. - * - * @param sql SQL query to execute. - * @param params Query parameters. - * @returns Promise resolved with the result. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async execute(sql: string, params?: any[]): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - // With WebSQL, all queries must be run in a transaction. - this.db?.transaction((tx) => { - tx.executeSql( - sql, - params, - (_, results) => resolve(results), - (_, error) => reject(new Error(`SQL failed: ${sql}, reason: ${error?.message}`)), - ); - }); - }); - } - - /** - * Execute a set of SQL queries. This operation is atomic. - * IMPORTANT: Use this function only if you cannot use any of the other functions in this API. Please take into account that - * these query will be run in SQLite (Mobile) and Web SQL (desktop), so your query should work in both environments. - * - * @param sqlStatements SQL statements to execute. - * @returns Promise resolved with the result. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async executeBatch(sqlStatements: any[]): Promise { - await this.ready(); - - return new Promise((resolve, reject): void => { - // Create a transaction to execute the queries. - this.db?.transaction((tx) => { - const promises: Promise[] = []; - - // Execute all the queries. Each statement can be a string or an array. - sqlStatements.forEach((statement) => { - promises.push(new Promise((resolve, reject): void => { - let query; - let params; - - if (Array.isArray(statement)) { - query = statement[0]; - params = statement[1]; - } else { - query = statement; - params = null; - } - - tx.executeSql(query, params, (_, results) => resolve(results), (_, error) => reject(error)); - })); - }); - - // eslint-disable-next-line promise/catch-or-return - Promise.all(promises).then(resolve, reject); - }); - }); - } - - /** - * Open the database. Only needed if it was closed before, a database is automatically opened when created. - * - * @returns Promise resolved when done. - */ - open(): Promise { - // WebSQL databases can't closed, so the open method isn't needed. - return Promise.resolve(); - } - - /** - * @inheritdoc - */ - protected async createDatabase(): Promise { - // This DB is for desktop apps, so use a big size to be sure it isn't filled. - return (window as unknown as WebSQLWindow).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); - } - - /** - * @inheritdoc - */ - protected getDatabaseSpies(db: SQLiteObject): Partial { - const dbName = this.name; - - return { - transaction: (callback) => db.transaction((transaction) => { - const transactionSpy: DbTransaction = { - executeSql(sql, params, success, error) { - const start = performance.now(); - - return transaction.executeSql( - sql, - params, - (...args) => { - CoreDB.logQuery({ - sql, - params, - duration: performance.now() - start, - dbName, - }); - - return success?.(...args); - }, - (...args) => { - CoreDB.logQuery({ - sql, - params, - error: args[0], - duration: performance.now() - start, - dbName, - }); - - return error?.(...args); - }, - ); - }, - }; - - return callback(transactionSpy); - }), - }; - } - -} - -interface WebSQLWindow extends Window { - openDatabase(name: string, version: string, displayName: string, estimatedSize: number): SQLiteObject; -} diff --git a/src/core/features/emulator/classes/wasm-sqlite-object.ts b/src/core/features/emulator/classes/wasm-sqlite-object.ts new file mode 100644 index 000000000..e76fb54e4 --- /dev/null +++ b/src/core/features/emulator/classes/wasm-sqlite-object.ts @@ -0,0 +1,130 @@ +// (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. + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { CorePromisedValue } from '@classes/promised-value'; +import { Sqlite3Worker1Promiser, sqlite3Worker1Promiser } from '@sqlite.org/sqlite-wasm'; + +/** + * Throw an error indicating that the given method hasn't been implemented. + * + * @param method Method name. + */ +function notImplemented(method: string): any { + throw new Error(`${method} method not implemented.`); +} + +/** + * SQLiteObject adapter implemented using the sqlite-wasm package. + */ +export class WasmSQLiteObject implements SQLiteObject { + + private name: string; + private promisedPromiser: CorePromisedValue; + private promiser: Sqlite3Worker1Promiser; + + constructor(name: string) { + this.name = name; + this.promisedPromiser = new CorePromisedValue(); + this.promiser = async (...args) => { + const promiser = await this.promisedPromiser; + + return promiser.call(promiser, ...args); + }; + } + + /** + * Delete the database. + */ + async delete(): Promise { + if (!this.promisedPromiser.isResolved()) { + await this.open(); + } + + await this.promiser('close', { unlink: true }); + } + + /** + * @inheritdoc + */ + async open(): Promise { + const promiser = await new Promise((resolve) => { + const _promiser = sqlite3Worker1Promiser(() => resolve(_promiser)); + }); + + await promiser('open', { filename: `file:${this.name}.sqlite3`, vfs: 'opfs' }); + + this.promisedPromiser.resolve(promiser); + } + + /** + * @inheritdoc + */ + async close(): Promise { + await this.promiser('close', {}); + } + + /** + * @inheritdoc + */ + async executeSql(statement: string, params?: any[] | undefined): Promise { + const rows = [] as unknown[]; + + await this.promiser('exec', { + sql: statement, + bind: params, + callback({ row, columnNames }) { + if (!row) { + return; + } + + rows.push(columnNames.reduce((record, column, index) => { + record[column] = row[index]; + + return record; + }, {})); + }, + }); + + return { + rows: { + item: (i: number) => rows[i], + length: rows.length, + }, + rowsAffected: rows.length, + }; + } + + /** + * @inheritdoc + */ + async sqlBatch(sqlStatements: any[]): Promise { + await Promise.all(sqlStatements.map(sql => this.executeSql(sql))); + } + + // These methods and properties are not used in our app, + // but still need to be declared to conform with the SQLiteObject interface. + _objectInstance = null; // eslint-disable-line @typescript-eslint/naming-convention + databaseFeatures = { isSQLitePluginDatabase: false }; + openDBs = null; + addTransaction = () => notImplemented('SQLiteObject.addTransaction'); + transaction = () => notImplemented('SQLiteObject.transaction'); + readTransaction = () => notImplemented('SQLiteObject.readTransaction'); + startNextTransaction = () => notImplemented('SQLiteObject.startNextTransaction'); + abortallPendingTransactions = () => notImplemented('SQLiteObject.abortallPendingTransactions'); + +} diff --git a/src/core/features/emulator/emulator.module.ts b/src/core/features/emulator/emulator.module.ts index b40e57b73..098ef3e9e 100644 --- a/src/core/features/emulator/emulator.module.ts +++ b/src/core/features/emulator/emulator.module.ts @@ -42,6 +42,8 @@ import { CorePlatform } from '@services/platform'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreNative } from '@features/native/services/native'; import { SecureStorageMock } from '@features/emulator/classes/SecureStorage'; +import { CoreDbProvider } from '@services/db'; +import { CoreDbProviderMock } from '@features/emulator/services/db'; /** * This module handles the emulation of Cordova plugins in browser and desktop. @@ -95,6 +97,10 @@ import { SecureStorageMock } from '@features/emulator/classes/SecureStorage'; ? new LocalNotifications() : new LocalNotificationsMock(), }, + { + provide: CoreDbProvider, + useFactory: (): CoreDbProvider => CorePlatform.is('cordova') ? new CoreDbProvider() : new CoreDbProviderMock(), + }, { provide: APP_INITIALIZER, useValue: async () => { diff --git a/src/core/features/emulator/services/db.ts b/src/core/features/emulator/services/db.ts new file mode 100644 index 000000000..44a9947c0 --- /dev/null +++ b/src/core/features/emulator/services/db.ts @@ -0,0 +1,47 @@ +// (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 { asyncInstance } from '@/core/utils/async-instance'; +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { WasmSQLiteObject } from '@features/emulator/classes/wasm-sqlite-object'; +import { CoreDbProvider } from '@services/db'; + +/** + * Emulates the database provider in the browser. + */ +export class CoreDbProviderMock extends CoreDbProvider { + + /** + * @inheritdoc + */ + protected createDatabase(name: string): SQLiteObject { + return asyncInstance(async () => { + const db = new WasmSQLiteObject(name); + + await db.open(); + + return db; + }); + } + + /** + * @inheritdoc + */ + protected async deleteDatabase(name: string): Promise { + const db = new WasmSQLiteObject(name); + + await db.delete(); + } + +} diff --git a/src/core/services/db.ts b/src/core/services/db.ts index 87eefd519..80df9a09f 100644 --- a/src/core/services/db.ts +++ b/src/core/services/db.ts @@ -15,10 +15,11 @@ import { Injectable } from '@angular/core'; import { SQLiteDB } from '@classes/sqlitedb'; -import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; import { CoreBrowser } from '@singletons/browser'; -import { makeSingleton, SQLite } from '@singletons'; +import { SQLite, makeSingleton } from '@singletons'; import { CorePlatform } from '@services/platform'; +import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx'; +import { asyncInstance } from '@/core/utils/async-instance'; const tableNameRegex = new RegExp([ '^SELECT.*FROM ([^ ]+)', @@ -208,45 +209,129 @@ export class CoreDbProvider { */ getDB(name: string, forceNew?: boolean): SQLiteDB { if (this.dbInstances[name] === undefined || forceNew) { - if (CorePlatform.is('cordova')) { - this.dbInstances[name] = new SQLiteDB(name); - } else { - this.dbInstances[name] = new SQLiteDBMock(name); + let db = this.createDatabase(name); + + if (this.loggingEnabled()) { + const spies = this.getDatabaseSpies(name, db); + + db = new Proxy(db, { + get: (target, property, receiver) => spies[property] ?? Reflect.get(target, property, receiver), + }) as unknown as SQLiteObject; } + + this.dbInstances[name] = new SQLiteDB(name, db); } return this.dbInstances[name]; } + /** + * Create database connection. + * + * @param name Database name. + * @returns Database connection. + */ + protected createDatabase(name: string): SQLiteObject { + // Ideally, this method would return a Promise instead of resorting to Duck typing; + // but doing so would mean that the getDB() method should also return a promise. + // Given that it is heavily used throughout the app, we want to avoid it for now. + return asyncInstance(async () => { + await CorePlatform.ready(); + + return SQLite.create({ name, location: 'default' }); + }); + } + /** * Delete a DB. * * @param name DB name. - * @returns Promise resolved when the DB is deleted. */ async deleteDB(name: string): Promise { if (this.dbInstances[name] !== undefined) { - // Close the database first. await this.dbInstances[name].close(); - const db = this.dbInstances[name]; delete this.dbInstances[name]; - - if (db instanceof SQLiteDBMock) { - // In WebSQL we cannot delete the database, just empty it. - return db.emptyDatabase(); - } else { - return SQLite.deleteDatabase({ - name, - location: 'default', - }); - } - } else if (CorePlatform.is('cordova')) { - return SQLite.deleteDatabase({ - name, - location: 'default', - }); } + + await this.deleteDatabase(name); + } + + /** + * Delete database. + * + * @param name Database name. + */ + protected async deleteDatabase(name: string): Promise { + await SQLite.deleteDatabase({ + name, + location: 'default', + }); + } + + /** + * Get database spy methods to intercept database calls and track logging information. + * + * @param dbName Database name. + * @param db Database to spy. + * @returns Spy methods. + */ + protected getDatabaseSpies(dbName: string, db: SQLiteObject): Partial { + return { + async executeSql(statement, params) { + const start = performance.now(); + + try { + const result = await db.executeSql(statement, params); + + CoreDB.logQuery({ + params, + sql: statement, + duration: performance.now() - start, + dbName, + }); + + return result; + } catch (error) { + CoreDB.logQuery({ + params, + error, + sql: statement, + duration: performance.now() - start, + dbName, + }); + + throw error; + } + }, + async sqlBatch(statements) { + const start = performance.now(); + const sql = Array.isArray(statements) + ? statements.join(' | ') + : String(statements); + + try { + const result = await db.sqlBatch(statements); + + CoreDB.logQuery({ + sql, + duration: performance.now() - start, + dbName, + }); + + return result; + } catch (error) { + CoreDB.logQuery({ + sql, + error, + duration: performance.now() - start, + dbName, + }); + + throw error; + } + }, + }; } } diff --git a/src/types/sqlite-wasm.d.ts b/src/types/sqlite-wasm.d.ts new file mode 100644 index 000000000..2e89bf70d --- /dev/null +++ b/src/types/sqlite-wasm.d.ts @@ -0,0 +1,93 @@ +// (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 { Brand } from '@/core/utils/types'; + +// Can be removed when the following issue is fixed: +// https://github.com/sqlite/sqlite-wasm/issues/53 + +declare module '@sqlite.org/sqlite-wasm' { + + export type SqliteDbId = Brand; + + export interface SqliteRowData { + columnNames: string[]; + row: SqlValue[] | undefined; + rowNumber: number | null; + } + + export interface Sqlite3Worker1Messages { + close: { + args?: { + unlink?: boolean; + }; + result: { + filename?: string; + }; + }; + 'config-get': { + result: { + version: object; + bigIntEnabled: boolean; + vfsList: unknown; + }; + }; + exec: { + args: { + sql: string; + bind?: BindingSpec; + callback?(data: SqliteRowData): void | false; + }; + }; + open: { + args: { + filename: string; + vfs?: string; + }; + result: { + dbId: SqliteDbId; + filename: string; + persistent: boolean; + vfs: string; + }; + }; + } + + export interface Sqlite3Worker1PromiserConfig { + onready(): void; + worker?: unknown; + generateMessageId?(message: object): string; + debug?(...args: unknown[]): void; + onunhandled?(event: unknown): void; + } + + export type Sqlite3Worker1PromiserMethodOptions = + Sqlite3Worker1Messages[T] extends { args?: infer TArgs } + ? { type: T; args: TArgs } + : { type: T; args?: Sqlite3Worker1Messages[T]['args'] }; + + export type Sqlite3Worker1Promiser = + (( + type: T, + args: Sqlite3Worker1Messages[T]['args'], + ) => Promise) & + (( + options: Sqlite3Worker1PromiserMethodOptions, + ) => Promise); + + export function sqlite3Worker1Promiser(): Sqlite3Worker1Promiser; + export function sqlite3Worker1Promiser(onready: () => void): Sqlite3Worker1Promiser; + export function sqlite3Worker1Promiser(config: Sqlite3Worker1PromiserOptions): Sqlite3Worker1Promiser; + +}