UNPKG

sails-arangojs

Version:

A sails-arangojs adapter for Sails / Waterline

557 lines (510 loc) 24.4 kB
/* eslint-disable brace-style */ /** * Module dependencies */ const assert = require('assert'); const util = require('util'); const url = require('url'); const _ = require('@sailshq/lodash'); const flaverr = require('flaverr'); const qs = require('qs'); const normalizeDatabase = require('./private/normalize-database'); const normalizeUser = require('./private/normalize-user'); const normalizePort = require('./private/normalize-port'); const normalizeHost = require('./private/normalize-host'); const normalizePassword = require('./private/normalize-password'); /** * normalizeDatastoreConfig() * * Normalize the provided datastore config dictionary. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * NOTES: * • All modifications are performed in-place! * • Top-level overrides like `host`, `port`, `user`, etc. are normalized and validated. * • Top-level overrides like `host`, `port`, `user`, etc. take precedence over whatever is in the URL. * • Normalized versions of top-level overrides like `host`, `port`, `user`, etc. °°ARE°° sucked into the URL automatically. * • Recognized URL pieces like the host, port, user, etc. **ARE NOT** attached as top-level props automatically. * • Recognized URL pieces like the host, port, user, etc. **ARE** validated and normalized individually, rebuilding the URL if necessary. * • Miscellanous properties **ARE NOT** sucked in to the URL automatically. * • Miscellaneous querystring opts in the URL °°ARE°° attached automatically as top-level props. * · They are left as-is in the URL as well. * · They are treated as strings (e.g. `?foo=0&bar=false` becomes `{foo:'0', bar: 'false'}`) * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Dictionary} dsConfig * ˚¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯\ * ˙ url: {String?} :: * ˙ host: {String?} :: * ˙ port: {String?} :: * ˙ user: {String?} :: * ˙ password: {String?} :: * ˙ database: {String?} :: * * @param {Array} whitelist * Optional. If provided, this is an array of strings indicating which custom settings * are recognized and should be allowed. The standard `url`/`host`/`database` etc. are * always allowed, no matter what. e.g. `['ssl', 'replicaSet']` * * @param {String} expectedProtocolPrefix * Optional. If specified, this restricts `dsConfig.url` to use a mandatory protocol (e.g. "mongodb") * If no protocol is included (or if it is simply `://`), then this mandatory protocol * will be tacked on automatically. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ module.exports = function normalizeDatastoreConfig( dsConfig, whitelist, expectedProtocolPrefix ) { // Sanity checks assert(_.isObject(dsConfig), '`dsConfig` should exist and be a dictionary!'); assert( _.isUndefined(whitelist) || _.isArray(whitelist), "If provided, 2nd argument should be a whitelist of valid custom settings-- e.g. ['ssl', 'replicaSet']" ); assert( _.isUndefined(expectedProtocolPrefix) || _.isString(expectedProtocolPrefix), 'If provided, 3rd argument should be a string (e.g. "mongodb") representing the prefix for the expected protocol.' ); // Default top-level things. // If items in BASELINE_PROPS are included in the querystring of the connection url, // they are allowed to remain, but are not automatically applied at the top-level. // (Note that this whitelist applies to overrides AND to querystring-encoded values) const BASELINE_PROPS = [ 'url', 'adapter', 'schema', 'identity', 'user', 'password', 'host', 'port', 'database', ]; // // Get convenient reference to this datastore's name. // var datastoreName = dsConfig.identity; // Have a look at the datastore config to get an idea of what's there. const hasUrl = !_.isUndefined(dsConfig.url); const hasUserOverride = !_.isUndefined(dsConfig.user); const hasPasswordOverride = !_.isUndefined(dsConfig.password); const hasHostOverride = !_.isUndefined(dsConfig.host); const hasPortOverride = !_.isUndefined(dsConfig.port); const hasDatabaseOverride = !_.isUndefined(dsConfig.database); // ┌┐┌┌─┐┬─┐┌┬┐┌─┐┬ ┬┌─┐┌─┐ ╔═╗╦ ╦╔═╗╦═╗╦═╗╦╔╦╗╔═╗╔═╗ // ││││ │├┬┘│││├─┤│ │┌─┘├┤ ║ ║╚╗╔╝║╣ ╠╦╝╠╦╝║ ║║║╣ ╚═╗ // ┘└┘└─┘┴└─┴ ┴┴ ┴┴─┘┴└─┘└─┘ ╚═╝ ╚╝ ╚═╝╩╚═╩╚═╩═╩╝╚═╝╚═╝ // ┬ ┌─┐ ┌─┐┌─┐┌┬┐┌┬┐┬┌┐┌┌─┐┌─┐ ┌┬┐┬ ┬┌─┐┌┬┐ ┌┬┐┌─┐┬┌─┌─┐ // │ ├┤ └─┐├┤ │ │ │││││ ┬└─┐ │ ├─┤├─┤ │ │ ├─┤├┴┐├┤ // ooo ┴o└─┘o └─┘└─┘ ┴ ┴ ┴┘└┘└─┘└─┘ ┴ ┴ ┴┴ ┴ ┴ ┴ ┴ ┴┴ ┴└─┘ // ┌─┐┬─┐┌─┐┌─┐┌─┐┌┬┐┌─┐┌┐┌┌─┐┌─┐ ┌─┐┬ ┬┌─┐┬─┐ ┌─┐┌┬┐┌─┐┌┐┌┌┬┐┌─┐┬─┐┌┬┐ // ├─┘├┬┘├┤ │ ├┤ ││├┤ ││││ ├┤ │ │└┐┌┘├┤ ├┬┘ └─┐ │ ├─┤│││ ││├─┤├┬┘ ││ // ┴ ┴└─└─┘└─┘└─┘─┴┘└─┘┘└┘└─┘└─┘ └─┘ └┘ └─┘┴└─ └─┘ ┴ ┴ ┴┘└┘─┴┘┴ ┴┴└──┴┘ // ┌─┐┬ ┬┬ ┬┌┐┌┬┌─┌─┐ ┌─┐┌─┐ ┌┬┐┬ ┬┌─┐ ┌─┐┌─┐┌┐┌┌┐┌┌─┐┌─┐┌┬┐┬┌─┐┌┐┌ ┬ ┬┬─┐┬ // │ ├─┤│ ││││├┴┐└─┐ │ │├┤ │ ├─┤├┤ │ │ │││││││├┤ │ │ ││ ││││ │ │├┬┘│ // └─┘┴ ┴└─┘┘└┘┴ ┴└─┘ └─┘└ ┴ ┴ ┴└─┘ └─┘└─┘┘└┘┘└┘└─┘└─┘ ┴ ┴└─┘┘└┘ └─┘┴└─┴─┘ try { if (hasUserOverride) { dsConfig.user = normalizeUser(dsConfig.user); } if (hasPasswordOverride) { dsConfig.password = normalizePassword(dsConfig.password); } if (hasHostOverride) { dsConfig.host = normalizeHost(dsConfig.host); } if (hasPortOverride) { dsConfig.port = normalizePort(dsConfig.port); } if (hasDatabaseOverride) { dsConfig.database = normalizeDatabase(dsConfig.database); } } catch (e) { switch (e.code) { case 'E_BAD_CONFIG': throw flaverr( 'E_BAD_CONFIG', new Error( `Invalid override specified. ${e.message}\n` + '--\n' + 'Please correct this and try again... Or better yet, specify a `url`! ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); default: throw e; } } // </catch> // Strip out any overrides w/ undefined values. // (And along the way, check overrides against whitelist if relevant) let unrecognizedKeys; _.each(Object.keys(dsConfig), (key) => { if (_.isUndefined(dsConfig[key])) { delete dsConfig[key]; } if ( whitelist && !_.contains(whitelist, key) && !_.contains(BASELINE_PROPS, key) ) { unrecognizedKeys = unrecognizedKeys || []; unrecognizedKeys.push(key); } }); if (unrecognizedKeys) { throw flaverr( 'E_BAD_CONFIG', new Error( `Unrecognized options (\`${unrecognizedKeys}\`) specified as config overrides.\n` + 'This adapter expects only whitelisted properties.\n' + '--\n' + 'See http://sailsjs.com/config/datastores#?the-connection-url for info,\n' + 'or visit https://sailsjs.com/support for more help.' ) ); } // ┬ ┬┌─┐┌┐┌┌┬┐┬ ┌─┐ ┌─┐┌┐ ┌─┐┌─┐┌┐┌┌─┐┌─┐ ┌─┐┌─┐ ┬ ┬┬─┐┬ // ├─┤├─┤│││ │││ ├┤ ├─┤├┴┐└─┐├┤ ││││ ├┤ │ │├┤ │ │├┬┘│ // ┴ ┴┴ ┴┘└┘─┴┘┴─┘└─┘ ┴ ┴└─┘└─┘└─┘┘└┘└─┘└─┘ └─┘└ └─┘┴└─┴─┘ // ┌─ ┌┐ ┌─┐┌─┐┬┌─┬ ┬┌─┐┬─┐┌┬┐┌─┐ ┌─┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┬┌┐ ┬┬ ┬┌┬┐┬ ┬ ─┐ // │─── ├┴┐├─┤│ ├┴┐│││├─┤├┬┘ ││└─┐───│ │ ││││├─┘├─┤ │ │├┴┐││ │ │ └┬┘ ───│ // └─ └─┘┴ ┴└─┘┴ ┴└┴┘┴ ┴┴└──┴┘└─┘ └─┘└─┘┴ ┴┴ ┴ ┴ ┴ ┴└─┘┴┴─┘┴ ┴ ┴ ─┘ // If a URL config value was not given, ensure that all the various pieces // needed to create one exist. Then build a URL and attach it to the datastore config. if (!hasUrl) { // FUTURE: Appropriately URL/URIComponent-encode this stuff as we build the url // Invent a connection URL on the fly. let inventedUrl = `${expectedProtocolPrefix || 'db'}://`; // If authentication info was specified, add it: if (hasPasswordOverride && hasUserOverride) { inventedUrl += `${dsConfig.user}:${dsConfig.password}@`; } else if (!hasPasswordOverride && hasUserOverride) { inventedUrl += `${dsConfig.user}@`; } else if (hasPasswordOverride && !hasUserOverride) { throw flaverr( 'E_BAD_CONFIG', new Error( 'No `url` was specified, so tried to infer an appropriate connection URL from other properties. ' + 'However, it looks like a `password` was specified, but no `user` was specified to go along with it.\n' + '--\n' + 'Please remove `password` or also specify a `user`. Or better yet, specify a `url`! ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // If a host was specified, use it. if (hasHostOverride) { inventedUrl += dsConfig.host; } else { throw flaverr( 'E_BAD_CONFIG', new Error( `${ 'No `url` was specified, and no appropriate connection URL can be inferred (tried to use ' + '`host: ' }${util.inspect(dsConfig.host)}\`).\n` + '--\n' + 'Please specify a `host`... Or better yet, specify a `url`! ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); // Or alternatively... // ``` // inventedUrl += 'localhost'; // ``` } // If a port was specified, use it. if (hasPortOverride) { inventedUrl += `:${dsConfig.port}`; } // If a database was specified, use it. if (hasDatabaseOverride) { inventedUrl += `/${dsConfig.database}`; } else { throw flaverr( 'E_BAD_CONFIG', new Error( `${ 'No `url` was specified, and no appropriate connection URL can be inferred (tried to use ' + '`database: ' }${util.inspect(dsConfig.database)}\`).\n` + '--\n' + 'Please specify a `database`... Or better yet, specify a `url`! ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Log a compatibility warning..? Maybe. // // > To help standardize configuration for end users, adapter authors // > are encouraged to support the `url` setting, if conceivable. // > // > Read more here: // > http://sailsjs.com/config/datastores#?the-connection-url // - - - - - - - - - - - - - - - - - - - - - - - - // Now save our invented URL as `url`. dsConfig.url = inventedUrl; } // ┌─┐┌─┐┬─┐┌─┐┌─┐ ┬ ┌┐┌┌─┐┬─┐┌┬┐┌─┐┬ ┬┌─┐┌─┐ ┌─┐┌─┐┌┐┌┌┐┌┌─┐┌─┐┌┬┐┬┌─┐┌┐┌ ╦ ╦╦═╗╦ // ├─┘├─┤├┬┘└─┐├┤ ┌┼─ ││││ │├┬┘│││├─┤│ │┌─┘├┤ │ │ │││││││├┤ │ │ ││ ││││ ║ ║╠╦╝║ // ┴ ┴ ┴┴└─└─┘└─┘ └┘ ┘└┘└─┘┴└─┴ ┴┴ ┴┴─┘┴└─┘└─┘ └─┘└─┘┘└┘┘└┘└─┘└─┘ ┴ ┴└─┘┘└┘ ╚═╝╩╚═╩═╝ // Otherwise, normalize & parse the connection URL. else { // Perform a basic sanity check & string coercion. if (!_.isString(dsConfig.url) || dsConfig.url === '') { throw flaverr( 'E_BAD_CONFIG', new Error( 'Invalid `url` specified. Must be a non-empty string.\n' + '--\n' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // Before beginning to do further string parsing, look for a little loophole: // If the given URL includes a comma, we'll assume it's a complex URL like you get // from Mongo Atlas and trust that it contains all the info we need. // // > But note that this _does not_ handle automatically attaching host, password, // > port, etc. to the `dsConfig` dictionary. It also doesn't do any verification // > of these aspects of the URL, which means it could be entirely invalid. if (dsConfig.url.indexOf(',') > -1) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: Implement explicit parsing for this kind of URL instead of just bailing silently. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - return; } // • // IWMIH, this is the general case where we're actually going to validate the URL like normal. // First, make sure there's a protocol: // We don't actually care about the protocol... but the underlying library (e.g. `mongodb`) might. // Plus, more importantly, Node's `url.parse()` returns funky results if the argument doesn't // have one. So we'll add one if necessary. // > See https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax let urlToParse; if (dsConfig.url.match(/^:\/\//)) { urlToParse = dsConfig.url.replace( /^:\/\//, `${expectedProtocolPrefix || 'db'}://` ); } else if (!dsConfig.url.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) { urlToParse = `${expectedProtocolPrefix || 'db'}://${dsConfig.url}`; } else { urlToParse = dsConfig.url; } // console.log('dsConfig.url',dsConfig.url); // console.log('\n\n**********\nurl to parse:',urlToParse, (new Error()).stack); // Now attempt to parse out the URL's pieces and validate each one. const parsedConnectionStr = url.parse(urlToParse); // Ensure a valid protocol. // Validate that a protocol was found before other pieces // (otherwise other parsed info could be very weird and wrong) if (!parsedConnectionStr.protocol) { throw flaverr( 'E_BAD_CONFIG', new Error( `Could not parse provided URL (${util.inspect(dsConfig.url, { depth: 5, })}).\n` + '(If you continue to experience issues, try checking that the URL begins with an ' + 'appropriate protocol; e.g. `mysql://` or `mongo://`.\n' + '--\n' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // If relevant, validate that the RIGHT protocol was found. if (expectedProtocolPrefix) { if (parsedConnectionStr.protocol !== `${expectedProtocolPrefix}:`) { throw flaverr( 'E_BAD_CONFIG', new Error( `Provided URL (${util.inspect(dsConfig.url, { depth: 5, })}) has an invalid protocol.\n` + `If included, the protocol must be "${expectedProtocolPrefix}://".\n` + '--\n' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } } // >- // Parse authentication credentials from url, if specified. let userInUrl; let passwordInUrl; if (parsedConnectionStr.auth && _.isString(parsedConnectionStr.auth)) { const authPieces = parsedConnectionStr.auth.split(/:/); if (authPieces[0]) { // eslint-disable-next-line prefer-destructuring userInUrl = authPieces[0]; } // >- if (authPieces[1]) { // eslint-disable-next-line prefer-destructuring passwordInUrl = authPieces[1]; } } // Parse the rest of the standard information from the URL. const hostInUrl = parsedConnectionStr.hostname; const portInUrl = parsedConnectionStr.port; let databaseInUrl = parsedConnectionStr.pathname; // And finally parse the non-standard info from the URL's querystring. let miscOptsInUrlQs; try { miscOptsInUrlQs = qs.parse(parsedConnectionStr.query); } catch (e) { throw flaverr( 'E_BAD_CONFIG', new Error( `Could not parse query string from URL: \`${dsConfig.url}\`. ` + `Details: ${e.stack}` ) ); } // Now normalize + restore parsed values back into overrides. // > • Note that we prefer overrides to URL data here. // > • Also remember that overrides have already been normalized/validated above. // > • And finally, also note that we enforce the whitelist for non-standard props // > here, if relevant. This is so we can provide a clear error message about where // > the whitelist violation came from. try { if (userInUrl && !hasUserOverride) { dsConfig.user = normalizeUser(userInUrl); } if (passwordInUrl && !hasPasswordOverride) { dsConfig.password = normalizePassword(passwordInUrl); } if (hostInUrl && !hasHostOverride) { dsConfig.host = normalizeHost(hostInUrl); } if (portInUrl && !hasPortOverride) { dsConfig.port = normalizePort(portInUrl); } if (databaseInUrl && !hasDatabaseOverride) { databaseInUrl = _.trim(databaseInUrl, '/'); dsConfig.database = normalizeDatabase(databaseInUrl); } _.each(miscOptsInUrlQs, (val, key) => { if (whitelist && !_.contains(whitelist, key)) { throw flaverr( 'E_BAD_CONFIG', new Error( `Unrecognized option (\`${key}\`) specified in query string of connection URL.\n` + '(This adapter expects only standard, whitelisted properties.)\n' + '--\n' + 'See http://sailsjs.com/config/datastores#?the-connection-url for info, or visit\n)' + 'https://sailsjs.com/support for more help.' ) ); } if (_.contains(BASELINE_PROPS, key)) { // Currently, we ignore these-- we'll technically put them back in the URL later below, // but we're careful not to also stick them at the top level (because they would interfere // with other reserved properties). // // We're leaving it this way for now in the interest of flexibility. But there's another way: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: consider bringing this back instead, for clarity: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // throw flaverr('E_BAD_CONFIG', new Error( // 'Unexpected option (`'+key+'`) is NEVER allowed in the query string of a connection URL.\n'+ // '--\n'+ // 'See http://sailsjs.com/config/datastores#?the-connection-url for info, or visit\n)'+ // 'https://sailsjs.com/support for more help.' // )); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } else if (_.isUndefined(dsConfig[key])) { dsConfig[key] = val; } }); // </_.each()> } catch (e) { switch (e.code) { case 'E_BAD_CONFIG': throw flaverr( 'E_BAD_CONFIG', new Error( `Could not process connection url. ${e.message}\n` + '--\n' + 'Please correct this and try again.\n' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); default: throw e; } } // </catch> // And finally, rebuild the URL let rebuiltUrl = ''; // Start with the protocol... rebuiltUrl += `${parsedConnectionStr.protocol}//`; // If user/password were specified in the url OR as overrides, use them. if (dsConfig.user && dsConfig.password) { rebuiltUrl += `${dsConfig.user}:${dsConfig.password}@`; } else if (!dsConfig.password && dsConfig.user) { rebuiltUrl += `${dsConfig.user}@`; } else if (dsConfig.password && !dsConfig.user) { throw flaverr( 'E_BAD_CONFIG', new Error( 'It looks like a `password` was specified, but no `user` was specified to go along with it.\n' + '--\n' + 'Please remove `password` or also specify a `user`. ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // If a host was specified in the url OR as an override, use it. // (prefer override) if (dsConfig.host) { rebuiltUrl += dsConfig.host; } else { throw flaverr( 'E_BAD_CONFIG', new Error( `${ 'No host could be determined from configuration (tried to use ' + '`host: ' }${util.inspect(dsConfig.host)}\`).\n` + '--\n' + 'Please specify a `host` or, better yet, include it in the `url`. ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); // Or alternatively... // ``` // rebuiltUrl += 'localhost'; // ``` } // If a port was specified in the url OR as an override, use it. // (prefer override) if (dsConfig.port) { rebuiltUrl += `:${dsConfig.port}`; } // If a database was specified in the url OR as an override, use it. // (prefer override) if (dsConfig.database) { rebuiltUrl += `/${dsConfig.database}`; } else { throw flaverr( 'E_BAD_CONFIG', new Error( `${ 'No database could be determined from configuration (tried to use ' + '`database: ' }${util.inspect(dsConfig.database)}\`).\n` + '--\n' + 'Please specify a `database` or, better yet, include it in the `url`. ' + '(See http://sailsjs.com/config/datastores#?the-connection-url for more info.)' ) ); } // Reattach any non-standard querystring options from the URL. // > If there were any non-standard options, we'll **LEAVE THEM IN** the URL // > when we rebuild it. But note that we did fold them into the dsConfig // > dictionary as well earlier. const newQs = qs.stringify(miscOptsInUrlQs); if (newQs.length > 0) { rebuiltUrl += `?${newQs}`; } // Now save our rebuilt URL as `url`. dsConfig.url = rebuiltUrl; } };