UNPKG

sqlite3orm

Version:

ORM for sqlite3 and TypeScript/JavaScript

563 lines 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqlDatabase = exports.SQL_OPEN_DEFAULT = exports.SQL_OPEN_DEFAULT_NO_URI = exports.SQL_OPEN_DEFAULT_URI = exports.SQL_MEMORY_DB_SHARED = exports.SQL_MEMORY_DB_PRIVATE = exports.SQL_DEFAULT_SCHEMA = exports.SQL_OPEN_PRIVATECACHE = exports.SQL_OPEN_SHAREDCACHE = exports.SQL_OPEN_URI = exports.SQL_OPEN_CREATE = exports.SQL_OPEN_READWRITE = exports.SQL_OPEN_READONLY = void 0; const tslib_1 = require("tslib"); /* eslint-disable @typescript-eslint/no-explicit-any */ const _dbg = tslib_1.__importStar(require("debug")); const sqlite3_1 = require("sqlite3"); const SqlBackup_1 = require("./SqlBackup"); const SqlStatement_1 = require("./SqlStatement"); exports.SQL_OPEN_READONLY = sqlite3_1.OPEN_READONLY; exports.SQL_OPEN_READWRITE = sqlite3_1.OPEN_READWRITE; exports.SQL_OPEN_CREATE = sqlite3_1.OPEN_CREATE; // introduced by https://github.com/mapbox/node-sqlite3/pull/1078 exports.SQL_OPEN_URI = sqlite3_1.OPEN_URI; exports.SQL_OPEN_SHAREDCACHE = sqlite3_1.OPEN_SHAREDCACHE; exports.SQL_OPEN_PRIVATECACHE = sqlite3_1.OPEN_PRIVATECACHE; exports.SQL_DEFAULT_SCHEMA = 'main'; // see https://www.sqlite.org/inmemorydb.html exports.SQL_MEMORY_DB_PRIVATE = ':memory:'; exports.SQL_MEMORY_DB_SHARED = 'file:sqlite3orm?mode=memory&cache=shared'; const debug = _dbg.default('sqlite3orm:database'); exports.SQL_OPEN_DEFAULT_URI = exports.SQL_OPEN_READWRITE | exports.SQL_OPEN_CREATE | exports.SQL_OPEN_URI; exports.SQL_OPEN_DEFAULT_NO_URI = exports.SQL_OPEN_READWRITE | exports.SQL_OPEN_CREATE; exports.SQL_OPEN_DEFAULT = exports.SQL_OPEN_DEFAULT_NO_URI; // TODO: Breaking Change: change to 'SQL_OPEN_DEFAULT_URI' /** * A thin wrapper for the 'Database' class from 'node-sqlite3' using Promises * instead of callbacks * see * https://github.com/mapbox/node-sqlite3/wiki/API * * see why we may want to have a connection pool running on nodejs serving multiple requests * https://github.com/mapbox/node-sqlite3/issues/304 * * @export * @class SqlDatabase */ class SqlDatabase { static lastId = 0; db; dbId; databaseFile; dirty; /** * Open a database connection * * @param databaseFile - The path to the database file or URI * @param [mode=SQL_OPEN_DEFAULT] - A bit flag combination of: SQL_OPEN_CREATE | * SQL_OPEN_READONLY | SQL_OPEN_READWRITE * @returns A promise */ open(databaseFile, mode, settings) { return new Promise((resolve, reject) => { const db = new sqlite3_1.Database(databaseFile, mode || exports.SQL_OPEN_DEFAULT, (err) => { if (err) { debug(`opening connection to ${databaseFile} failed: ${err.message}`); reject(err); } else { this.db = db; this.dbId = SqlDatabase.lastId++; this.databaseFile = databaseFile; debug(`${this.dbId}: opened`); resolve(); } }); }).then(() => { if (settings) { return this.applySettings(settings); } return Promise.resolve(); }); } /** * Close the database connection * * @returns {Promise<void>} */ close() { return new Promise((resolve, reject) => { if (!this.db) { resolve(); } else { const db = this.db; debug(`${this.dbId}: close`); this.db = undefined; this.dbId = undefined; this.databaseFile = undefined; db.close((err) => { db.removeAllListeners(); /* istanbul ignore if */ if (err) { debug(`closing connection failed: ${err.message}`); reject(err); } else { resolve(); } }); } }); } /** * Test if a connection is open * * @returns {boolean} */ isOpen() { return !!this.db; } /** * Runs a SQL statement with the specified parameters * * @param sql - The SQL statment * @param [params] - The parameters referenced in the statement; you can * provide multiple parameters as array * @returns A promise */ run(sql, params) { return new Promise((resolve, reject) => { // trace('run stmt=' + sql); // trace('>input: ' + JSON.stringify(params)); /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; this.db.run(sql, params, function (err) { // do not use arrow function for this callback // the below 'this' does not reference ourself if (err) { debug(`${self.dbId}: failed sql: ${err.message} ${sql}\nparams: `, params); reject(err); } else { const res = { lastID: this.lastID, changes: this.changes, }; resolve(res); } }); }); } /** * Runs a SQL query with the specified parameters, fetching only the first row * * @param sql - The DQL statement * @param [params] - The parameters referenced in the statement; you can * provide multiple parameters as array * @returns A promise */ get(sql, params) { return new Promise((resolve, reject) => { // trace('get stmt=' + sql); // trace('>input: ' + JSON.stringify(params)); /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); this.db.get(sql, params, (err, row) => { if (err) { debug(`${this.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { // trace('>succeeded: ' + JSON.stringify(row)); resolve(row); } }); }); } /** * Runs a SQL query with the specified parameters, fetching all rows * * @param sql - The DQL statement * @param [params] - The parameters referenced in the statement; you can * provide multiple parameters as array * @returns A promise */ all(sql, params) { return new Promise((resolve, reject) => { // trace('all stmt=' + sql); // trace('>input: ' + JSON.stringify(params)); /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); this.db.all(sql, params, (err, rows) => { if (err) { debug(`${this.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { // trace('>succeeded: ' + JSON.stringify(rows)); resolve(rows); } }); }); } /** * Runs a SQL query with the specified parameters, fetching all rows * using a callback for each row * * @param sql - The DQL statement * @param [params] - The parameters referenced in the statement; you can * provide multiple parameters as array * @param [callback] - The callback function * @returns A promise */ each(sql, params, callback) { return new Promise((resolve, reject) => { /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); this.db.each(sql, params, callback, (err, count) => { if (err) { debug(`${this.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { resolve(count); } }); }); } /** * Execute a SQL statement * * @param sql - The SQL statement * @returns A promise */ exec(sql) { return new Promise((resolve, reject) => { // trace('exec stmt=' + sql); /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); this.db.exec(sql, (err) => { if (err) { debug(`${this.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { resolve(); } }); }); } /** * Prepare a SQL statement * * @param sql - The SQL statement * @param [params] - The parameters referenced in the statement; you can * provide multiple parameters as array * @returns A promise */ prepare(sql, params) { return new Promise((resolve, reject) => { /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); const dbstmt = this.db.prepare(sql, params, (err) => { if (err) { debug(`${this.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { resolve(new SqlStatement_1.SqlStatement(dbstmt)); } }); }); } /** * serialized sqlite3 calls * if callback is provided, run callback in serialized mode * otherwise, switch connection to serialized mode * * @param [callback] */ serialize(callback) { /* istanbul ignore if */ if (!this.db) { throw new Error('database connection not open'); } return this.db.serialize(callback); } /** * parallelized sqlite3 calls * if callback is provided, run callback in parallel mode * otherwise, switch connection to parallel mode * * @param [callback] */ parallelize(callback) { /* istanbul ignore if */ if (!this.db) { throw new Error('database connection not open'); } return this.db.parallelize(callback); } /** * Run callback inside a database transaction * * @param [callback] */ transactionalize(callback) { return this.beginTransaction() .then(callback) .then((res) => this.commitTransaction().then(() => Promise.resolve(res))) .catch((err) => this.rollbackTransaction().then(() => Promise.reject(err))); } beginTransaction() { return this.run('BEGIN IMMEDIATE TRANSACTION'); } commitTransaction() { return this.run('COMMIT TRANSACTION'); } rollbackTransaction() { return this.run('ROLLBACK TRANSACTION'); } endTransaction(commit) { // TODO: node-sqlite3 does not yet support `sqlite3_txn_state` // please see https://www.sqlite.org/draft/c3ref/txn_state.html // we would need this do test if a transaction is open // without this we have to manually ignore 'no transaction' errors const sql = commit ? `COMMIT TRANSACTION` : `ROLLBACK TRANSACTION`; return new Promise((resolve, reject) => { if (!this.db) { reject(new Error('database connection not open')); return; } debug(`${this.dbId}: sql: ${sql}`); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; this.db.run(sql, undefined, function (err) { // do not use arrow function for this callback // the below 'this' does not reference ourself /* istanbul ignore if */ if (err && !err.message.includes('no transaction')) { debug(`${self.dbId}: failed sql: ${err.message} ${sql}`); reject(err); } else { resolve(); } }); }); } /** * initiate online backup * * @param database - the database file to backup from or to * @param databaseIsDestination - if the provided database parameter is source or destination of the backup * @param destName - the destination name * @param sourceName - the source name * @returns A promise */ backup(database, /* | SqlDatabase */ databaseIsDestination = true, destName = 'main', sourceName = 'main') { return new Promise((resolve, reject) => { /* istanbul ignore if */ if (!this.db) { reject(new Error('database connection not open')); return; } // TODO(Backup API): typings not yet available const db = this.db; const backup = db.backup(database, // TODO: jsdoc for Database#backup seems to be wrong; `sourceName` and` destName` are probably swapped // please see upstream issue: https://github.com/mapbox/node-sqlite3/issues/1482#issuecomment-903233196 // swapping again: destName, sourceName, databaseIsDestination, (err) => { /* istanbul ignore if */ if (err) { debug(`${this.dbId}: backup init failed for '${database}': ${err.message}`); reject(err); } else { resolve(new SqlBackup_1.SqlBackup(backup)); } }); }); } /** * @param event * @param listener */ on(event, listener) { /* istanbul ignore if */ if (!this.db) { throw new Error('database connection not open'); } this.db.on(event, listener); return this; } /** * Get the 'user_version' from the database * @returns A promise of the user version number */ getUserVersion() { return this.get('PRAGMA user_version').then((res) => res.user_version); } /** * Set the 'user_version' in the database * * @param newver * @returns A promise */ setUserVersion(newver) { return this.exec(`PRAGMA user_version = ${newver}`); } /** * Get the 'cipher_version' from the database * @returns A promise of the cipher version */ getCipherVersion() { return this.get('PRAGMA cipher_version').then((res) => ( /* istanbul ignore next */res ? res.cipher_version : undefined)); } applySettings(settings) { /* istanbul ignore if */ if (!this.db) { return Promise.reject(new Error('database connection not open')); } const promises = []; try { /* istanbul ignore if */ if (settings.cipherCompatibility) { this._addPragmaSetting(promises, 'cipher_compatibility', settings.cipherCompatibility); } /* istanbul ignore if */ if (settings.key) { this._addPragmaSetting(promises, 'key', settings.key); } /* istanbul ignore else */ if (settings.journalMode) { this._addPragmaSchemaSettings(promises, 'journal_mode', settings.journalMode); } /* istanbul ignore else */ if (settings.busyTimeout) { this._addPragmaSetting(promises, 'busy_timeout', settings.busyTimeout); } /* istanbul ignore else */ if (settings.synchronous) { this._addPragmaSchemaSettings(promises, 'synchronous', settings.synchronous); } /* istanbul ignore else */ if (settings.caseSensitiveLike) { this._addPragmaSetting(promises, 'case_sensitive_like', settings.caseSensitiveLike); } /* istanbul ignore else */ if (settings.foreignKeys) { this._addPragmaSetting(promises, 'foreign_keys', settings.foreignKeys); } /* istanbul ignore else */ if (settings.ignoreCheckConstraints) { this._addPragmaSetting(promises, 'ignore_check_constraints', settings.ignoreCheckConstraints); } /* istanbul ignore else */ if (settings.queryOnly) { this._addPragmaSetting(promises, 'query_only', settings.queryOnly); } /* istanbul ignore else */ if (settings.readUncommitted) { this._addPragmaSetting(promises, 'read_uncommitted', settings.readUncommitted); } /* istanbul ignore else */ if (settings.recursiveTriggers) { this._addPragmaSetting(promises, 'recursive_triggers', settings.recursiveTriggers); } /* istanbul ignore else */ if (settings.secureDelete) { this._addPragmaSchemaSettings(promises, 'secure_delete', settings.secureDelete); } if (settings.executionMode) { switch (settings.executionMode.toUpperCase()) { case 'SERIALIZE': this.serialize(); break; case 'PARALLELIZE': this.parallelize(); break; default: throw new Error(`failed to read executionMode setting: ${settings.executionMode.toString()}`); } } else { this.parallelize(); } } catch (err) { return Promise.reject(err); } if (promises.length) { return Promise.all(promises).then(() => { }); } return Promise.resolve(); } _addPragmaSchemaSettings(promises, pragma, setting) { if (Array.isArray(setting)) { setting.forEach((val) => { this._addPragmaSetting(promises, pragma, val, true); }); } else { this._addPragmaSetting(promises, pragma, setting, true); } } _addPragmaSetting(promises, pragma, setting, schemaSupport = false) { if (typeof setting === 'number') { promises.push(this.exec(`PRAGMA ${pragma} = ${setting}`)); return; } if (schemaSupport) { const splitted = setting.split('.'); switch (splitted.length) { case 1: promises.push(this.exec(`PRAGMA ${pragma} = ${setting.toUpperCase()}`)); return; case 2: promises.push(this.exec(`PRAGMA ${splitted[0]}.${pragma} = ${splitted[1].toUpperCase()}`)); return; } throw new Error(`failed to read ${pragma} setting: ${setting.toString()}`); } else { promises.push(this.exec(`PRAGMA ${pragma} = ${setting}`)); } } /** * Set the execution mode to verbose to produce long stack traces. There is no way to reset this. * See https://github.com/mapbox/node-sqlite3/wiki/Debugging * * @param newver */ static verbose() { (0, sqlite3_1.verbose)(); } } exports.SqlDatabase = SqlDatabase; //# sourceMappingURL=SqlDatabase.js.map