// ██╗ ██╗ █████╗ ████████╗███████╗██████╗ ██╗ ██╗███╗ ██╗███████╗ // ██║ ██║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗██║ ██║████╗ ██║██╔════╝ // ██║ █╗ ██║███████║ ██║ █████╗ ██████╔╝██║ ██║██╔██╗ ██║█████╗ // ██║███╗██║██╔══██║ ██║ ██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║██╔══╝ // ╚███╔███╔╝██║ ██║ ██║ ███████╗██║ ██║███████╗██║██║ ╚████║███████╗ // ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝ // var assert = require('assert'); var util = require('util'); var _ = require('@sailshq/lodash'); var async = require('async'); // var EA = require('encrypted-attr'); « this is required below for node compat. var flaverr = require('flaverr'); var Schema = require('waterline-schema'); var buildDatastoreMap = require('./waterline/utils/system/datastore-builder'); var buildLiveWLModel = require('./waterline/utils/system/collection-builder'); var BaseMetaModel = require('./waterline/MetaModel'); var getModel = require('./waterline/utils/ontology/get-model'); /** * ORM (Waterline) * * Construct a Waterline ORM instance. * * @constructs {Waterline} */ function Waterline() { // Start by setting up an array of model definitions. // (This will hold the raw model definitions that were passed in, // plus any implicitly introduced models-- but that part comes later) // // > `wmd` stands for "weird intermediate model def thing". // - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: make this whole wmd thing less weird. // - - - - - - - - - - - - - - - - - - - - - - - - var wmds = []; // Hold a map of the instantaited and active datastores and models. var modelMap = {}; var datastoreMap = {}; // This "context" dictionary will be passed into the BaseMetaModel constructor // later every time we instantiate a new BaseMetaModel instance (e.g. `User` // or `Pet` or generically, sometimes called "WLModel" -- sorry about the // capital letters!!) // var context = { collections: modelMap, datastores: datastoreMap }; // ^^FUTURE: Level this out (This is currently just a stop gap to prevent // re-writing all the "collection query" stuff.) // Now build an ORM instance. var orm = {}; // ┌─┐─┐ ┬┌─┐┌─┐┌─┐┌─┐ ┌─┐┬─┐┌┬┐ ╦═╗╔═╗╔═╗╦╔═╗╔╦╗╔═╗╦═╗╔╦╗╔═╗╔╦╗╔═╗╦ // ├┤ ┌┴┬┘├─┘│ │└─┐├┤ │ │├┬┘│││ ╠╦╝║╣ ║ ╦║╚═╗ ║ ║╣ ╠╦╝║║║║ ║ ║║║╣ ║ // └─┘┴ └─┴ └─┘└─┘└─┘ └─┘┴└─┴ ┴o╩╚═╚═╝╚═╝╩╚═╝ ╩ ╚═╝╩╚═╩ ╩╚═╝═╩╝╚═╝╩═╝ /** * .registerModel() * * Register a "weird intermediate model definition thing". (see above) * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * FUTURE: Deprecate support for this method in favor of simplified `Waterline.start()` * (see bottom of this file). In WL 1.0, remove this method altogether. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Dictionary} wmd */ orm.registerModel = function registerModel(wmd) { wmds.push(wmd); }; // Alias for backwards compatibility: orm.loadCollection = function heyThatsDeprecated(){ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Change this alias method so that it throws an error in WL 0.14. // (And in WL 1.0, just remove it altogether.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - console.warn('\n'+ 'Warning: As of Waterline 0.13, `loadCollection()` is now `registerModel()`. Please call that instead.\n'+ 'I get what you mean, so I temporarily renamed it for you this time, but here is a stack trace\n'+ 'so you know where this is coming from in the code, and can change it to prevent future warnings:\n'+ '```\n'+ (new Error()).stack+'\n'+ '```\n' ); orm.registerModel.apply(orm, Array.prototype.slice.call(arguments)); }; // ┌─┐─┐ ┬┌─┐┌─┐┌─┐┌─┐ ┌─┐┬─┐┌┬┐ ╦╔╗╔╦╔╦╗╦╔═╗╦ ╦╔═╗╔═╗ // ├┤ ┌┴┬┘├─┘│ │└─┐├┤ │ │├┬┘│││ ║║║║║ ║ ║╠═╣║ ║╔═╝║╣ // └─┘┴ └─┴ └─┘└─┘└─┘ └─┘┴└─┴ ┴o╩╝╚╝╩ ╩ ╩╩ ╩╩═╝╩╚═╝╚═╝ /** * .initialize() * * Start the ORM and set up active datastores. * * @param {Dictionary} options * @param {Function} done */ orm.initialize = function initialize(options, done) { try { // First, verify traditional settings, check compat.: // ============================================================================================= // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: In WL 0.14, deprecate support for this method in favor of the simplified // `Waterline.start()` (see bottom of this file). In WL 1.0, remove it altogether. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Ensure the ORM hasn't already been initialized. // (This prevents all sorts of issues, because model definitions are modified in-place.) if (_.keys(modelMap).length > 0) { throw new Error('A Waterline ORM instance cannot be initialized more than once. To reset the ORM, create a new instance of it by running `new Waterline()`.'); } // Backwards-compatibility for `connections`: if (!_.isUndefined(options.connections)){ // Sanity check assert(_.isUndefined(options.datastores), 'Attempted to provide backwards-compatibility for `connections`, but `datastores` was ALSO defined! This should never happen.'); options.datastores = options.connections; console.warn('\n'+ 'Warning: `connections` is no longer supported. Please use `datastores` instead.\n'+ 'I get what you mean, so I temporarily renamed it for you this time, but here is a stack trace\n'+ 'so you know where this is coming from in the code, and can change it to prevent future warnings:\n'+ '```\n'+ (new Error()).stack+'\n'+ '```\n' ); delete options.connections; }//>- // Usage assertions if (_.isUndefined(options) || !_.keys(options).length) { throw new Error('Usage Error: .initialize(options, callback)'); } if (_.isUndefined(options.adapters) || !_.isPlainObject(options.adapters)) { throw new Error('Options must contain an `adapters` dictionary'); } if (_.isUndefined(options.datastores) || !_.isPlainObject(options.datastores)) { throw new Error('Options must contain a `datastores` dictionary'); } // - - - - - - - - - - - - - - - - - - - - - // FUTURE: anchor ruleset checks // - - - - - - - - - - - - - - - - - - - - - // Next, validate ORM settings related to at-rest encryption, if it is in use. // ============================================================================================= var areAnyModelsUsingAtRestEncryption; _.each(wmds, function(wmd){ _.each(wmd.prototype.attributes, function(attrDef){ if (attrDef.encrypt !== undefined) { areAnyModelsUsingAtRestEncryption = true; } });//∞ });//∞ // Only allow using at-rest encryption for compatible Node versions var EA; if (areAnyModelsUsingAtRestEncryption) { var RX_NODE_MAJOR_DOT_MINOR = /^v([^.]+\.?[^.]+)\./; var parsedNodeMajorAndMinorVersion = process.version.match(RX_NODE_MAJOR_DOT_MINOR) && (+(process.version.match(RX_NODE_MAJOR_DOT_MINOR)[1])); var MIN_NODE_VERSION = 6; var isNativeCryptoFullyCapable = parsedNodeMajorAndMinorVersion >= MIN_NODE_VERSION; if (!isNativeCryptoFullyCapable) { throw new Error('Current installed node version\'s native `crypto` module is not fully capable of the necessary functionality for encrypting/decrypting data at rest with Waterline. To use this feature, please upgrade to Node v' + MIN_NODE_VERSION + ' or above, flush your node_modules, run npm install, and then try again. Otherwise, if you cannot upgrade Node.js, please remove the `encrypt` property from your models\' attributes.'); } EA = require('encrypted-attr'); }//fi _.each(wmds, function(wmd){ var modelDef = wmd.prototype; // Verify that `encrypt` attr prop is valid, if in use. var isThisModelUsingAtRestEncryption; try { _.each(modelDef.attributes, function(attrDef, attrName){ if (attrDef.encrypt !== undefined) { if (!_.isBoolean(attrDef.encrypt)){ throw flaverr({ code: 'E_INVALID_ENCRYPT', attrName: attrName, message: 'If set, `encrypt` must be either `true` or `false`.' }); }//• if (attrDef.encrypt === true){ isThisModelUsingAtRestEncryption = true; if (attrDef.type === 'ref') { throw flaverr({ code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION', attrName: attrName, whyNotCompatible: 'with `type: \'ref\'` attributes.' }); }//• if (attrDef.autoCreatedAt || attrDef.autoUpdatedAt) { throw flaverr({ code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION', attrName: attrName, whyNotCompatible: 'with `'+(attrDef.autoCreatedAt?'autoCreatedAt':'autoUpdatedAt')+'` attributes.' }); }//• if (attrDef.model || attrDef.collection) { throw flaverr({ code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION', attrName: attrName, whyNotCompatible: 'with associations.' }); }//• if (attrDef.defaultsTo !== undefined) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Consider adding support for this. Will require some refactoring // in order to do it right (i.e. otherwise we'll just be copying and pasting // the encryption logic.) We'll want to pull it out from normalize-value-to-set // into a new utility, then call that from the appropriate spot in // normalize-new-record in order to encrypt the initial default value. // // (See also the other note in normalize-new-record re defaultsTo + cloneDeep.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - throw flaverr({ code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION', attrName: attrName, whyNotCompatible: 'with an attribute that also specifies a `defaultsTo`. '+ 'Please remove the `defaultsTo` from this attribute definition.' }); }//• }//fi }//fi });//∞ } catch (err) { switch (err.code) { case 'E_INVALID_ENCRYPT': throw flaverr({ message: 'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+ '`'+err.attrName+'` attribute. '+err.message }, err); case 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION': throw flaverr({ message: 'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+ '`'+err.attrName+'` attribute. At-rest encryption (`encrypt: true`) cannot be used '+ err.whyNotCompatible }, err); default: throw err; } } // Verify `dataEncryptionKeys`. // (Remember, if there is a secondary key system in use, these DEKs should have // already been "unwrapped" before they were passed in to Waterline as model settings.) if (modelDef.dataEncryptionKeys !== undefined) { if (!_.isObject(modelDef.dataEncryptionKeys) || _.isArray(modelDef.dataEncryptionKeys) || _.isFunction(modelDef.dataEncryptionKeys)) { throw flaverr({ message: 'In the definition for the `'+modelDef.identity+'` model, the `dataEncryptionKeys` model setting '+ 'is invalid. If specified, `dataEncryptionKeys` must be a dictionary (plain JavaScript object).' }); }//• // Check all DEKs for validity. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // (FUTURE: maybe extend EA to support a `validateKeys()` method instead of this-- // or at least to have error code) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - try { _.each(modelDef.dataEncryptionKeys, function(dek, dekId){ if (!dek || !_.isString(dek)) { throw flaverr({ code: 'E_INVALID_DATA_ENCRYPTION_KEYS', dekId: dekId, message: 'Must be a cryptographically random, 32 byte string.' }); }//• if (!dekId.match(/^[a-z\$]([a-z0-9])*$/i)){ throw flaverr({ code: 'E_INVALID_DATA_ENCRYPTION_KEYS', dekId: dekId, message: 'Please make sure the ids of all of your data encryption keys begin with a letter and do not contain any special characters.' }); }//• if (areAnyModelsUsingAtRestEncryption) { try { EA(undefined, { keys: modelDef.dataEncryptionKeys, keyId: dekId }).encryptAttribute(undefined, 'test-value-purely-for-validation'); } catch (err) { throw flaverr({ code: 'E_INVALID_DATA_ENCRYPTION_KEYS', dekId: dekId }, err); } } });//∞ } catch (err) { switch (err.code) { case 'E_INVALID_DATA_ENCRYPTION_KEYS': throw flaverr({ message: 'In the definition for the `'+modelDef.identity+'` model, one of the data encryption keys (`dataEncryptionKeys.'+err.dekId+'`) is invalid.\n'+ 'Details:\n'+ ' '+err.message }, err); default: throw err; } } }//fi // If any attrs have `encrypt: true`, verify that there is both a valid // `dataEncryptionKeys` dictionary and a valid `dataEncryptionKeys.default` DEK set. if (isThisModelUsingAtRestEncryption) { if (!modelDef.dataEncryptionKeys || !modelDef.dataEncryptionKeys.default) { throw flaverr({ message: 'DEKs should be 32 bytes long, and cryptographically random. A random, default DEK is included '+ 'in new Sails apps, so one easy way to generate a new DEK is to generate a new Sails app. '+ 'Alternatively, you could run:\n'+ ' require(\'crypto\').randomBytes(32).toString(\'base64\')\n'+ '\n'+ 'Remember: once in production, you should manage your DEKs like you would any other sensitive credential. '+ 'For example, one common best practice is to configure them using environment variables.\n'+ 'In a Sails app:\n'+ ' sails_models__dataEncryptionKeys__default=vpB2EhXaTi+wYKUE0ojI5cVQX/VRGP++Fa0bBW/NFSs=\n'+ '\n'+ ' [?] If you\'re unsure or want advice, head over to https://sailsjs.com/support' }); }//• }//fi });//∞ // Next, set up support for the default archive, and validate related settings: // ============================================================================================= var DEFAULT_ARCHIVE_MODEL_IDENTITY = 'archive'; // Notes for use in docs: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // • To choose which datastore the Archive model will live in: // // …in top-level orm settings: // archiveModelIdentity: 'myarchive', // // …in 'MyArchive' model: // datastore: 'foo' // // // • To choose the `tableName` and `columnName`s for your Archive model: // …in top-level orm settings: // archiveModelIdentity: 'archive', // // …in 'archive' model: // tableName: 'foo', // attributes: { // originalRecord: { type: 'json', columnName: 'barbaz' }, // fromModel: { type: 'string', columnName: 'bingbong' } // } // // // • To disable support for the `.archive()` model method: // // …in top-level orm settings: // archiveModelIdentity: false // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var archiversInfoByArchiveIdentity = {}; _.each(wmds, function(wmd){ var modelDef = wmd.prototype; // console.log('· checking `'+util.inspect(wmd,{depth:null})+'`…'); // console.log('· checking `'+modelDef.identity+'`…'); // Check the `archiveModelIdentity` model setting. if (modelDef.archiveModelIdentity === undefined) { if (modelDef.archiveModelIdentity !== modelDef.identity) { // console.log('setting default archiveModelIdentity for model `'+modelDef.identity+'`…'); modelDef.archiveModelIdentity = DEFAULT_ARCHIVE_MODEL_IDENTITY; } else { // A model can't be its own archive model! modelDef.archiveModelIdentity = false; } }//fi if (modelDef.archiveModelIdentity === false) { // This will cause the .archive() method for this model to error out and explain // that the feature was explicitly disabled. } else if (modelDef.archiveModelIdentity === modelDef.identity) { return done(new Error('Invalid `archiveModelIdentity` setting. A model cannot be its own archive! But model `'+modelDef.identity+'` has `archiveModelIdentity: \''+modelDef.archiveModelIdentity+'\'`.')); } else if (!modelDef.archiveModelIdentity || !_.isString(modelDef.archiveModelIdentity)){ return done(new Error('Invalid `archiveModelIdentity` setting. If set, expecting either `false` (to disable .archive() altogether) or the identity of a registered model (e.g. "archive"), but instead got: '+util.inspect(options.defaults.archiveModelIdentity,{depth:null}))); }//fi // Keep track of the model identities of all archive models, as well as info about the models using them. if (modelDef.archiveModelIdentity !== false) { if (!_.contains(Object.keys(archiversInfoByArchiveIdentity), modelDef.archiveModelIdentity)) { // Save an initial info dictionary: archiversInfoByArchiveIdentity[modelDef.archiveModelIdentity] = { archivers: [] }; }//fi archiversInfoByArchiveIdentity[modelDef.archiveModelIdentity].archivers.push(modelDef); }//fi });//∞ // If any models are using the default archive, then register the default archive model // if it isn't already registered. if (_.contains(Object.keys(archiversInfoByArchiveIdentity), DEFAULT_ARCHIVE_MODEL_IDENTITY)) { // Inject the built-in Archive model into the ORM's ontology: // • id (pk-- string or number, depending on where the Archive model is being stored) // • createdAt (timestamp-- this is effectively ≈ "archivedAt") // • originalRecord (json-- the original record, completely unpopulated) // • originalRecordId (pk-- string or number, the pk of the original record) // • fromModel (string-- the original model identity) // // > Note there's no updatedAt! var existingDefaultArchiveWmd = _.find(wmds, function(wmd){ return wmd.prototype.identity === DEFAULT_ARCHIVE_MODEL_IDENTITY; }); if (!existingDefaultArchiveWmd) { var defaultArchiversInfo = archiversInfoByArchiveIdentity[DEFAULT_ARCHIVE_MODEL_IDENTITY]; // Arbitrarily pick the first archiver. // (we'll use this to derive a datastore and pk style so that they both match) var arbitraryArchiver = defaultArchiversInfo.archivers[0]; // console.log('arbitraryArchiver', arbitraryArchiver); var newWmd = Waterline.Model.extend({ identity: DEFAULT_ARCHIVE_MODEL_IDENTITY, // > Note that we inject a "globalId" for potential use in higher-level frameworks (e.g. Sails) // > that might want to globalize this model. This way, it'd show up as "Archive" instead of "archive". // > Remember: Waterline is NOT responsible for any globalization itself, this is just advisory. globalId: _.capitalize(DEFAULT_ARCHIVE_MODEL_IDENTITY), primaryKey: 'id', datastore: arbitraryArchiver.datastore, attributes: { id: arbitraryArchiver.attributes[arbitraryArchiver.primaryKey], createdAt: { type: 'number', autoCreatedAt: true, autoMigrations: { columnType: '_numbertimestamp' } }, fromModel: { type: 'string', required: true, autoMigrations: { columnType: '_string' } }, originalRecord: { type: 'json', required: true, autoMigrations: { columnType: '_json' } }, // Use `type:'json'` for this: // (since it might contain pks for records from different datastores) originalRecordId: { type: 'json', autoMigrations: { columnType: '_json' } }, } }); wmds.push(newWmd); }//fi }//fi // Now make sure all archive models actually exist, and that they're valid. _.each(archiversInfoByArchiveIdentity, function(archiversInfo, archiveIdentity) { var archiveWmd = _.find(wmds, function(wmd){ return wmd.prototype.identity === archiveIdentity; }); if (!archiveWmd) { throw new Error('Invalid `archiveModelIdentity` setting. A model declares `archiveModelIdentity: \''+archiveIdentity+'\'`, but there\'s no other model actually registered with that identity to use as an archive!'); } // Validate that this archive model can be used for the purpose of Waterline's .archive() // > (note that the error messages here should be considerate of the case where someone is // > upgrading their app from an older version of Sails/Waterline and might happen to have // > a model named "Archive".) var EXPECTED_ATTR_NAMES = ['id', 'createdAt', 'fromModel', 'originalRecord', 'originalRecordId']; var actualAttrNames = _.keys(archiveWmd.prototype.attributes); var namesOfMissingAttrs = _.difference(EXPECTED_ATTR_NAMES, actualAttrNames); try { if (namesOfMissingAttrs.length > 0) { throw flaverr({ code: 'E_INVALID_ARCHIVE_MODEL', because: 'it is missing '+ namesOfMissingAttrs.length+' mandatory attribute'+(namesOfMissingAttrs.length===1?'':'s')+': '+namesOfMissingAttrs+'.' }); }//• if (archiveWmd.prototype.primaryKey !== 'id') { throw flaverr({ code: 'E_INVALID_ARCHIVE_MODEL', because: 'it is using an attribute other than `id` as its logical primary key attribute.' }); }//• if (_.any(EXPECTED_ATTR_NAMES, { encrypt: true })) { throw flaverr({ code: 'E_INVALID_ARCHIVE_MODEL', because: 'it is using at-rest encryption on one of its mandatory attributes, when it shouldn\'t be.' }); }//• // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: do more checks (there's a lot of things we should probably check-- e.g. the `type` of each // mandatory attribute, that no crazy defaultsTo is provided, that the auto-timestamp is correct, etc.) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } catch (err) { switch (err.code) { case 'E_INVALID_ARCHIVE_MODEL': throw new Error( 'The `'+archiveIdentity+'` model cannot be used as a custom archive, because '+err.because+'\n'+ 'Please adjust this custom archive model accordingly, or otherwise switch to a different '+ 'model as your custom archive. (For reference, this `'+archiveIdentity+'` model this is currently '+ 'configured as the custom archive model for '+archiversInfo.archivers.length+' other '+ 'model'+(archiversInfo.archivers.length===1?'':'s')+': '+_.pluck(archiversInfo.archivers, 'identity')+'.' ); default: throw err; } } });//∞ // Build up a dictionary of datastores (used by our models?) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: verify the last part of that statement ^^ (not seeing how this is related to "used by our models") // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ================================================================= try { datastoreMap = buildDatastoreMap(options.adapters, options.datastores); } catch (err) { throw err; } // Now check out the models and build a schema map (using wl-schema) // ================================================================= var internalSchema; try { internalSchema = new Schema(wmds, options.defaults); } catch (err) { throw err; } // Check the internal "schema map" for any junction models that were // implicitly introduced above and handle them. _.each(_.keys(internalSchema), function(table) { if (internalSchema[table].junctionTable) { // Whenever one is found, flag it as `_private: true` and generate // a custom constructor for it (based on a clone of the `BaseMetaModel` // constructor), then push it on to our set of wmds. internalSchema[table]._private = true; wmds.push(BaseMetaModel.extend(internalSchema[table])); }//fi });//∞ // Now build live models // ================================================================= // Hydrate each model definition (in-place), and also set up a // reference to it in the model map. _.each(wmds, function (wmd) { // Set the attributes and schema values using the normalized versions from // Waterline-Schema where everything has already been processed. var schemaVersion = internalSchema[wmd.prototype.identity]; // Set normalized values from the schema version on the model definition. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: no need to use a prototype here, so let's avoid it to minimize future boggling // (or if we determine it significantly improves the performance of ORM initialization, then // let's keep it, but document that here and leave a link to the benchmark as a comment) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - wmd.prototype.identity = schemaVersion.identity; wmd.prototype.tableName = schemaVersion.tableName; wmd.prototype.datastore = schemaVersion.datastore; wmd.prototype.primaryKey = schemaVersion.primaryKey; wmd.prototype.meta = schemaVersion.meta; wmd.prototype.attributes = schemaVersion.attributes; wmd.prototype.schema = schemaVersion.schema; wmd.prototype.hasSchema = schemaVersion.hasSchema; // Mixin junctionTable or throughTable if available if (_.has(schemaVersion, 'junctionTable')) { wmd.prototype.junctionTable = schemaVersion.junctionTable; } if (_.has(schemaVersion, 'throughTable')) { wmd.prototype.throughTable = schemaVersion.throughTable; } var WLModel = buildLiveWLModel(wmd, datastoreMap, context); // Store the live Waterline model so it can be used // internally to create other records modelMap[WLModel.identity] = WLModel; }); } catch (err) { return done(err); } // Finally, register datastores. // ================================================================= // Simultaneously register each datastore with the correct adapter. // (This is async because the `registerDatastore` method in adapters // is async. But since they're not interdependent, we run them all in parallel.) async.each(_.keys(datastoreMap), function(datastoreName, next) { var datastore = datastoreMap[datastoreName]; if (_.isFunction(datastore.adapter.registerConnection)) { return next(new Error('The adapter for datastore `' + datastoreName + '` is invalid: the `registerConnection` method must be renamed to `registerDatastore`.')); } try { // Note: at this point, the datastore should always have a usable adapter // set as its `adapter` property. // Check if the datastore's adapter has a `registerDatastore` method if (!_.has(datastore.adapter, 'registerDatastore')) { // FUTURE: get rid of this `setImmediate` (or if it's serving a purpose, document what that is) setImmediate(function() { next(); });//_∏_ return; }//-• // Add the datastore name as the `identity` property in its config. datastore.config.identity = datastoreName; // Get the identities of all the models which use this datastore, and then build up // a simple mapping that can be passed down to the adapter. var usedSchemas = {}; var modelIdentities = _.uniq(datastore.collections); // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // TODO: figure out if we still need this `uniq` or not. If so, document why. // If not, remove it. (hopefully the latter) // // e.g. // ``` // assert(modelIdentities.length === datastore.collections.length); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - _.each(modelIdentities, function(modelIdentity) { var WLModel = modelMap[modelIdentity]; // Track info about this model by table name (for use in the adapter) var tableName; if (_.has(Object.getPrototypeOf(WLModel), 'tableName')) { tableName = Object.getPrototypeOf(WLModel).tableName; } else { tableName = modelIdentity; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: Suck the `getPrototypeOf()` poison out of this stuff. Mike is too dumb for this. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - assert(WLModel.tableName === tableName, 'Expecting `WLModel.tableName === tableName`. (Please open an issue: http://sailsjs.com/bugs)'); assert(WLModel.identity === modelIdentity, 'Expecting `WLModel.identity === modelIdentity`. (Please open an issue: http://sailsjs.com/bugs)'); assert(WLModel.primaryKey && _.isString(WLModel.primaryKey), 'How flabbergasting! Expecting truthy string in `WLModel.primaryKey`, but got something else. (If you\'re seeing this, there\'s probably a bug in Waterline. Please open an issue: http://sailsjs.com/bugs)'); assert(WLModel.schema && _.isObject(WLModel.schema), 'Expecting truthy string in `WLModel.schema`, but got something else. (Please open an issue: http://sailsjs.com/bugs)'); usedSchemas[tableName] = { primaryKey: WLModel.primaryKey, definition: WLModel.schema, tableName: tableName, identity: modelIdentity }; });// // Call the `registerDatastore` adapter method. datastore.adapter.registerDatastore(datastore.config, usedSchemas, next); } catch (err) { return next(err); } }, function(err) { if (err) { return done(err); } // Build up and return the ontology. return done(undefined, { collections: modelMap, datastores: datastoreMap }); });// };// // ┌─┐─┐ ┬┌─┐┌─┐┌─┐┌─┐ ┌─┐┬─┐┌┬┐╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╗╔ // ├┤ ┌┴┬┘├─┘│ │└─┐├┤ │ │├┬┘│││ ║ ║╣ ╠═╣╠╦╝ ║║║ ║║║║║║║ // └─┘┴ └─┴ └─┘└─┘└─┘ └─┘┴└─┴ ┴o╩ ╚═╝╩ ╩╩╚══╩╝╚═╝╚╩╝╝╚╝ orm.teardown = function teardown(done) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: In WL 0.14, deprecate support for this method in favor of the simplified // `Waterline.start()` (see bottom of this file). In WL 1.0, remove it altogether. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - async.each(_.keys(datastoreMap), function(datastoreName, next) { var datastore = datastoreMap[datastoreName]; // Check if the adapter has a teardown method implemented. // If not, then just skip this datastore. if (!_.has(datastore.adapter, 'teardown')) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // FUTURE: get rid of this `setImmediate` (or if it's serving a purpose, document what that is) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setImmediate(function() { next(); });//_∏_ return; }//-• // But otherwise, call its teardown method. try { datastore.adapter.teardown(datastoreName, next); } catch (err) { return next(err); } }, done); }; // ╦═╗╔═╗╔╦╗╦ ╦╦═╗╔╗╔ ┌┐┌┌─┐┬ ┬ ┌─┐┬─┐┌┬┐ ┬┌┐┌┌─┐┌┬┐┌─┐┌┐┌┌─┐┌─┐ // ╠╦╝║╣ ║ ║ ║╠╦╝║║║ │││├┤ │││ │ │├┬┘│││ ││││└─┐ │ ├─┤││││ ├┤ // ╩╚═╚═╝ ╩ ╚═╝╩╚═╝╚╝ ┘└┘└─┘└┴┘ └─┘┴└─┴ ┴ ┴┘└┘└─┘ ┴ ┴ ┴┘└┘└─┘└─┘ return orm; } // Export the Waterline ORM constructor. module.exports = Waterline; // ╔═╗═╗ ╦╔╦╗╔═╗╔╗╔╔═╗╦╔═╗╔╗╔╔═╗ // ║╣ ╔╩╦╝ ║ ║╣ ║║║╚═╗║║ ║║║║╚═╗ // ╚═╝╩ ╚═ ╩ ╚═╝╝╚╝╚═╝╩╚═╝╝╚╝╚═╝ // Expose the generic, stateless BaseMetaModel constructor for direct access from // vanilla Waterline applications (available as `Waterline.Model`) // // > Note that this is technically a "MetaModel", because it will be "newed up" // > into a Waterline model instance (WLModel) like `User`, `Pet`, etc. // > But since, from a userland perspective, there is no real distinction, we // > still expose this as `Model` for the sake of simplicity. module.exports.Model = BaseMetaModel; // Expose `Collection` as an alias for `Model`, but only for backwards compatibility. module.exports.Collection = BaseMetaModel; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ^^FUTURE: In WL 1.0, remove this alias. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Waterline.start() * * Build and initialize a new Waterline ORM instance using the specified * userland ontology, including model definitions, datastore configurations, * and adapters. * * --EXPERIMENTAL-- * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * FUTURE: Have this return a Deferred using parley (so it supports `await`) * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Dictionary} options * @property {Dictionary} models * @property {Dictionary} datastores * @property {Dictionary} adapters * @property {Dictionary?} defaultModelSettings * * @param {Function} done * @param {Error?} err * @param {Ref} orm */ module.exports.start = function (options, done){ // Verify usage & apply defaults: if (!_.isFunction(done)) { throw new Error('Please provide a valid callback function as the 2nd argument to `Waterline.start()`. (Instead, got: `'+done+'`)'); } try { if (!_.isObject(options) || _.isArray(options) || _.isFunction(options)) { throw new Error('Please provide a valid dictionary (plain JS object) as the 1st argument to `Waterline.start()`. (Instead, got: `'+options+'`)'); } if (!_.isObject(options.adapters) || _.isArray(options.adapters) || _.isFunction(options.adapters)) { throw new Error('`adapters` must be provided as a valid dictionary (plain JS object) of adapter definitions, keyed by adapter identity. (Instead, got: `'+options.adapters+'`)'); } if (!_.isObject(options.datastores) || _.isArray(options.datastores) || _.isFunction(options.datastores)) { throw new Error('`datastores` must be provided as a valid dictionary (plain JS object) of datastore configurations, keyed by datastore name. (Instead, got: `'+options.datastores+'`)'); } if (!_.isObject(options.models) || _.isArray(options.models) || _.isFunction(options.models)) { throw new Error('`models` must be provided as a valid dictionary (plain JS object) of model definitions, keyed by model identity. (Instead, got: `'+options.models+'`)'); } if (_.isUndefined(options.defaultModelSettings)) { options.defaultModelSettings = {}; } else if (!_.isObject(options.defaultModelSettings) || _.isArray(options.defaultModelSettings) || _.isFunction(options.defaultModelSettings)) { throw new Error('If specified, `defaultModelSettings` must be a dictionary (plain JavaScript object). (Instead, got: `'+options.defaultModelSettings+'`)'); } var VALID_OPTIONS = ['adapters', 'datastores', 'models', 'defaultModelSettings']; var unrecognizedOptions = _.difference(_.keys(options), VALID_OPTIONS); if (unrecognizedOptions.length > 0) { throw new Error('Unrecognized option(s):\n '+unrecognizedOptions+'\n\nValid options are:\n '+VALID_OPTIONS+'\n'); } // Check adapter identities. _.each(options.adapters, function (adapter, key){ if (_.isUndefined(adapter.identity)) { adapter.identity = key; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Note: We removed the following purely for convenience. // If this comes up again, we should consider bringing it back instead of the more // friendly behavior above. But in the mean time, erring on the side of less typing // in userland by gracefully adjusting the provided adapter def. // ``` // throw new Error('All adapters should declare an `identity`. But the adapter passed in under `'+key+'` has no identity! (Keep in mind that this adapter could get require()-d from somewhere else.)'); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } else if (adapter.identity !== key) { throw new Error('The `identity` explicitly defined on an adapter should exactly match the key under which it is passed in to `Waterline.start()`. But the adapter passed in for key `'+key+'` has an identity that does not match: `'+adapter.identity+'`'); } });// // Now go ahead: start building & initializing the ORM. var orm = new Waterline(); // Register models (checking model identities along the way). // // > In addition: Unfortunately, passing in `defaults` in `initialize()` // > below doesn't _ACTUALLY_ apply the specified model settings as // > defaults right now -- it only does so for implicit junction models. // > So we have to do that ourselves for the rest of the models out here // > first in this iteratee. Also note that we handle `attributes` as a // > special case. _.each(options.models, function (userlandModelDef, key){ if (_.isUndefined(userlandModelDef.identity)) { userlandModelDef.identity = key; } else if (userlandModelDef.identity !== key) { throw new Error('If `identity` is explicitly defined on a model definition, it should exactly match the key under which it is passed in to `Waterline.start()`. But the model definition passed in for key `'+key+'` has an identity that does not match: `'+userlandModelDef.identity+'`'); } _.defaults(userlandModelDef, _.omit(options.defaultModelSettings, 'attributes')); if (options.defaultModelSettings.attributes) { userlandModelDef.attributes = userlandModelDef.attributes || {}; _.defaults(userlandModelDef.attributes, options.defaultModelSettings.attributes); } orm.registerModel(Waterline.Model.extend(userlandModelDef)); });// // Fire 'er up orm.initialize({ adapters: options.adapters, datastores: options.datastores, defaults: options.defaultModelSettings }, function (err, _classicOntology) { if (err) { return done(err); } // Attach two private properties for compatibility's sake. // (These are necessary for utilities that accept `orm` to work.) // > But note that we do this as non-enumerable properties // > to make it less tempting to rely on them in userland code. // > (Instead, use `getModel()`!) Object.defineProperty(orm, 'collections', { value: _classicOntology.collections }); Object.defineProperty(orm, 'datastores', { value: _classicOntology.datastores }); return done(undefined, orm); }); } catch (err) { return done(err); } };// // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // To test quickly: // ``` // require('./').start({adapters: { 'sails-foo': { identity: 'sails-foo' } }, datastores: { default: { adapter: 'sails-foo' } }, models: { user: { attributes: {id: {type: 'number'}}, primaryKey: 'id', datastore: 'default'} }}, function(err, _orm){ if(err){throw err;} console.log(_orm); /* and expose as `orm`: */ orm = _orm; }); // ``` // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Waterline.stop() * * Tear down the specified Waterline ORM instance. * * --EXPERIMENTAL-- * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * FUTURE: Have this return a Deferred using parley (so it supports `await`) * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Ref} orm * * @param {Function} done * @param {Error?} err */ module.exports.stop = function (orm, done){ // Verify usage & apply defaults: if (!_.isFunction(done)) { throw new Error('Please provide a valid callback function as the 2nd argument to `Waterline.stop()`. (Instead, got: `'+done+'`)'); } try { if (!_.isObject(orm)) { throw new Error('Please provide a Waterline ORM instance (obtained from `Waterline.start()`) as the first argument to `Waterline.stop()`. (Instead, got: `'+orm+'`)'); } orm.teardown(function (err){ if (err) { return done(err); } return done(); });//_∏_ } catch (err) { return done(err); } }; /** * Waterline.getModel() * * Look up one of an ORM's models by identity. * (If no matching model is found, this throws an error.) * * --EXPERIMENTAL-- * * ------------------------------------------------------------------------------------------ * @param {String} modelIdentity * The identity of the model this is referring to (e.g. "pet" or "user") * * @param {Ref} orm * The ORM instance to look for the model in. * ------------------------------------------------------------------------------------------ * @returns {Ref} [the Waterline model] * ------------------------------------------------------------------------------------------ * @throws {Error} If no such model exists. * E_MODEL_NOT_REGISTERED * * @throws {Error} If anything else goes wrong. * ------------------------------------------------------------------------------------------ */ module.exports.getModel = function (modelIdentity, orm){ return getModel(modelIdentity, orm); };