MOBILE-4304 core: Replace WebSQL with sqlite-wasm
This commit is contained in:
		
							parent
							
								
									368bf02bc2
								
							
						
					
					
						commit
						a7bd1e5f89
					
				| @ -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": { | ||||
|  | ||||
							
								
								
									
										9
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -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", | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
							
								
								
									
										31
									
								
								patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								patches/@sqlite.org+sqlite-wasm+3.45.0-build1.patch
									
									
									
									
									
										Normal file
									
								
							| @ -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 { | ||||
| @ -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) { | ||||
|  | ||||
| @ -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<void>; | ||||
| 
 | ||||
|     /** | ||||
|      * 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<void> { | ||||
|         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<any> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         await this.ready(); | ||||
| 
 | ||||
|         await this.db?.open(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Wait for the DB to be ready. | ||||
|      * | ||||
|      * @returns Promise resolved when ready. | ||||
|      */ | ||||
|     ready(): Promise<void> { | ||||
|         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<SQLiteObject> { | ||||
|         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<SQLiteObject> { | ||||
|         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 = { | ||||
|  | ||||
| @ -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<any> { | ||||
|         // 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<any> { | ||||
|         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<void>[] = []; | ||||
| 
 | ||||
|                     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<any> { | ||||
|         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<any> { | ||||
|         await this.ready(); | ||||
| 
 | ||||
|         return new Promise((resolve, reject): void => { | ||||
|             // Create a transaction to execute the queries.
 | ||||
|             this.db?.transaction((tx) => { | ||||
|                 const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|                 // 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<void> { | ||||
|         // WebSQL databases can't closed, so the open method isn't needed.
 | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async createDatabase(): Promise<SQLiteObject> { | ||||
|         // 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<SQLiteObject> { | ||||
|         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; | ||||
| } | ||||
							
								
								
									
										130
									
								
								src/core/features/emulator/classes/wasm-sqlite-object.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/core/features/emulator/classes/wasm-sqlite-object.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<Sqlite3Worker1Promiser>; | ||||
|     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<any> { | ||||
|         if (!this.promisedPromiser.isResolved()) { | ||||
|             await this.open(); | ||||
|         } | ||||
| 
 | ||||
|         await this.promiser('close', { unlink: true }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async open(): Promise<any> { | ||||
|         const promiser = await new Promise<Sqlite3Worker1Promiser>((resolve) => { | ||||
|             const _promiser = sqlite3Worker1Promiser(() => resolve(_promiser)); | ||||
|         }); | ||||
| 
 | ||||
|         await promiser('open', { filename: `file:${this.name}.sqlite3`, vfs: 'opfs' }); | ||||
| 
 | ||||
|         this.promisedPromiser.resolve(promiser); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async close(): Promise<any> { | ||||
|         await this.promiser('close', {}); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async executeSql(statement: string, params?: any[] | undefined): Promise<any> { | ||||
|         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<any> { | ||||
|         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'); | ||||
| 
 | ||||
| } | ||||
| @ -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 () => { | ||||
|  | ||||
							
								
								
									
										47
									
								
								src/core/features/emulator/services/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/core/features/emulator/services/db.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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<void> { | ||||
|         const db = new WasmSQLiteObject(name); | ||||
| 
 | ||||
|         await db.delete(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -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<void> { | ||||
|         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<void> { | ||||
|         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<SQLiteObject> { | ||||
|         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; | ||||
|                 } | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
							
								
								
									
										93
									
								
								src/types/sqlite-wasm.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/types/sqlite-wasm.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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<unknown, 'SqliteDbId'>; | ||||
| 
 | ||||
|     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<T extends keyof Sqlite3Worker1Messages> = | ||||
|         Sqlite3Worker1Messages[T] extends { args?: infer TArgs } | ||||
|             ? { type: T; args: TArgs } | ||||
|             : { type: T; args?: Sqlite3Worker1Messages[T]['args'] }; | ||||
| 
 | ||||
|     export type Sqlite3Worker1Promiser = | ||||
|         (<T extends keyof Sqlite3Worker1Messages>( | ||||
|             type: T, | ||||
|             args: Sqlite3Worker1Messages[T]['args'], | ||||
|         ) => Promise<Sqlite3Worker1Messages[T]['result']>) & | ||||
|         (<T extends keyof Sqlite3Worker1Messages>( | ||||
|             options: Sqlite3Worker1PromiserMethodOptions<T>, | ||||
|         ) => Promise<Sqlite3Worker1Messages[T]['result']>); | ||||
| 
 | ||||
|     export function sqlite3Worker1Promiser(): Sqlite3Worker1Promiser; | ||||
|     export function sqlite3Worker1Promiser(onready: () => void): Sqlite3Worker1Promiser; | ||||
|     export function sqlite3Worker1Promiser(config: Sqlite3Worker1PromiserOptions): Sqlite3Worker1Promiser; | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user