UNPKG

loopback-connector-redshift

Version:
505 lines (459 loc) 14.1 kB
// Copyright IBM Corp. 2013,2016. All Rights Reserved. // Node module: loopback-connector-redshift // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 /*! * Redshift connector for LoopBack */ 'use strict'; var SG = require('strong-globalize'); var g = SG(); var postgresql = require('pg'); var SqlConnector = require('loopback-connector').SqlConnector; var ParameterizedSQL = SqlConnector.ParameterizedSQL; var util = require('util'); var debug = require('debug')('loopback:connector:redshift'); /** * * Initialize the Redshift connector against the given data source * * @param {DataSource} dataSource The loopback-datasource-juggler dataSource * @callback {Function} [callback] The callback function * @param {String|Error} err The error string or object * @header Redshift.initialize(dataSource, [callback]) */ exports.initialize = function initializeDataSource(dataSource, callback) { if (!postgresql) { return; } var dbSettings = dataSource.settings || {}; dbSettings.host = dbSettings.host || dbSettings.hostname; dbSettings.user = dbSettings.user || dbSettings.username; dbSettings.debug = dbSettings.debug || debug.enabled; dataSource.connector = new Redshift(postgresql, dbSettings); dataSource.connector.dataSource = dataSource; if (callback) { if (dbSettings.lazyConnect) { process.nextTick(function() { callback(); }); } else { dataSource.connecting = true; dataSource.connector.connect(callback); } } }; /** * Redshift connector constructor * * @param {Redshift} postgresql Redshift node.js binding * @options {Object} settings An object for the data source settings. * See [node-postgres documentation](https://github.com/brianc/node-postgres/wiki/Client#parameters). * @property {String} url URL to the database, such as 'postgres://test:mypassword@localhost:5432/devdb'. * Other parameters can be defined as query string of the url * @property {String} hostname The host name or ip address of the Redshift DB server * @property {Number} port The port number of the Redshift DB Server * @property {String} user The user name * @property {String} password The password * @property {String} database The database name * @property {Boolean} ssl Whether to try SSL/TLS to connect to server * * @constructor */ function Redshift(postgresql, settings) { this.constructor.super_.call(this, 'redshift', settings); if (settings.url) { // pg-pool doesn't handle string config correctly this.clientConfig = { connectionString: settings.url, }; } else { this.clientConfig = settings; } this.pg = new postgresql.Pool(this.clientConfig); this.settings = settings; if (settings.debug) { debug('Settings %j', settings); } } // Inherit from loopback-datasource-juggler BaseSQL util.inherits(Redshift, SqlConnector); Redshift.prototype.debug = function() { if (this.settings.debug) { debug.apply(debug, arguments); } }; Redshift.prototype.getDefaultSchemaName = function() { return 'public'; }; /** * Connect to Redshift * @callback {Function} [callback] The callback after the connection is established */ Redshift.prototype.connect = function(callback) { var self = this; self.pg.connect(function(err, client, done) { self.client = client; process.nextTick(done); callback && callback(err, client); }); }; /** * Execute the sql statement * * @param {String} sql The SQL statement * @param {String[]} params The parameter values for the SQL statement * @param {Object} [options] Options object * @callback {Function} [callback] The callback after the SQL statement is executed * @param {String|Error} err The error string or object * @param {Object[]) data The result from the SQL */ Redshift.prototype.executeSQL = function(sql, params, options, callback) { var self = this; if (self.settings.debug) { if (params && params.length > 0) { self.debug('SQL: %s\nParameters: %j', sql, params); } else { self.debug('SQL: %s', sql); } } function executeWithConnection(connection, done) { connection.query(sql, params, function(err, data) { // if(err) console.error(err); if (err && self.settings.debug) { self.debug(err); } if (self.settings.debug && data) self.debug('%j', data); if (done) { process.nextTick(function() { // Release the connection in next tick done(err); }); } var result = null; if (data) { switch (data.command) { case 'DELETE': case 'UPDATE': result = {count: data.rowCount}; break; default: result = data.rows; } } callback(err ? err : null, result); }); } var transaction = options.transaction; if (transaction && transaction.connection && transaction.connector === this) { debug('Execute SQL within a transaction'); // Do not release the connection executeWithConnection(transaction.connection, null); } else { self.pg.connect(function(err, connection, done) { if (err) return callback(err); executeWithConnection(connection, done); }); } }; Redshift.prototype.buildInsertReturning = function(model, data, options) { // Redshift does not support the RETURNING keyword. return; }; Redshift.prototype.buildInsertDefaultValues = function(model, data, options) { return 'DEFAULT VALUES'; }; // FIXME: [rfeng] The native implementation of upsert only works with // postgresql 9.1 or later as it requres writable CTE // See https://github.com/strongloop/loopback-connector-postgresql/issues/27 /** * Update if the model instance exists with the same id or create a new instance * * @param {String} model The model name * @param {Object} data The model instance data * @callback {Function} [callback] The callback function * @param {String|Error} err The error string or object * @param {Object} The updated model instance */ /* Redshift.prototype.updateOrCreate = function (model, data, callback) { var self = this; data = self.mapToDB(model, data); var props = self._categorizeProperties(model, data); var idColumns = props.ids.map(function(key) { return self.columnEscaped(model, key); } ); var nonIdsInData = props.nonIdsInData; var query = []; query.push('WITH update_outcome AS (UPDATE ', self.tableEscaped(model), ' SET '); query.push(self.toFields(model, data, false)); query.push(' WHERE '); query.push(idColumns.map(function (key, i) { return ((i > 0) ? ' AND ' : ' ') + key + '=$' + (nonIdsInData.length + i + 1); }).join(',')); query.push(' RETURNING ', idColumns.join(','), ')'); query.push(', insert_outcome AS (INSERT INTO ', self.tableEscaped(model), ' '); query.push(self.toFields(model, data, true)); query.push(' WHERE NOT EXISTS (SELECT * FROM update_outcome) RETURNING ', idColumns.join(','), ')'); query.push(' SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome'); var queryParams = []; nonIdsInData.forEach(function(key) { queryParams.push(data[key]); }); props.ids.forEach(function(key) { queryParams.push(data[key] || null); }); var idColName = self.idColumn(model); self.query(query.join(''), queryParams, function(err, info) { if (err) { return callback(err); } var idValue = null; if (info && info[0]) { idValue = info[0][idColName]; } callback(err, idValue); }); }; */ Redshift.prototype.fromColumnValue = function(prop, val) { if (val == null) { return val; } var type = prop.type && prop.type.name; if (prop && type === 'Boolean') { if (typeof val === 'boolean') { return val; } else { return (val === 'Y' || val === 'y' || val === 'T' || val === 't' || val === '1'); } } else { return val; } }; /*! * Convert to the Database name * @param {String} name The name * @returns {String} The converted name */ Redshift.prototype.dbName = function(name) { if (!name) { return name; } // Redshift default to lowercase names return name.toLowerCase(); }; function escapeIdentifier(str) { var escaped = '"'; for (var i = 0; i < str.length; i++) { var c = str[i]; if (c === '"') { escaped += c + c; } else { escaped += c; } } escaped += '"'; return escaped; } function escapeLiteral(str) { var hasBackslash = false; var escaped = '\''; for (var i = 0; i < str.length; i++) { var c = str[i]; if (c === '\'') { escaped += c + c; } else if (c === '\\\\') { escaped += c + c; hasBackslash = true; } else { escaped += c; } } escaped += '\''; if (hasBackslash === true) { escaped = ' E' + escaped; } return escaped; } /*! * Escape the name for Redshift DB * @param {String} name The name * @returns {String} The escaped name */ Redshift.prototype.escapeName = function(name) { if (!name) { return name; } return escapeIdentifier(name); }; Redshift.prototype.escapeValue = function(value) { if (typeof value === 'string') { return escapeLiteral(value); } if (typeof value === 'number' || typeof value === 'boolean') { return value; } return value; }; Redshift.prototype.tableEscaped = function(model) { var schema = this.schema(model) || 'public'; return this.escapeName(schema) + '.' + this.escapeName(this.table(model)); }; function buildLimit(limit, offset) { var clause = []; if (isNaN(limit)) { limit = 0; } if (isNaN(offset)) { offset = 0; } if (!limit && !offset) { return ''; } if (limit) { clause.push('LIMIT ' + limit); } if (offset) { clause.push('OFFSET ' + offset); } return clause.join(' '); } Redshift.prototype.applyPagination = function(model, stmt, filter) { var limitClause = buildLimit(filter.limit, filter.offset || filter.skip); return stmt.merge(limitClause); }; Redshift.prototype.buildExpression = function(columnName, operator, operatorValue, propertyDefinition) { switch (operator) { case 'like': return new ParameterizedSQL(columnName + ' LIKE ?', [operatorValue]); case 'nlike': return new ParameterizedSQL(columnName + ' NOT LIKE ?', [operatorValue]); case 'ilike': return new ParameterizedSQL(columnName + " ILIKE ?", [operatorValue]); case 'nilike': return new ParameterizedSQL(columnName + " NOT ILIKE ?", [operatorValue]); case 'regexp': if (operatorValue.global) g.warn('{{Redshift}} regex syntax does not respect the {{`g`}} flag'); if (operatorValue.multiline) g.warn('{{Redshift}} regex syntax does not respect the {{`m`}} flag'); if (operatorValue.ignoreCase) g.warn('{{Redshift}} regex syntax does not respect the {{`i`}} flag'); var regexOperator = ' ~ ?'; return new ParameterizedSQL(columnName + regexOperator, [operatorValue.source]); default: // invoke the base implementation of `buildExpression` return this.invokeSuper('buildExpression', columnName, operator, operatorValue, propertyDefinition); } }; /** * Disconnect from Redshift * @param {Function} [cb] The callback function */ Redshift.prototype.disconnect = function disconnect(cb) { if (this.pg) { if (this.settings.debug) { this.debug('Disconnecting from ' + this.settings.hostname); } var pg = this.pg; this.pg = null; pg.end(); // This is sync } if (cb) { process.nextTick(cb); } }; Redshift.prototype.ping = function(cb) { this.execute('SELECT 1 AS result', [], cb); }; Redshift.prototype.getInsertedId = function(model, info) { var idColName = this.idColumn(model); var idValue; if (info && info[0]) { idValue = info[0][idColName]; } return idValue; }; /*! * Convert property name/value to an escaped DB column value * @param {Object} prop Property descriptor * @param {*} val Property value * @returns {*} The escaped value of DB column */ Redshift.prototype.toColumnValue = function(prop, val) { if (val == null) { // Redshift complains with NULLs in not null columns // If we have an autoincrement value, return DEFAULT instead if (prop.autoIncrement || prop.id) { return new ParameterizedSQL('DEFAULT'); } else { return null; } } if (prop.type === String) { return String(val); } if (prop.type === Number) { if (isNaN(val)) { // Map NaN to NULL return val; } return val; } if (prop.type === Date || prop.type.name === 'Timestamp') { if (!val.toISOString) { val = new Date(val); } var iso = val.toISOString(); // Pass in date as UTC and make sure Redshift stores using UTC timezone. // Redshift does not support the TIMESTAMP WITH TIME ZONE data type. return new ParameterizedSQL({ sql: '?::TIMESTAMP WITHOUT TIME ZONE', params: [iso], }); } if (prop.type === Boolean) { if (val) { return true; } else { return false; } } if (prop.type.name === 'GeoPoint' || prop.type.name === 'Point') { throw new Error(g.f('The Point datatype is not supported in Redshift.')); } return val; }; /** * Get the place holder in SQL for identifiers, such as ?? * @param {String} key Optional key, such as 1 or id * @returns {String} The place holder */ Redshift.prototype.getPlaceholderForIdentifier = function(key) { throw new Error(g.f('{{Placeholder}} for identifiers is not supported')); }; /** * Get the place holder in SQL for values, such as :1 or ? * @param {String} key Optional key, such as 1 or id * @returns {String} The place holder */ Redshift.prototype.getPlaceholderForValue = function(key) { return '$' + key; }; Redshift.prototype.getCountForAffectedRows = function(model, info) { return info && info.count; }; require('./discovery')(Redshift); require('./migration')(Redshift); require('./transaction')(Redshift);