balderdashy/waterline

View on GitHub
lib/waterline.js

Summary

Maintainability
F
1 wk
Test Coverage
//  ██╗    ██╗ █████╗ ████████╗███████╗██████╗ ██╗     ██╗███╗   ██╗███████╗
//  ██║    ██║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗██║     ██║████╗  ██║██╔════╝
//  ██║ █╗ ██║███████║   ██║   █████╗  ██████╔╝██║     ██║██╔██╗ ██║█████╗
//  ██║███╗██║██╔══██║   ██║   ██╔══╝  ██╔══██╗██║     ██║██║╚██╗██║██╔══╝
//  ╚███╔███╔╝██║  ██║   ██║   ███████╗██║  ██║███████╗██║██║ ╚████║███████╗
//   ╚══╝╚══╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝  ╚═╝╚══════╝╚═╝╚═╝  ╚═══╝╚══════╝
//

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');
var validateDatastoreConnectivity = require('./waterline/utils/system/validate-datastore-connectivity');




/**
 * 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
          };
        });//</ each model identity >

        // Call the `registerDatastore` adapter method.
        datastore.adapter.registerDatastore(datastore.config, usedSchemas, function(err) {
          if (err) {
            return next(err);
          }

          return validateDatastoreConnectivity(datastore, 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
      });

    });//</async.each>

  };//</ definition of `orm.initialize` >


  //  ┌─┐─┐ ┬┌─┐┌─┐┌─┐┌─┐  ┌─┐┬─┐┌┬┐╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╗╔
  //  ├┤ ┌┴┬┘├─┘│ │└─┐├┤   │ │├┬┘│││ ║ ║╣ ╠═╣╠╦╝ ║║║ ║║║║║║║
  //  └─┘┴ └─┴  └─┘└─┘└─┘  └─┘┴└─┴ ┴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+'`');
      }

    });//</_.each>


    // 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));

    });//</_.each>


    // 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); }

};//</Waterline.start()>

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// 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);
};