salix/loopback/server/connectors/vn-mysql.js

467 lines
14 KiB
JavaScript
Raw Permalink Normal View History

const mysql = require('mysql');
const MySQL = require('loopback-connector-mysql').MySQL;
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
const {Transaction, SQLConnector, ParameterizedSQL} = require('loopback-connector');
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);
}
2020-11-20 14:05:57 +00:00
/**
* 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(', ')}`;
}
2018-08-21 11:38:16 +00:00
/**
* 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);
2018-08-21 11:38:16 +00:00
}
/**
* 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)
]);
2018-08-21 11:38:16 +00:00
}
/**
* 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);
2018-08-21 11:38:16 +00:00
}
});
return wrappedConnector.buildWhere(model, where);
2018-08-21 11:38:16 +00:00
}
/**
* 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)
]);
2018-08-21 11:38:16 +00:00
}
create(model, data, opts, cb) {
const ctx = {data};
this.invokeMethod('create',
arguments, model, ctx, opts, cb);
}
createAll(model, data, opts, cb) {
const ctx = {data};
this.invokeMethod('createAll',
arguments, model, ctx, opts, cb);
}
save(model, data, opts, cb) {
const ctx = {data};
this.invokeMethod('save',
arguments, model, ctx, opts, cb);
}
updateOrCreate(model, data, opts, cb) {
const ctx = {data};
this.invokeMethod('updateOrCreate',
arguments, model, ctx, opts, cb);
}
replaceOrCreate(model, data, opts, cb) {
const ctx = {data};
this.invokeMethod('replaceOrCreate',
arguments, model, ctx, opts, cb);
}
destroyAll(model, where, opts, cb) {
const ctx = {where};
this.invokeMethod('destroyAll',
arguments, model, ctx, opts, cb);
}
update(model, where, data, opts, cb) {
const ctx = {where, data};
this.invokeMethod('update',
arguments, model, ctx, opts, cb);
}
replaceById(model, id, data, opts, cb) {
const ctx = {id, data};
this.invokeMethod('replaceById',
arguments, model, ctx, opts, cb);
}
isLoggable(model) {
const Model = this.getModelDefinition(model).model;
2024-01-18 09:39:12 +00:00
const {settings} = Model.definition;
2024-01-18 13:25:48 +00:00
return settings.mixins?.Loggable;
}
invokeMethod(method, args, model, ctx, opts, cb) {
2024-03-14 07:36:19 +00:00
if (!this.isLoggable(model) && !opts?.userId)
return super[method].apply(this, args);
this.invokeMethodP(method, [...args], model, ctx, opts)
.then(res => cb(...[null].concat(res)), cb);
}
async invokeMethodP(method, args, model, ctx, opts) {
2023-03-07 13:11:03 +00:00
const Model = this.getModelDefinition(model).model;
let tx;
if (!opts.transaction) {
tx = await Transaction.begin(this, {});
2024-03-14 07:36:19 +00:00
opts = Object.assign({transaction: tx}, opts);
}
try {
2024-03-14 07:36:19 +00:00
const {userId} = opts;
2023-04-12 12:47:07 +00:00
if (userId) {
const user = await Model.app.models.VnUser.findById(userId, {fields: ['name']}, opts);
2023-04-12 12:47:07 +00:00
await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts);
}
const res = await new Promise((resolve, reject) => {
const fnArgs = args.slice(0, -2);
fnArgs.push(opts, (err, ...args) => {
if (err) return reject(err);
resolve(args);
});
super[method].apply(this, fnArgs);
});
2023-04-12 12:47:07 +00:00
if (userId) await this.executeP(`CALL account.myUser_logout()`, null, opts);
if (tx) await tx.commit();
return res;
} catch (err) {
if (tx) tx.rollback();
throw err;
}
}
}
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() { });
2018-08-21 11:38:16 +00:00
dataSource.EnumFactory = EnumFactory;
2019-02-25 09:04:11 +00:00
if (callback) {
if (dataSource.settings.lazyConnect) {
process.nextTick(function() {
callback();
});
} else
dataSource.connector.connect(callback);
2019-02-25 09:04:11 +00:00
}
2018-08-21 11:38:16 +00:00
};
2020-12-28 08:44:21 +00:00
2024-08-30 07:14:21 +00:00
// Code extracted from original Loopback MySQL connector
// since it cannot be reused, please try not to alter it.
// https://github.com/loopbackio/loopback-connector-mysql/blob/v6.2.0/lib/mysql.js
MySQL.prototype.connect = function(callback) {
2020-12-28 08:44:21 +00:00
const self = this;
const options = generateOptions(this.settings);
if (this.client) {
if (callback) {
process.nextTick(function() {
2020-12-28 08:44:21 +00:00
callback(null, self.client);
});
}
} else
this.client = connectionHandler(options, callback);
function connectionHandler(options, callback) {
const client = mysql.createPool(options);
client.getConnection(function(err, connection) {
2020-12-28 08:44:21 +00:00
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;
}
SQLConnector.prototype.all = function find(model, filter, options, cb) {
const self = this;
// Order by id if no order is specified
filter = filter || {};
const stmt = this.buildSelect(model, filter, options);
this.execute(stmt.sql, stmt.params, options, function(err, data) {
if (err)
return cb(err, []);
try {
const objs = data.map(function(obj) {
return self.fromRow(model, obj);
});
if (filter && filter.include) {
self.getModelDefinition(model).model.include(
objs, filter.include, options, cb,
);
} else
cb(null, objs);
} catch (error) {
cb(error, []);
}
});
};