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

450 lines • 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require("lodash"); const Promise = require("bluebird"); const TypedError = require("typed-error"); const env = require("../config-loader/env"); const { DEBUG } = process.env; const isSqlError = (value) => { return value != null && value.constructor != null && value.constructor.name === 'SQLError'; }; class DatabaseError extends TypedError { constructor(message) { if (isSqlError(message)) { super(message.message); } else { super(message); } if (message != null && !_.isString(message) && 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 NotADatabaseError = (err) => !(err instanceof DatabaseError); const alwaysExport = { DatabaseError, ConstraintError, UniqueConstraintError, ForeignKeyConstraintError, }; exports.engines = {}; const atomicExecuteSql = function (sql, bindings) { return this.transaction((tx) => tx.executeSql(sql, bindings)); }; const tryFn = (fn) => { Promise.try(fn); }; let timeoutMS; if (process.env.TRANSACTION_TIMEOUT_MS) { timeoutMS = _.parseInt(process.env.TRANSACTION_TIMEOUT_MS); if (_.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: () => Promise.reject(rejectionValue), rollback: () => Promise.reject(rejectionValue), }; } : (message) => { const rejectFn = () => Promise.reject(new Error(message)); return { executeSql: rejectFn, rollback: rejectFn, }; }; 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(); return this._executeSql(sql, bindings, ...args) .finally(() => this.decrementPending()) .catch(NotADatabaseError, (err) => { throw new DatabaseError(err); }); } rollback() { const promise = this._rollback().finally(() => { this.listeners.rollback.forEach(tryFn); return null; }); this.closeTransaction('Transaction has been rolled back.'); return promise; } end() { const promise = this._commit().tap(() => { this.listeners.end.forEach(tryFn); return null; }); this.closeTransaction('Transaction has been ended.'); return promise; } on(name, fn) { this.listeners[name].push(fn); } dropTable(tableName, ifExists = true) { if (!_.isString(tableName)) { return Promise.reject(new TypeError('"tableName" must be a string')); } if (_.includes(tableName, '"')) { return Promise.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) => { function transaction(fn) { const stackTraceErr = getStackTraceErr(); return new Promise((resolve, reject, onCancel) => { if (onCancel) { onCancel(() => { promise.call('rollback'); }); } let promise = createFunc(stackTraceErr); if (fn) { promise.tap((tx) => Promise.try(() => fn(tx)) .tap(() => tx.end()).tapCatch(() => tx.rollback()) .then(resolve) .catch(reject)); } else { promise .then(resolve) .catch(reject); } }); } return transaction; }; 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 (_.isString(connectString)) { const pgConnectionString = require('pg-connection-string'); config = pgConnectionString.parse(connectString); } else { config = connectString; } config.Promise = Promise; config.max = env.db.poolSize; config.idleTimeoutMillis = env.db.idleTimeoutMillis; 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}"` }); }); } const connect = Promise.promisify(pool.connect, { context: pool }); const createResult = ({ rowCount, rows }) => { return { rows, rowsAffected: rowCount, insertId: _.get(rows, [0, 'id']), }; }; class PostgresTx extends Tx { constructor(db, close, stackTraceErr) { super(stackTraceErr); this.db = db; this.close = close; } _executeSql(sql, bindings, addReturning = false) { if (addReturning && /^\s*INSERT\s+INTO/i.test(sql)) { sql = sql.replace(/;?$/, ' RETURNING "' + addReturning + '";'); } return Promise.fromCallback((callback) => { this.db.query({ text: sql, values: bindings }, callback); }).catch({ code: PG_UNIQUE_VIOLATION }, (err) => { throw new UniqueConstraintError(err); }).catch({ code: PG_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 * FROM ( SELECT tablename as name FROM pg_tables WHERE schemaname = 'public' ) t ${extraWhereClause}; `); } } return _.extend({ engine: 'postgres', executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => connect() .then((client) => { const tx = new PostgresTx(client, client.release, stackTraceErr); tx.executeSql('START TRANSACTION;'); 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 connect = Promise.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 Promise.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 _.extend({ engine: 'mysql', executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => connect() .then((client) => { const close = () => client.release(); const tx = new MySqlTx(client, close, stackTraceErr); 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 rows = _.times(result.rows.length, (i) => { return result.rows.item(i); }); return { rows: 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 Promise((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 Promise((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 Promise.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 _.extend({ engine: 'websql', executeSql: atomicExecuteSql, transaction: createTransaction((stackTraceErr) => new Promise((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