send-native-query.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. // Dependencies
  2. var util = require('util');
  3. var _ = require('@sailshq/lodash');
  4. var debug = require('debug')('query');
  5. module.exports = {
  6. friendlyName: 'Send native query',
  7. description: 'Send a native query to the MySQL database.',
  8. inputs: {
  9. connection: {
  10. friendlyName: 'Connection',
  11. description: 'An active database connection.',
  12. extendedDescription: 'The provided database connection instance must still be active. Only database connection instances created by the `getConnection()` machine in this driver are supported.',
  13. example: '===',
  14. required: true
  15. },
  16. nativeQuery: {
  17. description: 'A native query for the database.',
  18. extendedDescription: 'If `valuesToEscape` is provided, this supports template syntax like `$1`, `$2`, etc.',
  19. whereToGet: {
  20. description: 'Write a native query for this database, or if this driver supports it, use `compileStatement()` to build a native query from Waterline syntax.',
  21. extendedDescription: 'This might be compiled from a Waterline statement (stage 4 query) using "Compile statement", however it could also originate directly from userland code.'
  22. },
  23. example: 'SELECT * FROM pets WHERE species=$1 AND nickname=$2',
  24. required: true
  25. },
  26. valuesToEscape: {
  27. description: 'An optional list of strings, numbers, or special literals (true, false, or null) to escape and include in the native query, in order.',
  28. 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.',
  29. example: '===',
  30. defaultsTo: []
  31. },
  32. meta: {
  33. friendlyName: 'Meta (custom)',
  34. description: 'Additional stuff to pass to the driver.',
  35. extendedDescription: 'This is reserved for custom driver-specific extensions. Please refer to the documentation for the driver you are using for more specific information.',
  36. example: '==='
  37. }
  38. },
  39. exits: {
  40. success: {
  41. description: 'The native query was executed successfully.',
  42. outputVariableName: 'report',
  43. outputDescription: 'The `result` property is the result data the database sent back. The `meta` property is reserved for custom driver-specific extensions.',
  44. moreInfoUrl: 'https://github.com/felixge/node-mysql#getting-the-id-of-an-inserted-row',
  45. outputExample: '==='
  46. // outputExample: {
  47. // result: '===',
  48. // meta: '==='
  49. // }
  50. },
  51. queryFailed: {
  52. description: 'The database returned an error when attempting to execute the native query.',
  53. outputVariableName: 'report',
  54. 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.',
  55. outputExample: '==='
  56. // outputExample: {
  57. // error: '===',
  58. // meta: '==='
  59. // }
  60. },
  61. badConnection: {
  62. friendlyName: 'Bad connection',
  63. description: 'The provided connection is not valid or no longer active. Are you sure it was obtained by calling this driver\'s `getConnection()` method?',
  64. 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.',
  65. outputVariableName: 'report',
  66. outputDescription: 'The `meta` property is reserved for custom driver-specific extensions.',
  67. outputExample: '==='
  68. // outputExample: {
  69. // meta: '==='
  70. // }
  71. }
  72. },
  73. fn: function sendNativeQuery(inputs, exits) {
  74. var validateConnection = require('./private/validate-connection');
  75. // Validate provided connection.
  76. if (!validateConnection({ connection: inputs.connection }).execSync()) {
  77. return exits.badConnection({
  78. meta: inputs.meta
  79. });
  80. }
  81. // Validate provided native query.
  82. var sql = inputs.nativeQuery;
  83. var bindings = inputs.valuesToEscape || [];
  84. var queryInfo;
  85. debug('Running SQL Query:');
  86. debug('SQL: ' + sql);
  87. debug('Bindings: ' + bindings);
  88. debug('Connection Id: ' + inputs.connection.id);
  89. // If the meta flag is defined and it has a flag titled `isUsingQuestionMarks`
  90. // then the query was generated by Knex in compileStatement and the query
  91. // string is using `?` in place of values rather than the Waterline standardized
  92. // $1, $2, etc.
  93. if (!inputs.meta || !inputs.meta.isUsingQuestionMarks) {
  94. // Process SQL template, escaping bindings.
  95. // This converts `$1`, `$2`, etc. into the escaped binding.
  96. sql = sql.replace(/\$[1-9][0-9]*/g, function (substr){
  97. // e.g. `'$3'` => `'3'` => `3` => `2`
  98. var idx = +( substr.slice(1) ) - 1;
  99. // If no such binding exists, then just leave the original
  100. // template string (e.g. "$3") alone.
  101. if (idx >= bindings.length) {
  102. return substr;
  103. }
  104. // But otherwise, replace it with the escaped binding.
  105. return inputs.connection.escape(bindings[idx]);
  106. });
  107. // In this case the query has the values inline.
  108. queryInfo = sql;
  109. } else {
  110. queryInfo = {
  111. sql: sql,
  112. values: bindings
  113. };
  114. }
  115. debug('Compiled (final) SQL: ' + sql);
  116. // Send native query to the database using node-mysql.
  117. inputs.connection.query(queryInfo, function query() {
  118. // The exact format of the arguments for this callback are not part of
  119. // the officially documented behavior of node-mysql (at least not as
  120. // of March 2016 when this comment is being written).
  121. //
  122. // If you need to trace this down to the implementation, you might try
  123. // checking out the following links in order (from top to bottom):
  124. // • https://github.com/felixge/node-mysql#performing-queries
  125. // • https://github.com/felixge/node-mysql/blob/f5bd13d8c54ce524a6bff48bfceb15fdca3a938a/lib/protocol/ResultSet.js
  126. // • https://github.com/felixge/node-mysql/blob/d4a5fd7b5e92a1e09bf3c85d24265eada8a84ad8/lib/protocol/sequences/Sequence.js#L96
  127. // • https://github.com/felixge/node-mysql/blob/1720920f7afc660d37430c35c7128b20f77735e3/lib/protocol/sequences/Query.js#L94
  128. // • https://github.com/felixge/node-mysql/blob/1720920f7afc660d37430c35c7128b20f77735e3/lib/protocol/sequences/Query.js#L144
  129. //
  130. // For example, here are the raw arguments provided to the `.query()`
  131. // callback for different types of queries:
  132. // ====================================================================
  133. // * * * * * *
  134. // CREATE TABLE
  135. // * * * * * *
  136. // ```
  137. // null,
  138. // { // an OkPacket instance
  139. // fieldCount: 0,
  140. // affectedRows: 0,
  141. // insertId: 0,
  142. // serverStatus: 2,
  143. // warningCount: 0,
  144. // message: '',
  145. // protocol41: true,
  146. // changedRows: 0
  147. // },
  148. // undefined
  149. // ```
  150. //
  151. // * * * * * *
  152. // SELECT
  153. // * * * * * *
  154. // ```
  155. // null,
  156. // [ // an array of `RowDataPacket` instances:
  157. // {
  158. // id: 1,
  159. // CustomerName: 'Cardinal',
  160. // ...
  161. // },
  162. // ...
  163. // ],
  164. // [ // an array of `FieldPacket` instances:
  165. // {
  166. // catalog: 'def',
  167. // db: 'mikermcneil',
  168. // table: 'some_table',
  169. // orgTable: 'some_table',
  170. // name: 'id',
  171. // orgName: 'id',
  172. // charsetNr: 33,
  173. // length: 765,
  174. // type: 253,
  175. // flags: 20483,
  176. // decimals: 0,
  177. // default: undefined,
  178. // zeroFill: false,
  179. // protocol41: true
  180. // },
  181. // ...
  182. // ]
  183. // ```
  184. //
  185. // * * * * * *
  186. // INSERT
  187. // * * * * * *
  188. // ```
  189. // null,
  190. // { // an OkPacket instance
  191. // fieldCount: 0,
  192. // affectedRows: 1,
  193. // insertId: 1,
  194. // serverStatus: 2,
  195. // warningCount: 0,
  196. // message: '',
  197. // protocol41: true,
  198. // changedRows: 0
  199. // },
  200. // undefined
  201. // ```
  202. //
  203. // * * * * * *
  204. // DELETE
  205. // * * * * * *
  206. // ```
  207. // null,
  208. // { // an OkPacket instance
  209. // fieldCount: 0,
  210. // affectedRows: 1,
  211. // insertId: 0,
  212. // serverStatus: 34,
  213. // warningCount: 0,
  214. // message: '',
  215. // protocol41: true,
  216. // changedRows: 0
  217. // },
  218. // undefined
  219. // ```
  220. // * * * * * *
  221. // UPDATE
  222. // * * * * * *
  223. // ```
  224. // null,
  225. // { // an OkPacket instance
  226. // fieldCount: 0,
  227. // affectedRows: 1,
  228. // insertId: 0,
  229. // serverStatus: 34,
  230. // warningCount: 0,
  231. // message: '(Rows matched: 1 Changed: 1 Warnings: 0',
  232. // protocol41: true,
  233. // changedRows: 1
  234. // },
  235. // undefined
  236. // ```
  237. // ====================================================================
  238. // If the first argument is truthy, then treat it as an error.
  239. // (i.e. close shop early &gtfo; via the `queryFailed` exit)
  240. if (arguments[0]) {
  241. return exits.queryFailed({
  242. error: arguments[0],
  243. meta: inputs.meta
  244. });
  245. }
  246. // Otherwise, the query was successful.
  247. // Since the arguments passed to this callback and their data format
  248. // can vary across different types of queries, we do our best to normalize
  249. // that here. However, in order to do so, we have to be somewhat
  250. // opinionated; i.e. using the following heuristics when building the
  251. // standard `result` dictionary:
  252. // • If the 2nd arg is an array, we expose it as `result.rows`.
  253. // • Otherwise if the 2nd arg is a dictionary, we expose it as `result`.
  254. // • If the 3rd arg is an array, we include it as `result.fields`.
  255. // (if the 3rd arg is an array AND the 2nd arg is a dictionary, then
  256. // the 3rd arg is tacked on as the `fields` property of the 2nd arg.
  257. // If the 2nd arg already had `fields`, it is overridden.)
  258. var normalizedNativeResult;
  259. if (arguments[1]) {
  260. // `result :=`
  261. // `result.rows :=`
  262. if (_.isArray(arguments[1])) {
  263. normalizedNativeResult = { rows: arguments[1] };
  264. // `result :=`
  265. } else if (_.isObject(arguments[1])) {
  266. normalizedNativeResult = arguments[1];
  267. } else {
  268. 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 })));
  269. }
  270. }
  271. if (arguments[2]) {
  272. // `result.fields :=`
  273. if (_.isArray(arguments[2])) {
  274. normalizedNativeResult.fields = arguments[2];
  275. } else {
  276. 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 })));
  277. }
  278. }
  279. // Finally, return the normalized result.
  280. return exits.success({
  281. result: normalizedNativeResult,
  282. meta: inputs.meta
  283. });
  284. });
  285. }
  286. };