123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- // Dependencies
- var util = require('util');
- var _ = require('@sailshq/lodash');
- var debug = require('debug')('query');
- module.exports = {
- friendlyName: 'Send native query',
- description: 'Send a native query to the MySQL database.',
- inputs: {
- connection: {
- friendlyName: 'Connection',
- description: 'An active database connection.',
- extendedDescription: 'The provided database connection instance must still be active. Only database connection instances created by the `getConnection()` machine in this driver are supported.',
- example: '===',
- required: true
- },
- nativeQuery: {
- description: 'A native query for the database.',
- extendedDescription: 'If `valuesToEscape` is provided, this supports template syntax like `$1`, `$2`, etc.',
- whereToGet: {
- description: 'Write a native query for this database, or if this driver supports it, use `compileStatement()` to build a native query from Waterline syntax.',
- extendedDescription: 'This might be compiled from a Waterline statement (stage 4 query) using "Compile statement", however it could also originate directly from userland code.'
- },
- example: 'SELECT * FROM pets WHERE species=$1 AND nickname=$2',
- required: true
- },
- valuesToEscape: {
- description: 'An optional list of strings, numbers, or special literals (true, false, or null) to escape and include in the native query, in order.',
- extendedDescription: 'The first value in the list will be used to replace `$1`, the second value to replace `$2`, and so on. Note that numbers, `true`, `false`, and `null` are interpreted _differently_ than if they were strings wrapped in quotes. This array must never contain any arrays or dictionaries.',
- example: '===',
- defaultsTo: []
- },
- meta: {
- friendlyName: 'Meta (custom)',
- description: 'Additional stuff to pass to the driver.',
- extendedDescription: 'This is reserved for custom driver-specific extensions. Please refer to the documentation for the driver you are using for more specific information.',
- example: '==='
- }
- },
- exits: {
- success: {
- description: 'The native query was executed successfully.',
- outputVariableName: 'report',
- outputDescription: 'The `result` property is the result data the database sent back. The `meta` property is reserved for custom driver-specific extensions.',
- moreInfoUrl: 'https://github.com/felixge/node-mysql#getting-the-id-of-an-inserted-row',
- outputExample: '==='
- // outputExample: {
- // result: '===',
- // meta: '==='
- // }
- },
- queryFailed: {
- description: 'The database returned an error when attempting to execute the native query.',
- outputVariableName: 'report',
- outputDescription: 'The `error` property is a JavaScript Error instance with more details about what went wrong. The `meta` property is reserved for custom driver-specific extensions.',
- outputExample: '==='
- // outputExample: {
- // error: '===',
- // meta: '==='
- // }
- },
- badConnection: {
- friendlyName: 'Bad connection',
- description: 'The provided connection is not valid or no longer active. Are you sure it was obtained by calling this driver\'s `getConnection()` method?',
- extendedDescription: 'Usually, this means the connection to the database was lost due to a logic error or timing issue in userland code. In production, this can mean that the database became overwhelemed or was shut off while some business logic was in progress.',
- outputVariableName: 'report',
- outputDescription: 'The `meta` property is reserved for custom driver-specific extensions.',
- outputExample: '==='
- // outputExample: {
- // meta: '==='
- // }
- }
- },
- fn: function sendNativeQuery(inputs, exits) {
- var validateConnection = require('./private/validate-connection');
- // Validate provided connection.
- if (!validateConnection({ connection: inputs.connection }).execSync()) {
- return exits.badConnection({
- meta: inputs.meta
- });
- }
- // Validate provided native query.
- var sql = inputs.nativeQuery;
- var bindings = inputs.valuesToEscape || [];
- var queryInfo;
- debug('Running SQL Query:');
- debug('SQL: ' + sql);
- debug('Bindings: ' + bindings);
- debug('Connection Id: ' + inputs.connection.id);
- // If the meta flag is defined and it has a flag titled `isUsingQuestionMarks`
- // then the query was generated by Knex in compileStatement and the query
- // string is using `?` in place of values rather than the Waterline standardized
- // $1, $2, etc.
- if (!inputs.meta || !inputs.meta.isUsingQuestionMarks) {
- // Process SQL template, escaping bindings.
- // This converts `$1`, `$2`, etc. into the escaped binding.
- sql = sql.replace(/\$[1-9][0-9]*/g, function (substr){
- // e.g. `'$3'` => `'3'` => `3` => `2`
- var idx = +( substr.slice(1) ) - 1;
- // If no such binding exists, then just leave the original
- // template string (e.g. "$3") alone.
- if (idx >= bindings.length) {
- return substr;
- }
- // But otherwise, replace it with the escaped binding.
- return inputs.connection.escape(bindings[idx]);
- });
- // In this case the query has the values inline.
- queryInfo = sql;
- } else {
- queryInfo = {
- sql: sql,
- values: bindings
- };
- }
- debug('Compiled (final) SQL: ' + sql);
- // Send native query to the database using node-mysql.
- inputs.connection.query(queryInfo, function query() {
- // The exact format of the arguments for this callback are not part of
- // the officially documented behavior of node-mysql (at least not as
- // of March 2016 when this comment is being written).
- //
- // If you need to trace this down to the implementation, you might try
- // checking out the following links in order (from top to bottom):
- // • https://github.com/felixge/node-mysql#performing-queries
- // • https://github.com/felixge/node-mysql/blob/f5bd13d8c54ce524a6bff48bfceb15fdca3a938a/lib/protocol/ResultSet.js
- // • https://github.com/felixge/node-mysql/blob/d4a5fd7b5e92a1e09bf3c85d24265eada8a84ad8/lib/protocol/sequences/Sequence.js#L96
- // • https://github.com/felixge/node-mysql/blob/1720920f7afc660d37430c35c7128b20f77735e3/lib/protocol/sequences/Query.js#L94
- // • https://github.com/felixge/node-mysql/blob/1720920f7afc660d37430c35c7128b20f77735e3/lib/protocol/sequences/Query.js#L144
- //
- // For example, here are the raw arguments provided to the `.query()`
- // callback for different types of queries:
- // ====================================================================
- // * * * * * *
- // CREATE TABLE
- // * * * * * *
- // ```
- // null,
- // { // an OkPacket instance
- // fieldCount: 0,
- // affectedRows: 0,
- // insertId: 0,
- // serverStatus: 2,
- // warningCount: 0,
- // message: '',
- // protocol41: true,
- // changedRows: 0
- // },
- // undefined
- // ```
- //
- // * * * * * *
- // SELECT
- // * * * * * *
- // ```
- // null,
- // [ // an array of `RowDataPacket` instances:
- // {
- // id: 1,
- // CustomerName: 'Cardinal',
- // ...
- // },
- // ...
- // ],
- // [ // an array of `FieldPacket` instances:
- // {
- // catalog: 'def',
- // db: 'mikermcneil',
- // table: 'some_table',
- // orgTable: 'some_table',
- // name: 'id',
- // orgName: 'id',
- // charsetNr: 33,
- // length: 765,
- // type: 253,
- // flags: 20483,
- // decimals: 0,
- // default: undefined,
- // zeroFill: false,
- // protocol41: true
- // },
- // ...
- // ]
- // ```
- //
- // * * * * * *
- // INSERT
- // * * * * * *
- // ```
- // null,
- // { // an OkPacket instance
- // fieldCount: 0,
- // affectedRows: 1,
- // insertId: 1,
- // serverStatus: 2,
- // warningCount: 0,
- // message: '',
- // protocol41: true,
- // changedRows: 0
- // },
- // undefined
- // ```
- //
- // * * * * * *
- // DELETE
- // * * * * * *
- // ```
- // null,
- // { // an OkPacket instance
- // fieldCount: 0,
- // affectedRows: 1,
- // insertId: 0,
- // serverStatus: 34,
- // warningCount: 0,
- // message: '',
- // protocol41: true,
- // changedRows: 0
- // },
- // undefined
- // ```
- // * * * * * *
- // UPDATE
- // * * * * * *
- // ```
- // null,
- // { // an OkPacket instance
- // fieldCount: 0,
- // affectedRows: 1,
- // insertId: 0,
- // serverStatus: 34,
- // warningCount: 0,
- // message: '(Rows matched: 1 Changed: 1 Warnings: 0',
- // protocol41: true,
- // changedRows: 1
- // },
- // undefined
- // ```
- // ====================================================================
- // If the first argument is truthy, then treat it as an error.
- // (i.e. close shop early >fo; via the `queryFailed` exit)
- if (arguments[0]) {
- return exits.queryFailed({
- error: arguments[0],
- meta: inputs.meta
- });
- }
- // Otherwise, the query was successful.
- // Since the arguments passed to this callback and their data format
- // can vary across different types of queries, we do our best to normalize
- // that here. However, in order to do so, we have to be somewhat
- // opinionated; i.e. using the following heuristics when building the
- // standard `result` dictionary:
- // • If the 2nd arg is an array, we expose it as `result.rows`.
- // • Otherwise if the 2nd arg is a dictionary, we expose it as `result`.
- // • If the 3rd arg is an array, we include it as `result.fields`.
- // (if the 3rd arg is an array AND the 2nd arg is a dictionary, then
- // the 3rd arg is tacked on as the `fields` property of the 2nd arg.
- // If the 2nd arg already had `fields`, it is overridden.)
- var normalizedNativeResult;
- if (arguments[1]) {
- // `result :=`
- // `result.rows :=`
- if (_.isArray(arguments[1])) {
- normalizedNativeResult = { rows: arguments[1] };
- // `result :=`
- } else if (_.isObject(arguments[1])) {
- normalizedNativeResult = arguments[1];
- } else {
- return exits.error(new Error('Query was successful, but output from node-mysql is in an unrecognized format. Output:\n' + util.inspect(Array.prototype.slice.call(arguments), { depth: null })));
- }
- }
- if (arguments[2]) {
- // `result.fields :=`
- if (_.isArray(arguments[2])) {
- normalizedNativeResult.fields = arguments[2];
- } else {
- return exits.error(new Error('Query was successful, but output from node-mysql is in an unrecognized format. Output:\n' + util.inspect(Array.prototype.slice.call(arguments), { depth: null })));
- }
- }
- // Finally, return the normalized result.
- return exits.success({
- result: normalizedNativeResult,
- meta: inputs.meta
- });
- });
- }
- };
|