UNPKG

@resin/pinejs

Version:

Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make

545 lines • 18.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.connect = exports.Tx = exports.engines = exports.ForeignKeyConstraintError = exports.UniqueConstraintError = exports.ConstraintError = exports.DatabaseError = exports.metrics = void 0; const Bluebird = require("bluebird"); const EventEmitter = require("eventemitter3"); const _ = require("lodash"); const typed_error_1 = require("typed-error"); const env = require("../config-loader/env"); exports.metrics = new EventEmitter(); const { DEBUG } = process.env; const isSqlError = (value) => { return (value != null && value.constructor != null && value.constructor.name === 'SQLError'); }; class DatabaseError extends typed_error_1.TypedError { constructor(message) { if (isSqlError(message)) { super(message.message); } else { super(message); } if (message != null && typeof message !== 'string' && message.code != null) { this.code = message.code; } } } exports.DatabaseError = DatabaseError; class ConstraintError extends DatabaseError { } exports.ConstraintError = ConstraintError; class UniqueConstraintError extends ConstraintError { } exports.UniqueConstraintError = UniqueConstraintError; class ForeignKeyConstraintError extends ConstraintError { } exports.ForeignKeyConstraintError = ForeignKeyConstraintError; const wrapDatabaseError = (err) => { exports.metrics.emit('db_error', err); if (!(err instanceof DatabaseError)) { throw new DatabaseError(err); } throw err; }; const alwaysExport = { DatabaseError, ConstraintError, UniqueConstraintError, ForeignKeyConstraintError, }; exports.engines = {}; const atomicExecuteSql = function (sql, bindings) { return this.transaction((tx) => tx.executeSql(sql, bindings)); }; const asyncTryFn = (fn) => { Bluebird.resolve().then(fn); }; let timeoutMS; if (process.env.TRANSACTION_TIMEOUT_MS) { timeoutMS = parseInt(process.env.TRANSACTION_TIMEOUT_MS, 10); if (Number.isNaN(timeoutMS) || timeoutMS <= 0) { throw new Error(`Invalid valid for TRANSACTION_TIMEOUT_MS: ${process.env.TRANSACTION_TIMEOUT_MS}`); } } else { timeoutMS = 10000; } const getRejectedFunctions = DEBUG ? (message) => { const rejectionValue = new Error(message); return { executeSql: () => Bluebird.reject(rejectionValue), rollback: () => Bluebird.reject(rejectionValue), }; } : (message) => { const rejectFn = () => Bluebird.reject(new Error(message)); return { executeSql: rejectFn, rollback: rejectFn, }; }; const onEnd = (name, fn) => { if (name === 'end') { asyncTryFn(fn); } }; const onRollback = (name, fn) => { if (name === 'rollback') { asyncTryFn(fn); } }; class Tx { constructor(stackTraceErr) { this.pending = 0; this.listeners = { end: [], rollback: [], }; this.automaticClose = () => { console.error('Transaction still open after ' + timeoutMS + 'ms without an execute call.'); if (stackTraceErr) { console.error(stackTraceErr.stack); } this.rollback(); }; this.automaticCloseTimeout = setTimeout(this.automaticClose, timeoutMS); } incrementPending() { if (this.pending === false) { return; } this.pending++; clearTimeout(this.automaticCloseTimeout); } decrementPending() { if (this.pending === false) { return; } this.pending--; if (this.pending === 0) { this.automaticCloseTimeout = setTimeout(this.automaticClose, timeoutMS); } else if (this.pending < 0) { console.error('Pending transactions is less than 0, wtf?'); this.pending = 0; } } cancelPending() { this.pending = false; clearTimeout(this.automaticCloseTimeout); } closeTransaction(message) { this.cancelPending(); const { executeSql, rollback } = getRejectedFunctions(message); this.executeSql = executeSql; this.rollback = this.end = rollback; } executeSql(sql, bindings = [], ...args) { this.incrementPending(); const t0 = Date.now(); return this._executeSql(sql, bindings, ...args) .finally(() => { this.decrementPending(); const queryTime = Date.now() - t0; exports.metrics.emit('db_query_time', { queryTime, queryType: sql.split(' ', 1)[0], }); }) .catch(wrapDatabaseError); } rollback() { const promise = this._rollback().finally(() => { this.listeners.rollback.forEach(asyncTryFn); this.on = onRollback; this.clearListeners(); return null; }); this.closeTransaction('Transaction has been rolled back.'); return promise; } end() { const promise = this._commit().tap(() => { this.listeners.end.forEach(asyncTryFn); this.on = onEnd; this.clearListeners(); return null; }); this.closeTransaction('Transaction has been ended.'); return promise; } on(name, fn) { this.listeners[name].push(fn); } clearListeners() { this.listeners.end.length = 0; this.listeners.rollback.length = 0; } dropTable(tableName, ifExists = true) { if (typeof tableName !== 'string') { return Bluebird.reject(new TypeError('"tableName" must be a string')); } if (tableName.includes('"')) { return Bluebird.reject(new TypeError('"tableName" cannot include double quotes')); } const ifExistsStr = ifExists === true ? ' IF EXISTS' : ''; return this.executeSql(`DROP TABLE${ifExistsStr} "${tableName}";`); } } exports.Tx = Tx; const getStackTraceErr = DEBUG ? () => new Error() : _.noop; const createTransaction = (createFunc) => { return (fn) => { const stackTraceErr = getStackTraceErr(); return new Bluebird((resolve, reject, onCancel) => { if (onCancel) { onCancel(() => { promise.call('rollback'); }); } const promise = createFunc(stackTraceErr); if (fn) { promise .tap((tx) => Bluebird.try(() => fn(tx)) .tap(() => tx.end()) .tapCatch(() => tx.rollback()) .then(resolve)) .catch(reject); } else { promise.then(resolve).catch(reject); } }); }; }; let maybePg; try { maybePg = require('pg'); } catch (e) { } if (maybePg != null) { const pg = maybePg; exports.engines.postgres = (connectString) => { const PG_UNIQUE_VIOLATION = '23505'; const PG_FOREIGN_KEY_VIOLATION = '23503'; let config; if (typeof connectString === 'string') { const pgConnectionString = require('pg-connection-string'); config = pgConnectionString.parse(connectString); } else { config = connectString; } config.Promise = Bluebird; config.max = env.db.poolSize; config.idleTimeoutMillis = env.db.idleTimeoutMillis; config.connectionTimeoutMillis = env.db.connectionTimeoutMillis; const pool = new pg.Pool(config); const { PG_SCHEMA } = process.env; if (PG_SCHEMA != null) { pool.on('connect', (client) => { client.query({ text: `SET search_path TO "${PG_SCHEMA}"` }); }); pool.on('error', (err) => { console.error('Pool error:', err.message); }); } const checkPgErrCode = (err) => { if (err.code === PG_UNIQUE_VIOLATION) { throw new UniqueConstraintError(err); } if (err.code === PG_FOREIGN_KEY_VIOLATION) { throw new ForeignKeyConstraintError(err); } throw err; }; const createResult = ({ rowCount, rows, }) => { var _a; return { rows, rowsAffected: rowCount, insertId: (_a = rows === null || rows === void 0 ? void 0 : rows[0]) === null || _a === void 0 ? void 0 : _a.id, }; }; class PostgresTx extends Tx { constructor(db, stackTraceErr) { super(stackTraceErr); this.db = db; } _executeSql(sql, bindings, addReturning = false) { if (addReturning && /^\s*INSERT\s+INTO/i.test(sql)) { sql = sql.replace(/;?$/, ' RETURNING "' + addReturning + '";'); } return this.db .query({ text: sql, values: bindings, }) .catch(checkPgErrCode) .then(createResult); } _rollback() { return this.executeSql('ROLLBACK;') .then(() => { this.db.release(); }) .tapCatch((err) => { this.db.release(err); }); } _commit() { return this.executeSql('COMMIT;') .then(() => { this.db.release(); }) .tapCatch((err) => { this.db.release(err); }); } tableList(extraWhereClause = '') { if (extraWhereClause !== '') { extraWhereClause = 'WHERE ' + extraWhereClause; } return this.executeSql(` SELECT * FROM ( SELECT tablename as name FROM pg_tables WHERE schemaname = 'public' ) t ${extraWhereClause}; `); } } return { engine: "postgres", executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => pool.connect().then((client) => { const tx = new PostgresTx(client, stackTraceErr); tx.executeSql('START TRANSACTION;'); return tx; })), readTransaction: createTransaction((stackTraceErr) => pool.connect().then((client) => { const tx = new PostgresTx(client, stackTraceErr); tx.executeSql('START TRANSACTION;'); tx.executeSql('SET TRANSACTION READ ONLY;'); return tx; })), ...alwaysExport, }; }; } let maybeMysql; try { maybeMysql = require('mysql'); } catch (e) { } if (maybeMysql != null) { const mysql = maybeMysql; exports.engines.mysql = (options) => { const MYSQL_UNIQUE_VIOLATION = 'ER_DUP_ENTRY'; const MYSQL_FOREIGN_KEY_VIOLATION = 'ER_ROW_IS_REFERENCED'; const pool = mysql.createPool(options); pool.on('connection', (db) => { db.query("SET sql_mode='ANSI_QUOTES';"); }); const getConnectionAsync = Bluebird.promisify(pool.getConnection, { context: pool, }); const createResult = (rows) => { return { rows, rowsAffected: rows.affectedRows, insertId: rows.insertId, }; }; class MySqlTx extends Tx { constructor(db, close, stackTraceErr) { super(stackTraceErr); this.db = db; this.close = close; } _executeSql(sql, bindings) { return Bluebird.fromCallback((callback) => { this.db.query(sql, bindings, callback); }) .catch({ code: MYSQL_UNIQUE_VIOLATION }, (err) => { throw new UniqueConstraintError(err); }) .catch({ code: MYSQL_FOREIGN_KEY_VIOLATION }, (err) => { throw new ForeignKeyConstraintError(err); }) .then(createResult); } _rollback() { const promise = this.executeSql('ROLLBACK;'); this.close(); return promise.return(); } _commit() { const promise = this.executeSql('COMMIT;'); this.close(); return promise.return(); } tableList(extraWhereClause = '') { if (extraWhereClause !== '') { extraWhereClause = ' WHERE ' + extraWhereClause; } return this.executeSql(` SELECT name FROM ( SELECT table_name AS name FROM information_schema.tables WHERE table_schema = ? ) t ${extraWhereClause}; `, [options.database]); } } return { engine: "mysql", executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => getConnectionAsync().then((client) => { const close = () => client.release(); const tx = new MySqlTx(client, close, stackTraceErr); tx.executeSql('START TRANSACTION;'); return tx; })), readTransaction: createTransaction((stackTraceErr) => getConnectionAsync().then((client) => { const close = () => client.release(); const tx = new MySqlTx(client, close, stackTraceErr); tx.executeSql('SET TRANSACTION READ ONLY;'); tx.executeSql('START TRANSACTION;'); return tx; })), ...alwaysExport, }; }; } if (typeof window !== 'undefined' && window.openDatabase != null) { exports.engines.websql = (databaseName) => { const WEBSQL_CONSTRAINT_ERR = 6; const db = window.openDatabase(databaseName, '1.0', 'rulemotion', 2 * 1024 * 1024); const getInsertId = (result) => { try { return result.insertId; } catch (e) { } }; const createResult = (result) => { const { length } = result.rows; const rows = Array(length); for (let i = 0; i < length; i++) { rows[i] = result.rows.item(i); } return { rows, rowsAffected: result.rowsAffected, insertId: getInsertId(result), }; }; class WebSqlTx extends Tx { constructor(tx, stackTraceErr) { super(stackTraceErr); this.tx = tx; this.running = true; this.queue = []; this.asyncRecurse = () => { let args; while ((args = this.queue.pop())) { console.debug('Running', args[0]); this.tx.executeSql(args[0], args[1], args[2], args[3]); } if (this.running) { console.debug('Looping'); this.tx.executeSql('SELECT 0', [], this.asyncRecurse); } }; this.asyncRecurse(); } _executeSql(sql, bindings) { return new Bluebird((resolve, reject) => { const successCallback = (_tx, results) => { resolve(results); }; const errorCallback = (_tx, err) => { reject(err); return false; }; this.queue.push([sql, bindings, successCallback, errorCallback]); }) .catch({ code: WEBSQL_CONSTRAINT_ERR }, () => { throw new ConstraintError('Constraint failed.'); }) .then(createResult); } _rollback() { return new Bluebird((resolve) => { const successCallback = () => { resolve(); throw new Error('Rollback'); }; const errorCallback = () => { resolve(); return true; }; this.queue = [ [ 'RUN A FAILING STATEMENT TO ROLLBACK', [], successCallback, errorCallback, ], ]; this.running = false; }); } _commit() { this.running = false; return Bluebird.resolve(); } tableList(extraWhereClause = '') { if (extraWhereClause !== '') { extraWhereClause = ' AND ' + extraWhereClause; } return this.executeSql(` SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT IN ( '__WebKitDatabaseInfoTable__', 'sqlite_sequence' ) ${extraWhereClause}; `); } } return { engine: "websql", executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => new Bluebird((resolve) => { db.transaction((tx) => { resolve(new WebSqlTx(tx, stackTraceErr)); }); })), ...alwaysExport, }; }; } exports.connect = (databaseOptions) => { if (exports.engines[databaseOptions.engine] == null) { throw new Error('Unsupported database engine: ' + databaseOptions.engine); } return exports.engines[databaseOptions.engine](databaseOptions.params); }; //# sourceMappingURL=db.js.map