UNPKG

carthage

Version:

An API Server and Framework with scaffold functions for node.js

738 lines (556 loc) 17.1 kB
'use strict'; const inflect = require('i')(); const SQLAdapter = require('../sql_adapter.js'); const utilities = require('../../utilities.js'); const async = require('async'); const pg = require('pg'); pg.defaults.poolSize = 8; class PostgresAdapter extends SQLAdapter { constructor(db, cfg) { super(); cfg = cfg.connectionString ? this.parseConnectionString(cfg.connectionString) : cfg; this.db = db; this._config = cfg; } close() { pg.end(); } query(query, params, callback) { if (arguments.length < 3) { throw new Error('.query requires 3 arguments'); } if (!(params instanceof Array)) { throw new Error('params must be a valid array'); } if(typeof callback !== 'function') { throw new Error('Callback must be a function'); } let start = new Date().valueOf(); let log = this.db.log.bind(this.db); pg.connect(this._config, (err, client, complete) => { if (err) { this.db.error(err.message); return complete(); } client.query(query, params, (function () { log(query, params, new Date().valueOf() - start); complete(); callback.apply(this, arguments); }).bind(this)); }); return true; } transaction(preparedArray, callback) { if (!preparedArray.length) { throw new Error('Must give valid array of statements (with or without parameters)'); } if (typeof preparedArray === 'string') { preparedArray = preparedArray.split(';').filter(function(v) { return !!v; }).map(function(v) { return [v]; }); } if(typeof callback !== 'function') { callback = function() {}; } let start = new Date().valueOf(); pg.connect(this._config, (err, client, complete) => { if (err) { this.db.error(err.message); callback(err); return complete(); } let queries = preparedArray.map(queryData => { let query = queryData[0]; let params = queryData[1] || []; return (callback) => { this.db.log(query, params, new Date().valueOf() - start); client.query(queryData[0], queryData[1], callback); }; }); queries = [].concat( (callback) => { client.query('BEGIN', callback); }, queries ); this.db.info('Transaction started...'); async.series(queries, (txnErr, results) => { if (txnErr) { this.db.error(txnErr.message); this.db.info('Rollback started...'); client.query('ROLLBACK', (err) => { if (err) { this.db.error(`Rollback failed - ${err.message}`); this.db.info('Transaction complete!'); complete(); callback(err); } else { this.db.info('Rollback complete!') this.db.info('Transaction complete!'); complete(); callback(txnErr); }; }); } else { this.db.info('Commit started...'); client.query('COMMIT', (err) => { if (err) { this.db.error(`Commit failed - ${err.message}`); this.db.info('Transaction complete!'); complete(); callback(err); return; } this.db.info('Commit complete!') this.db.info('Transaction complete!'); complete(); callback(null, results); }); } }); }); } /* Command functions... */ drop(databaseName, callback) { this.query(this.generateDropDatabaseQuery(databaseName), [], (err, result) => { if (err) { return callback(err); } this.db.info(`Dropped database "${databaseName}"`); callback(null); }); } create(databaseName, callback) { this.query(this.generateCreateDatabaseQuery(databaseName), [], (err, result) => { if (err) { return callback(err); } this.db.info(`Created empty database "${databaseName}"`); callback(null); }); } /* generate functions */ generateArray(arr) { return '{' + arr.join(',') + '}'; } generateConnectionString(host, port, database, user, password) { if (!host || !port || !database) { return ''; } return 'postgres://' + user + ':' + password + '@' + host + ':' + port + '/' + database; } parseConnectionString(str) { let cfg = { host: '', database: '', user: '', password: '', port: 5432, ssl: false }; let match = str.match(/^postgres:\/\/([A-Za-z0-9_]+)(?:\:([A-Za-z0-9_\-]+))?@([A-Za-z0-9_\.\-]+):(\d+)\/([A-Za-z0-9_]+)$/); if (match) { cfg.user = match[1]; cfg.password = match[2]; cfg.host = match[3]; cfg.port = match[4]; cfg.database = match[5]; } return cfg; } generateClearDatabaseQuery() { return [ 'DROP SCHEMA public CASCADE', 'CREATE SCHEMA public' ].join(';') } generateCreateDatabaseQuery(name) { return [ 'CREATE DATABASE', this.escapeField(name) ].join(' '); } generateDropDatabaseQuery(name) { return [ 'DROP DATABASE IF EXISTS', this.escapeField(name) ].join(' '); } generateColumn(columnName, columnType, columnProperties) { return [ this.escapeField(columnName), columnType, columnProperties.array ? 'ARRAY' : '', (columnProperties.primary_key || !columnProperties.nullable) ? 'NOT NULL' : '' ].filter(function(v) { return !!v; }).join(' '); } generateAlterColumn(columnName, columnType, columnProperties) { return [ 'ALTER COLUMN', this.escapeField(columnName), 'TYPE', columnType, columnProperties.array ? 'ARRAY' : '', ].filter(function(v) { return !!v; }).join(' '); } generateAlterColumnSetNull(columnName, columnType, columnProperties) { return [ 'ALTER COLUMN', this.escapeField(columnName), (columnProperties.primary_key || !columnProperties.nullable) ? 'SET' : 'DROP', 'NOT NULL' ].join(' '); } generateAlterColumnDropDefault(columnName, columnType, columnProperties) { return [ 'ALTER COLUMN', this.escapeField(columnName), 'DROP DEFAULT' ].join(' '); } generateAlterColumnSetDefaultSeq(columnName, seqName) { return [ 'ALTER COLUMN ', this.escapeField(columnName), ' SET DEFAULT nextval(\'', seqName, '\')' ].join(''); } generateIndex(table, columnName) { return this.generateConstraint(table, columnName, 'index'); } generateConstraint(table, columnName, suffix) { return this.escapeField([table, columnName, suffix].join('_')); } generatePrimaryKey(table, columnName) { return ['CONSTRAINT ', this.generateConstraint(table, columnName, 'pk'), ' PRIMARY KEY(', this.escapeField(columnName), ')'].join(''); } generateUniqueKey(table, columnName) { return ['CONSTRAINT ', this.generateConstraint(table, columnName, 'unique'), ' UNIQUE(', this.escapeField(columnName), ')'].join(''); } generateAlterTableRename(table, newTableName, columns) { let self = this; return [ [ 'ALTER TABLE', this.escapeField(table), 'RENAME TO', this.escapeField(newTableName) ].join(' '), ].concat( this.getPrimaryKeys(columns).map(function(columnData) { return [ 'ALTER TABLE', self.escapeField(newTableName), 'RENAME CONSTRAINT', self.generateConstraint(table, columnData.name, 'pk'), 'TO', self.generateConstraint(newTableName, columnData.name, 'pk') ].join(' '); }), this.getUniqueKeys(columns).map(function(columnData) { return [ 'ALTER TABLE', self.escapeField(newTableName), 'RENAME CONSTRAINT', self.generateConstraint(table, columnData.name, 'unique'), 'TO', self.generateConstraint(newTableName, columnData.name, 'unique') ].join(' '); }), this.getAutoIncrementKeys(columns).map(function(columnData) { return self.generateRenameSequenceQuery(table, columnData.name, newTableName, columnData.name); }) ).join(';'); } generateAlterTableColumnType(table, columnName, columnType, columnProperties) { let queries = [ [ 'ALTER TABLE', this.escapeField(table), this.generateAlterColumn(columnName, columnType, columnProperties) ].join(' '), [ 'ALTER TABLE', this.escapeField(table), this.generateAlterColumnSetNull(columnName, columnType, columnProperties) ].join(' '), [ 'ALTER TABLE', this.escapeField(table), this.generateAlterColumnDropDefault(columnName) ].join(' '), this.generateDropSequenceQuery(table, columnName) ] if (columnProperties.auto_increment) { queries.push(this.generateCreateSequenceQuery(table, columnName)); queries.push([ 'ALTER TABLE', this.escapeField(table), this.generateAlterColumnSetDefaultSeq(columnName, this.generateSequence(table, columnName)) ].join(' ')); } return queries.join(';'); } generateAlterTableAddPrimaryKey(table, columnName) { return [ 'ALTER TABLE', this.escapeField(table), 'ADD', this.generatePrimaryKey(table, columnName) ].join(' '); } generateAlterTableDropPrimaryKey(table, columnName) { return [ 'ALTER TABLE', this.escapeField(table), 'DROP CONSTRAINT IF EXISTS', this.generateConstraint(table, columnName, 'pk') ].join(' '); } generateAlterTableAddUniqueKey(table, columnName) { return [ 'ALTER TABLE', this.escapeField(table), 'ADD', this.generateUniqueKey(table, columnName) ].join(' '); } generateAlterTableDropUniqueKey(table, columnName) { return [ 'ALTER TABLE', this.escapeField(table), 'DROP CONSTRAINT IF EXISTS', this.generateConstraint(table, columnName, 'unique') ].join(' '); } generateAlterTableAddColumn(table, columnName, columnType, columnProperties) { return [ 'ALTER TABLE', this.escapeField(table), 'ADD COLUMN', this.generateColumn(columnName, columnType, columnProperties) ].join(' '); } generateAlterTableDropColumn(table, columnName) { return [ 'ALTER TABLE', this.escapeField(table), 'DROP COLUMN IF EXISTS', this.escapeField(columnName) ].join(' '); } generateAlterTableRenameColumn(table, columnName, newColumnName) { return [ 'ALTER TABLE', this.escapeField(table), 'RENAME COLUMN', this.escapeField(columnName), 'TO', this.escapeField(newColumnName) ].join(' '); } generateCreateIndex(table, columnName, indexType) { indexType = this.indexTypes.indexOf(indexType) > -1 ? indexType : this.indexTypes[0]; let indexName = columnName; let usingValue = this.escapeField(columnName); if (columnName.indexOf(this.columnDepthDelimiter) != -1) { // turn ex: recipie->name into recipe_name indexName = columnName.replace(new RegExp(this.columnDepthDelimiter, 'i'), '_'); usingValue = `(${columnName})`; } return [ 'CREATE INDEX', this.generateIndex(table, indexName), 'ON', this.escapeField(table), 'USING', indexType, ['(', usingValue, ')'].join('') ].join(' '); } generateDropIndex(table, columnName) { return [ 'DROP INDEX', this.generateIndex(table, columnName) ].join(' '); } generateSequence(table, columnName) { return this.generateConstraint(table, columnName, 'seq'); } generateCreateSequenceQuery(table, columnName) { return [ [ 'CREATE SEQUENCE', this.generateSequence(table, columnName), 'START 1', 'OWNED BY', [this.escapeField(table), this.escapeField(columnName)].join('.') ].join(' '), [ 'SELECT setval(\'', this.generateSequence(table, columnName), '\', GREATEST(COALESCE(MAX(', this.escapeField(columnName), '), 0), 0) + 1, false) FROM ', this.escapeField(table) ].join('') ].join(';'); } generateSimpleForeignKeyQuery(table, referenceTable) { return [ 'ALTER TABLE', this.escapeField(table), 'ADD CONSTRAINT', `${this.generateConstraint(table, referenceTable, 'id_fk')}`, 'FOREIGN KEY', `(${this.escapeField(`${inflect.singularize(referenceTable)}_id`)})`, 'REFERENCES', `${this.escapeField(referenceTable)} (${this.escapeField('id')})` ].join(' '); } generateDropSimpleForeignKeyQuery(table, referenceTable) { return [ 'ALTER TABLE', this.escapeField(table), 'DROP CONSTRAINT IF EXISTS', `${this.generateConstraint(table, referenceTable, 'id_fk')}`, ].join(' '); } generateRenameSequenceQuery(table, columnName, newTable, newColumnName) { return [ 'ALTER SEQUENCE', this.generateSequence(table, columnName), 'RENAME TO', this.generateSequence(newTable, newColumnName) ].join(' '); } generateDropSequenceQuery(table, columnName) { return [ 'DROP SEQUENCE IF EXISTS', this.generateSequence(table, columnName) ].join(' '); } generateCreateTableQuery(table, columns) { // Create sequences along with table let self = this; return [ super.generateCreateTableQuery(table, columns), this.getAutoIncrementKeys(columns).map(function(columnData) { return [ self.generateCreateSequenceQuery(table, columnData.name), [ 'ALTER TABLE', self.escapeField(table), self.generateAlterColumnSetDefaultSeq(columnData.name, self.generateSequence(table, columnData.name)) ].join(' ') ].join(';'); }) ].join(';'); } generateLimitClause(limitObj) { return (!limitObj) ? '' : (limitObj.count ? ` LIMIT ${limitObj.count}` : '') + (limitObj.offset ? ` OFFSET ${limitObj.offset}` : ''); } preprocessWhereObj(table, whereObj) { let whereObjArray = [] whereObj.forEach( where => { if (utilities.isObject(where.value)) { Object.keys(where.value).map( (k) => { whereObjArray.push(Object.assign({}, where, { columnName: `${where.columnName}${this.whereDepthDelimiter}'${k}'`, value: where.value[k] })); }); } else { whereObjArray.push(where); } }); return whereObjArray; } } PostgresAdapter.prototype.sanitizeType = { boolean: function(v) { return ['f', 't'][v | 0]; }, json: function(v) { return JSON.stringify(v); } } PostgresAdapter.prototype.escapeFieldCharacter = '"'; PostgresAdapter.prototype.columnDepthDelimiter = '->'; PostgresAdapter.prototype.whereDepthDelimiter = '->>'; PostgresAdapter.prototype.indexTypes = [ 'btree', 'hash', 'gist', 'gin' ]; PostgresAdapter.prototype.documentTypes = [ 'json' ]; PostgresAdapter.prototype.comparators = { is: field => `${field} = __VAR__`, not: field => `${field} <> __VAR__`, lt: field => `${field} < __VAR__`, lte: field => `${field} <= __VAR__`, gt: field => `${field} > __VAR__`, gte: field => `${field} >= __VAR__`, contains: field => `${field} LIKE '%' || __VAR__ || '%'`, icontains: field => `${field} ILIKE '%' || __VAR__ || '%'`, startswith: field => `${field} LIKE __VAR__ || '%'`, istartswith: field => `${field} ILIKE __VAR__ || '%'`, endswith: field => `${field} LIKE '%' || __VAR__`, iendswith: field => `${field} ILIKE '%' || __VAR__`, like: field => `${field} LIKE __VAR__`, ilike: field => `${field} ILIKE __VAR__`, is_null: field => `${field} IS NULL`, not_null: field => `${field} IS NOT NULL`, in: field => `ARRAY[${field}] <@ __VAR__`, not_in: field => `NOT (ARRAY[${field}] <@ __VAR__)`, json: (field, value) => { return `${field.replace(/"/g,"")} = __VAR__`; }, jsoncontains: (field) => { return `${field.replace(/"/g,"")} ? __VAR__`; } }; PostgresAdapter.prototype.types = { serial: { dbName: 'BIGINT', properties: { primary_key: true, nullable: false, auto_increment: true } }, int: { dbName: 'BIGINT' }, currency: { dbName: 'BIGINT' }, float: { dbName: 'FLOAT' }, string: { dbName: 'VARCHAR' }, text: { dbName: 'TEXT' }, datetime: { dbName: 'TIMESTAMP' }, boolean: { dbName: 'BOOLEAN' }, json: { dbName: 'JSONB' } }; PostgresAdapter.prototype.supportsForeignKey = true; module.exports = PostgresAdapter;