create-manager.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Dependencies
  2. var util = require('util');
  3. var Url = require('url');
  4. var _ = require('@sailshq/lodash');
  5. var felix = require('mysql');
  6. module.exports = {
  7. friendlyName: 'Create manager',
  8. description: 'Build and initialize a connection manager instance for this database.',
  9. extendedDescription:
  10. 'The `manager` instance returned by this method contains any configuration that is necessary ' +
  11. 'for communicating with the database and establishing connections (e.g. host, user, password) ' +
  12. 'as well as any other relevant metadata. The manager will often also contain a reference ' +
  13. 'to some kind of native container (e.g. a connection pool).\n' +
  14. '\n' +
  15. 'Note that a manager instance does not necessarily need to correspond with a pool though--' +
  16. 'it might simply be a container for storing config, or it might refer to multiple pools ' +
  17. '(e.g. a PoolCluster from felixge\'s `mysql` package).',
  18. sync: true,
  19. inputs: {
  20. connectionString: {
  21. description: 'A connection string to use to connect to a MySQL database.',
  22. extendedDescription: 'Be sure to include credentials. You can also optionally provide the name of an existing database on your MySQL server.',
  23. moreInfoUrl: 'https://gist.github.com/mikermcneil/46d10fd816c980cd3d9f',
  24. whereToGet: {
  25. url: 'https://gist.github.com/mikermcneil/46d10fd816c980cd3d9f'
  26. },
  27. example: '===',
  28. // example: 'mysql://mikermcneil:p4ssw02D@localhost:3306/some_db',
  29. required: true
  30. },
  31. onUnexpectedFailure: {
  32. description: 'A function to call any time an unexpected error event is received from this manager or any of its connections.',
  33. extendedDescription:
  34. 'This can be used for anything you like, whether that\'s sending an email to devops, ' +
  35. 'or something as simple as logging a warning to the console.\n' +
  36. '\n' +
  37. 'For example:\n' +
  38. '```\n' +
  39. 'onUnexpectedFailure: function (err) {\n' +
  40. ' console.warn(\'Unexpected failure in database manager:\',err);\n' +
  41. '}\n' +
  42. '```',
  43. example: '->'
  44. },
  45. meta: {
  46. friendlyName: 'Meta (additional options)',
  47. description: 'Additional MySQL-specific options to use when connecting.',
  48. extendedDescription: 'If specified, should be a dictionary. If there is a conflict between something provided in the connection string, and something in `meta`, the connection string takes priority.',
  49. moreInfoUrl: 'https://gist.github.com/mikermcneil/46d10fd816c980cd3d9f',
  50. example: '==='
  51. }
  52. },
  53. exits: {
  54. success: {
  55. description: 'The manager was successfully created.',
  56. extendedDescription:
  57. 'The new manager should be passed in to `getConnection()`.' +
  58. 'Note that _no matter what_, this manager must be capable of ' +
  59. 'spawning an infinite number of connections (i.e. via `getConnection()`). ' +
  60. 'The implementation of how exactly it does this varies on a driver-by-driver ' +
  61. 'basis; and it may also vary based on the configuration passed into the `meta` input.',
  62. outputVariableName: 'report',
  63. outputDescription: 'The `manager` property is a manager instance that will be passed into `getConnection()`. The `meta` property is reserved for custom driver-specific extensions.',
  64. outputExample: '==='
  65. // example: {
  66. // manager: '===',
  67. // meta: '==='
  68. // }
  69. },
  70. malformed: {
  71. description: 'The provided connection string is not valid for MySQL.',
  72. outputVariableName: 'report',
  73. outputDescription: 'The `error` property is a JavaScript Error instance explaining that (and preferably "why") the provided connection string is invalid. The `meta` property is reserved for custom driver-specific extensions.',
  74. outputExample: '==='
  75. // example: {
  76. // error: '===',
  77. // meta: '==='
  78. // }
  79. },
  80. failed: {
  81. description: 'Could not create a connection manager for this database using the specified connection string.',
  82. extendedDescription:
  83. 'If this exit is called, it might mean any of the following:\n' +
  84. ' + the credentials encoded in the connection string are incorrect\n' +
  85. ' + there is no database server running at the provided host (i.e. even if it is just that the database process needs to be started)\n' +
  86. ' + there is no software "database" with the specified name running on the server\n' +
  87. ' + the provided connection string does not have necessary access rights for the specified software "database"\n' +
  88. ' + this Node.js process could not connect to the database, perhaps because of firewall/proxy settings\n' +
  89. ' + any other miscellaneous connection error\n' +
  90. '\n' +
  91. 'Note that even if the database is unreachable, bad credentials are being used, etc, ' +
  92. 'this exit will not necessarily be called-- that depends on the implementation of the driver ' +
  93. 'and any special configuration passed to the `meta` input. e.g. if a pool is being used that spins up ' +
  94. 'multiple connections immediately when the manager is created, then this exit will be called if any of ' +
  95. 'those initial attempts fail. On the other hand, if the manager is designed to produce adhoc connections, ' +
  96. 'any errors related to bad credentials, connectivity, etc. will not be caught until `getConnection()` is called.',
  97. outputVariableName: 'report',
  98. outputDescription: 'The `error` property is a JavaScript Error instance with more information and a stack trace. The `meta` property is reserved for custom driver-specific extensions.',
  99. outputExample: '==='
  100. // outputExample: {
  101. // error: '===',
  102. // meta: '==='
  103. // }
  104. }
  105. },
  106. fn: function createManager(inputs, exits) {
  107. // Note:
  108. // Support for different types of managers is database-specific, and is not
  109. // built into the Waterline driver spec-- however this type of configurability
  110. // can be instrumented using `meta`.
  111. //
  112. // In particular, support for ad-hoc connections (i.e. no pool) and clusters/multiple
  113. // pools (see "PoolCluster": https://github.com/felixge/node-mysql/blob/v2.10.2/Readme.md#poolcluster)
  114. // could be implemented here, using properties on `meta` to determine whether or not
  115. // to have this manager produce connections ad-hoc, from a pool, or from a cluster of pools.
  116. //
  117. // Feel free to fork this driver and customize as you see fit. Also note that
  118. // contributions to the core driver in this area are welcome and greatly appreciated!
  119. // Build a local variable (`_mysqlClientConfig`) to house a dictionary
  120. // of additional MySQL options that will be passed into `.createPool()`
  121. // (Note that these could also be used with `.connect()` or `.createPoolCluster()`)
  122. //
  123. // This is pulled from the `connectionString` and `meta` inputs, and used for
  124. // configuring stuff like `host` and `password`.
  125. //
  126. // For a complete list of available options, see:
  127. // • https://github.com/felixge/node-mysql#connection-options
  128. //
  129. // However, note that supported options are explicitly whitelisted below.
  130. var _mysqlClientConfig = {};
  131. // Validate and parse `meta` (if specified).
  132. if (!_.isUndefined(inputs.meta)) {
  133. if (!_.isObject(inputs.meta)) {
  134. return exits.error('If provided, `meta` must be a dictionary.');
  135. }
  136. // Use properties of `meta` directly as MySQL client config.
  137. // (note that we're very careful to only stick a property on the client config
  138. // if it was not undefined, just in case that matters)
  139. [
  140. // MySQL Client Options:
  141. // ============================================
  142. // Basic:
  143. 'host', 'port', 'database', 'user', 'password',
  144. 'charset', 'timezone', 'ssl',
  145. // Advanced:
  146. 'connectTimeout', 'stringifyObjects', 'insecureAuth', 'typeCast',
  147. 'queryFormat', 'supportBigNumbers', 'bigNumberStrings', 'dateStrings',
  148. 'debug', 'trace', 'multipleStatements', 'flags',
  149. // Pool-specific:
  150. 'acquireTimeout', 'waitForConnections', 'connectionLimit', 'queueLimit',
  151. ].forEach(function processKey(mysqlClientConfKeyName) {
  152. if (!_.isUndefined(inputs.meta[mysqlClientConfKeyName])) {
  153. _mysqlClientConfig[mysqlClientConfKeyName] = inputs.meta[mysqlClientConfKeyName];
  154. }
  155. });
  156. // In the future, other special properties of `meta` could be used
  157. // as options for the manager-- e.g. whether or not to use pooling,
  158. // or the connection strings of replicas, etc.
  159. // // Now use other special properties of `meta` as our higher-level
  160. // // logical machinepack options.
  161. // [
  162. // // Machinepack Configuration:
  163. // // ============================================
  164. // '',
  165. // ].forEach(function (pkgConfKeyName) {
  166. // // ...
  167. // });
  168. }
  169. // Validate & parse connection string, pulling out MySQL client config
  170. // (call `malformed` if invalid).
  171. //
  172. // Remember: connection string takes priority over `meta` in the event of a conflict.
  173. try {
  174. var urlToParse = inputs.connectionString;
  175. // We don't actually care about the protocol, but `url.parse()` returns funky results
  176. // if the argument doesn't have one. So we'll add one if necessary.
  177. // See https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax
  178. if (!urlToParse.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) {
  179. urlToParse = 'mysql://' + urlToParse;
  180. }
  181. var parsedConnectionStr = Url.parse(urlToParse);
  182. // Parse port & host
  183. var DEFAULT_HOST = 'localhost';
  184. var DEFAULT_PORT = 3306;
  185. if (parsedConnectionStr.port) {
  186. _mysqlClientConfig.port = +parsedConnectionStr.port;
  187. } else {
  188. _mysqlClientConfig.port = DEFAULT_PORT;
  189. }
  190. if (parsedConnectionStr.hostname) {
  191. _mysqlClientConfig.host = parsedConnectionStr.hostname;
  192. } else {
  193. _mysqlClientConfig.host = DEFAULT_HOST;
  194. }
  195. // Parse user & password
  196. if (parsedConnectionStr.auth && _.isString(parsedConnectionStr.auth)) {
  197. var authPieces = parsedConnectionStr.auth.split(/:/);
  198. if (authPieces[0]) {
  199. _mysqlClientConfig.user = authPieces[0];
  200. }
  201. if (authPieces[1]) {
  202. _mysqlClientConfig.password = authPieces[1];
  203. }
  204. }
  205. // Parse database name
  206. if (_.isString(parsedConnectionStr.pathname)) {
  207. var _databaseName = parsedConnectionStr.pathname;
  208. // Trim leading and trailing slashes
  209. _databaseName = _databaseName.replace(/^\/+/, '');
  210. _databaseName = _databaseName.replace(/\/+$/, '');
  211. // If anything is left, use it as the database name.
  212. if (_databaseName) {
  213. _mysqlClientConfig.database = _databaseName;
  214. }
  215. }
  216. } catch (_e) {
  217. _e.message = util.format('Provided value (`%s`) is not a valid MySQL connection string.', inputs.connectionString) + ' Error details: ' + _e.message;
  218. return exits.malformed({
  219. error: _e,
  220. meta: inputs.meta
  221. });
  222. }
  223. // Create a connection pool.
  224. //
  225. // More about using pools with node-mysql:
  226. // • https://github.com/felixge/node-mysql#pooling-connections
  227. var pool = felix.createPool(_mysqlClientConfig);
  228. // Bind an "error" handler in order to handle errors from connections in the pool,
  229. // or from the pool itself. Otherwise, without any further protection, if any MySQL
  230. // connections in the pool die, then the process would crash with an error.
  231. //
  232. // For more background, see:
  233. // • https://github.com/felixge/node-mysql/blob/v2.10.2/Readme.md#error-handling
  234. pool.on('error', function err(err) {
  235. // When/if something goes wrong in this pool, call the `onUnexpectedFailure` notifier
  236. // (if one was provided)
  237. if (!_.isUndefined(inputs.onUnexpectedFailure)) {
  238. inputs.onUnexpectedFailure(err || new Error('One or more pooled connections to MySQL database were lost. Did the database server go offline?'));
  239. }
  240. });
  241. // Finally, build and return the manager.
  242. var mgr = {
  243. pool: pool,
  244. connectionString: inputs.connectionString
  245. };
  246. return exits.success({
  247. manager: mgr,
  248. meta: inputs.meta,
  249. });
  250. }
  251. };