sqlite3orm
Version:
ORM for sqlite3 and TypeScript/JavaScript
563 lines • 20.4 kB
JavaScript
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
;