const mysql = require('mysql'); const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const MySQL = require('loopback-connector-mysql').MySQL; const EnumFactory = require('loopback-connector-mysql').EnumFactory; const fs = require('fs'); class VnMySQL extends MySQL { /** * Promisified version of execute(). * * @param {String} query The SQL query string * @param {Array} params The query parameters * @param {Object} options The loopback options * @param {Function} cb The callback * @return {Promise} The operation promise */ executeP(query, params, options = {}, cb) { return new Promise((resolve, reject) => { this.execute(query, params, options, (error, response) => { if (cb) cb(error, response); if (error) reject(error); else resolve(response); }); }); } /** * Executes an SQL query from an Stmt. * * @param {ParameterizedSql} stmt - Stmt object * @param {Object} options Query options (Ex: {transaction}) * @return {Object} Connector promise */ executeStmt(stmt, options) { return this.executeP(stmt.sql, stmt.params, options); } /** * Executes a query from an SQL script. * * @param {String} sqlScript The sql script file * @param {Array} params The query parameters * @param {Object} options Query options (Ex: {transaction}) * @return {Object} Connector promise */ executeScript(sqlScript, params, options) { return new Promise((resolve, reject) => { fs.readFile(sqlScript, 'utf8', (err, contents) => { if (err) return reject(err); this.execute(contents, params, options) .then(resolve, reject); }); }); } /** * Build the SQL WHERE clause for the where object without checking that * properties exists in the model. * * @param {object} where An object for the where conditions * @return {ParameterizedSQL} The SQL WHERE clause */ makeWhere(where) { let wrappedConnector = Object.create(this); Object.assign(wrappedConnector, { getModelDefinition() { return { properties: new Proxy({}, { get: () => true }) }; }, toColumnValue(_, val) { return val; }, columnEscaped(_, property) { return this.escapeName(property); } }); return wrappedConnector.buildWhere(null, where); } /** * Constructs SQL GROUP BY clause from Loopback filter. * * @param {Object} group The group by definition * @return {String} Built SQL group by */ makeGroupBy(group) { if (!group) return ''; if (typeof group === 'string') group = [group]; let clauses = []; for (let clause of group) { let sqlGroup = ''; let t = clause.split(/[\s,]+/); sqlGroup += this.escapeName(t[0]); clauses.push(sqlGroup); } return `GROUP BY ${clauses.join(', ')}`; } /** * Constructs SQL order clause from Loopback filter. * * @param {Object} order The order definition * @return {String} Built SQL order */ makeOrderBy(order) { if (!order) return ''; if (typeof order === 'string') order = [order]; let clauses = []; for (let clause of order) { let sqlOrder = ''; let t = clause.split(/[\s,]+/); sqlOrder += this.escapeName(t[0]); if (t.length > 1) sqlOrder += ' ' + (t[1].toUpperCase() == 'ASC' ? 'ASC' : 'DESC'); clauses.push(sqlOrder); } return `ORDER BY ${clauses.join(', ')}`; } /** * Constructs SQL limit clause from Loopback filter. * * @param {Object} filter The loopback filter * @return {String} Built SQL limit */ makeLimit(filter) { let limit = parseInt(filter.limit); let offset = parseInt(filter.offset || filter.skip); return this._buildLimit(null, limit, offset); } /** * Constructs SQL pagination from Loopback filter. * * @param {Object} filter The loopback filter * @return {String} Built SQL pagination */ makePagination(filter) { return ParameterizedSQL.join([ this.makeOrderBy(filter.order), this.makeLimit(filter) ]); } /** * Constructs SQL filter including where, order and limit * clauses from Loopback filter. * * @param {Object} filter The loopback filter * @return {String} Built SQL filter */ makeSuffix(filter) { return ParameterizedSQL.join([ this.makeWhere(filter.where), this.makePagination(filter) ]); } /** * Constructs SQL where clause from Loopback filter discarding * properties that not pertain to the model. If defined, appends * the table alias to each field. * * @param {String} model The model name * @param {Object} where The loopback where filter * @param {String} tableAlias Query main table alias * @return {String} Built SQL where */ buildModelWhere(model, where, tableAlias) { let parent = this; let wrappedConnector = Object.create(this); Object.assign(wrappedConnector, { columnEscaped(model, property) { let sql = tableAlias ? this.escapeName(tableAlias) + '.' : ''; return sql + parent.columnEscaped(model, property); } }); return wrappedConnector.buildWhere(model, where); } /** * Constructs SQL where clause from Loopback filter discarding * properties that not pertain to the model. If defined, appends * the table alias to each field. * * @param {String} model The model name * @param {Object} filter The loopback filter * @param {String} tableAlias Query main table alias * @return {String} Built SQL suffix */ buildModelSuffix(model, filter, tableAlias) { return ParameterizedSQL.join([ this.buildModelWhere(model, filter.where, tableAlias), this.makePagination(filter) ]); } } exports.VnMySQL = VnMySQL; exports.initialize = function initialize(dataSource, callback) { dataSource.driver = mysql; dataSource.connector = new VnMySQL(dataSource.settings); dataSource.connector.dataSource = dataSource; const modelBuilder = dataSource.modelBuilder; const defineType = modelBuilder.defineValueType ? modelBuilder.defineValueType.bind(modelBuilder) : modelBuilder.constructor.registerType.bind(modelBuilder.constructor); defineType(function Point() {}); dataSource.EnumFactory = EnumFactory; if (callback) { if (dataSource.settings.lazyConnect) { process.nextTick(function() { callback(); }); } else dataSource.connector.connect(callback); } }; MySQL.prototype.connect = function(callback) { const self = this; const options = generateOptions(this.settings); if (this.client) { if (callback) { process.nextTick(function() { callback(null, self.client); }); } } else this.client = connectionHandler(options, callback); function connectionHandler(options, callback) { const client = mysql.createPool(options); client.getConnection(function(err, connection) { const conn = connection; if (!err) { if (self.debug) debug('MySQL connection is established: ' + self.settings || {}); connection.release(); } else { if (err.code == 'ECONNREFUSED' || err.code == 'PROTOCOL_CONNECTION_LOST') { // PROTOCOL_CONNECTION_LOST console.error(`MySQL connection lost (${err.code}). Retrying..`); return setTimeout(() => connectionHandler(options, callback), 5000); } if (self.debug || !callback) console.error('MySQL connection is failed: ' + self.settings || {}, err); } callback && callback(err, conn); }); return client; } }; function generateOptions(settings) { const s = settings || {}; if (s.collation) { // Charset should be first 'chunk' of collation. s.charset = s.collation.substr(0, s.collation.indexOf('_')); } else { s.collation = 'utf8_general_ci'; s.charset = 'utf8'; } s.supportBigNumbers = (s.supportBigNumbers || false); s.timezone = (s.timezone || 'local'); if (isNaN(s.connectionLimit)) s.connectionLimit = 10; let options; if (s.url) { // use url to override other settings if url provided options = s.url; } else { options = { host: s.host || s.hostname || 'localhost', port: s.port || 3306, user: s.username || s.user, password: s.password, timezone: s.timezone, socketPath: s.socketPath, charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd. supportBigNumbers: s.supportBigNumbers, connectionLimit: s.connectionLimit, }; // Don't configure the DB if the pool can be used for multiple DBs if (!s.createDatabase) options.database = s.database; // Take other options for mysql driver // See https://github.com/strongloop/loopback-connector-mysql/issues/46 for (const p in s) { if (p === 'database' && s.createDatabase) continue; if (options[p] === undefined) options[p] = s[p]; } // Legacy UTC Date Processing fallback - SHOULD BE TRANSITIONAL if (s.legacyUtcDateProcessing === undefined) s.legacyUtcDateProcessing = true; if (s.legacyUtcDateProcessing) options.timezone = 'Z'; } return options; }