12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016 |
- // ██╗ ██╗ █████╗ ████████╗███████╗██████╗ ██╗ ██╗███╗ ██╗███████╗
- // ██║ ██║██╔══██╗╚══██╔══╝██╔════╝██╔══██╗██║ ██║████╗ ██║██╔════╝
- // ██║ █╗ ██║███████║ ██║ █████╗ ██████╔╝██║ ██║██╔██╗ ██║█████╗
- // ██║███╗██║██╔══██║ ██║ ██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║██╔══╝
- // ╚███╔███╔╝██║ ██║ ██║ ███████╗██║ ██║███████╗██║██║ ╚████║███████╗
- // ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝╚══════╝
- //
- 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
- };
- });//</ each model identity >
- // 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
- });
- });//</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);
- };
|