MOBILE-4304 core: Replace WebSQL with sqlite-wasm
parent
368bf02bc2
commit
a7bd1e5f89
|
@ -95,7 +95,11 @@
|
||||||
"options": {
|
"options": {
|
||||||
"disableHostCheck": true,
|
"disableHostCheck": true,
|
||||||
"port": 8100,
|
"port": 8100,
|
||||||
"buildTarget": "app:build"
|
"buildTarget": "app:build",
|
||||||
|
"headers": {
|
||||||
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
|
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
"@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7",
|
"@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7",
|
||||||
"@ngx-translate/core": "^15.0.0",
|
"@ngx-translate/core": "^15.0.0",
|
||||||
"@ngx-translate/http-loader": "^8.0.0",
|
"@ngx-translate/http-loader": "^8.0.0",
|
||||||
|
"@sqlite.org/sqlite-wasm": "^3.45.0-build1",
|
||||||
"@types/chart.js": "^2.9.31",
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/dom-mediacapture-record": "1.0.7",
|
"@types/dom-mediacapture-record": "1.0.7",
|
||||||
|
@ -8997,6 +8998,14 @@
|
||||||
"@sinonjs/commons": "^3.0.0"
|
"@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": {
|
"node_modules/@stencil/core": {
|
||||||
"version": "4.10.0",
|
"version": "4.10.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"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",
|
"serve:test": "NODE_ENV=testing ionic serve --no-open",
|
||||||
"build": "ionic build",
|
"build": "ionic build",
|
||||||
"build:prod": "NODE_ENV=production ionic build --prod",
|
"build:prod": "NODE_ENV=production ionic build --prod",
|
||||||
|
@ -89,6 +89,7 @@
|
||||||
"@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7",
|
"@moodlehq/phonegap-plugin-push": "4.0.0-moodle.7",
|
||||||
"@ngx-translate/core": "^15.0.0",
|
"@ngx-translate/core": "^15.0.0",
|
||||||
"@ngx-translate/http-loader": "^8.0.0",
|
"@ngx-translate/http-loader": "^8.0.0",
|
||||||
|
"@sqlite.org/sqlite-wasm": "^3.45.0-build1",
|
||||||
"@types/chart.js": "^2.9.31",
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/dom-mediacapture-record": "1.0.7",
|
"@types/dom-mediacapture-record": "1.0.7",
|
||||||
|
|
|
@ -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',
|
'/src/core/features/h5p/assets': '/lib/h5p',
|
||||||
'/node_modules/ogv/dist': '/lib/ogv',
|
'/node_modules/ogv/dist': '/lib/ogv',
|
||||||
'/node_modules/video.js/dist/video-js.min.css': '/lib/video.js/video-js.min.css',
|
'/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) {
|
module.exports = function(ctx) {
|
||||||
|
|
|
@ -14,10 +14,7 @@
|
||||||
|
|
||||||
import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx';
|
import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx';
|
||||||
|
|
||||||
import { SQLite } from '@singletons';
|
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreDB } from '@services/db';
|
|
||||||
import { CorePlatform } from '@services/platform';
|
|
||||||
|
|
||||||
type SQLiteDBColumnType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
|
type SQLiteDBColumnType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
|
||||||
|
|
||||||
|
@ -137,17 +134,13 @@ export interface SQLiteDBForeignKeySchema {
|
||||||
*/
|
*/
|
||||||
export class SQLiteDB {
|
export class SQLiteDB {
|
||||||
|
|
||||||
db?: SQLiteObject;
|
|
||||||
promise!: Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and open the database.
|
* Create and open the database.
|
||||||
*
|
*
|
||||||
* @param name Database name.
|
* @param name Database name.
|
||||||
|
* @param db Database connection.
|
||||||
*/
|
*/
|
||||||
constructor(public name: string) {
|
constructor(public name: string, private db: SQLiteObject) {}
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a column to an existing table.
|
* Add a column to an existing table.
|
||||||
|
@ -277,9 +270,7 @@ export class SQLiteDB {
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
async executeBatch(sqlStatements: (string | string[] | any)[]): Promise<void> {
|
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.
|
* Insert a record into a table and return the "rowId" field.
|
||||||
*
|
*
|
||||||
|
@ -898,18 +866,7 @@ export class SQLiteDB {
|
||||||
* @returns Promise resolved when open.
|
* @returns Promise resolved when open.
|
||||||
*/
|
*/
|
||||||
async open(): Promise<void> {
|
async open(): Promise<void> {
|
||||||
await this.ready();
|
await this.db.open();
|
||||||
|
|
||||||
await this.db?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for the DB to be ready.
|
|
||||||
*
|
|
||||||
* @returns Promise resolved when ready.
|
|
||||||
*/
|
|
||||||
ready(): Promise<void> {
|
|
||||||
return this.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1094,83 +1051,6 @@ export class SQLiteDB {
|
||||||
return { sql, params };
|
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 = {
|
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;
|
|
||||||
}
|
|
|
@ -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 { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
import { CoreNative } from '@features/native/services/native';
|
import { CoreNative } from '@features/native/services/native';
|
||||||
import { SecureStorageMock } from '@features/emulator/classes/SecureStorage';
|
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.
|
* 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 LocalNotifications()
|
||||||
: new LocalNotificationsMock(),
|
: new LocalNotificationsMock(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CoreDbProvider,
|
||||||
|
useFactory: (): CoreDbProvider => CorePlatform.is('cordova') ? new CoreDbProvider() : new CoreDbProviderMock(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useValue: async () => {
|
useValue: async () => {
|
||||||
|
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { SQLiteDB } from '@classes/sqlitedb';
|
import { SQLiteDB } from '@classes/sqlitedb';
|
||||||
import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb';
|
|
||||||
import { CoreBrowser } from '@singletons/browser';
|
import { CoreBrowser } from '@singletons/browser';
|
||||||
import { makeSingleton, SQLite } from '@singletons';
|
import { SQLite, makeSingleton } from '@singletons';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
|
import { SQLiteObject } from '@awesome-cordova-plugins/sqlite/ngx';
|
||||||
|
import { asyncInstance } from '@/core/utils/async-instance';
|
||||||
|
|
||||||
const tableNameRegex = new RegExp([
|
const tableNameRegex = new RegExp([
|
||||||
'^SELECT.*FROM ([^ ]+)',
|
'^SELECT.*FROM ([^ ]+)',
|
||||||
|
@ -208,45 +209,129 @@ export class CoreDbProvider {
|
||||||
*/
|
*/
|
||||||
getDB(name: string, forceNew?: boolean): SQLiteDB {
|
getDB(name: string, forceNew?: boolean): SQLiteDB {
|
||||||
if (this.dbInstances[name] === undefined || forceNew) {
|
if (this.dbInstances[name] === undefined || forceNew) {
|
||||||
if (CorePlatform.is('cordova')) {
|
let db = this.createDatabase(name);
|
||||||
this.dbInstances[name] = new SQLiteDB(name);
|
|
||||||
} else {
|
if (this.loggingEnabled()) {
|
||||||
this.dbInstances[name] = new SQLiteDBMock(name);
|
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];
|
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.
|
* Delete a DB.
|
||||||
*
|
*
|
||||||
* @param name DB name.
|
* @param name DB name.
|
||||||
* @returns Promise resolved when the DB is deleted.
|
|
||||||
*/
|
*/
|
||||||
async deleteDB(name: string): Promise<void> {
|
async deleteDB(name: string): Promise<void> {
|
||||||
if (this.dbInstances[name] !== undefined) {
|
if (this.dbInstances[name] !== undefined) {
|
||||||
// Close the database first.
|
|
||||||
await this.dbInstances[name].close();
|
await this.dbInstances[name].close();
|
||||||
|
|
||||||
const db = this.dbInstances[name];
|
|
||||||
delete this.dbInstances[name];
|
delete this.dbInstances[name];
|
||||||
|
}
|
||||||
|
|
||||||
if (db instanceof SQLiteDBMock) {
|
await this.deleteDatabase(name);
|
||||||
// In WebSQL we cannot delete the database, just empty it.
|
}
|
||||||
return db.emptyDatabase();
|
|
||||||
} else {
|
/**
|
||||||
return SQLite.deleteDatabase({
|
* Delete database.
|
||||||
|
*
|
||||||
|
* @param name Database name.
|
||||||
|
*/
|
||||||
|
protected async deleteDatabase(name: string): Promise<void> {
|
||||||
|
await SQLite.deleteDatabase({
|
||||||
name,
|
name,
|
||||||
location: 'default',
|
location: 'default',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (CorePlatform.is('cordova')) {
|
|
||||||
return SQLite.deleteDatabase({
|
/**
|
||||||
name,
|
* Get database spy methods to intercept database calls and track logging information.
|
||||||
location: 'default',
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue