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