@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
JavaScript
"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