UNPKG

waterline

Version:

An ORM for Node.js and the Sails framework

266 lines (231 loc) 12.5 kB
/** * Module dependencies */ var util = require('util'); var _ = require('@sailshq/lodash'); var LifecycleCallbackBuilder = require('./utils/system/lifecycle-callback-builder'); var TransformerBuilder = require('./utils/system/transformer-builder'); var hasSchemaCheck = require('./utils/system/has-schema-check'); /** * MetaModel * * Construct a new MetaModel instance (e.g. `User` or `WLModel`) with methods for * interacting with a set of structured database records. * * > This is really just the same idea as constructing a "Model instance"-- we just * > use the term "MetaModel" for utmost clarity -- since at various points in the * > past, individual records were referred to as "model instances" rather than "records". * > * > In other words, this file contains the entry point for all ORM methods * > (e.g. User.find()). So like, `User` is a MetaModel instance. You might * > call it a "model" or a "model model" -- the important thing is just to * > understand that we're talking about the same thing in either case. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * Usage: * ``` * var WLModel = new MetaModel(orm, { adapter: require('sails-disk') }); * // (sorry about the capital "W" in the instance!) * ``` * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Dictionary} orm * * @param {Dictionary} adapterWrapper * @property {Dictionary} adapter * The adapter definition. * ************************************************************ * FUTURE: probably just remove this second argument. Instead of * passing it in, it seems like we should just look up the * appropriate adapter at the top of this constructor function * (or even just attach `._adapter` in userland- after instantiating * the new MetaModel instance-- e.g. "WLModel"). The only code that * directly runs `new MetaModel()` or `new SomeCustomizedMetaModel()` * is inside of Waterline core anyway.) * ************************************************************ * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @constructs {MetaModel} * The base MetaModel from whence other MetaModels are customized. * Remember: running `new MetaModel()` yields an instance like `User`, * which is itself generically called a WLModel. * * > This is kind of confusing, mainly because capitalization. And * > it feels silly to nitpick about something so confusing. But at * > least this way we know what everything's called, and it's consistent. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ var MetaModel = module.exports = function MetaModel (orm, adapterWrapper) { // Attach a private reference to the adapter definition indicated by // this model's configured `datastore`. this._adapter = adapterWrapper.adapter; // Attach a private reference to the ORM. this._orm = orm; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // > Note that we also alias it as `this.waterline`. this.waterline = orm; // ^^^ // FUTURE: remove this alias in Waterline v1.0 // (b/c it implies that `this.waterline` might be the stateless export from // the Waterline package itself, rather than what it actually is: a configured // ORM instance) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Initialize the `attributes` of our new MetaModel instance (e.g. `User.attributes`) // to an empty dictionary, unless they're already set. if (_.isUndefined(this.attributes)) { this.attributes = {}; } else { if (!_.isObject(this.attributes)) { throw new Error('Consistency violation: When instantiating this new instance of MetaModel, it became clear (within the constructor) that `this.attributes` was already set, and not a dictionary: '+util.inspect(this.attributes, {depth: 5})+''); } else { // FUTURE: Consider not allowing this, because it's weird. } } // Build a dictionary of all lifecycle callbacks applicable to this model, and // attach it as a private property (`_callbacks`). this._callbacks = LifecycleCallbackBuilder(this); //^^FUTURE: bust this utility apart to make it stateless like the others // //^^FUTURE: Also, document what's going on here as far as timing-- i.e. answering questions //like "when are model settings from the original model definition applied?" and //"How are they set?". // Set the `hasSchema` flag for this model. // > This is based on a handful of factors, including the original model definition, // > ORM-wide default model settings, and (if defined) an implicit default from the // > adapter itself. this.hasSchema = hasSchemaCheck(this); // ^^FUTURE: change utility's name to either the imperative mood (e.g. `getSchemafulness()`) // or interrogative mood (`isSchemaful()`) for consistency w/ the other utilities // (and to avoid confusion, because the name of the flag makes it kind of crazy in this case.) // Build a TransformerBuilder instance and attach it as a private property (`_transformer`). this._transformer = new TransformerBuilder(this.schema); // ^^FUTURE: bust this utility apart to make it stateless like the others return this; // ^^FUTURE: remove this `return` (it shouldn't be necessary) }; // ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗ // ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝ // ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗ // ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║ // ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║ // ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ // // MODEL METHODS // // Now extend the MetaModel constructor's `prototype` with each built-in model method. // > This allows for the use of `Foo.find()`, etc., and it's equivalent to attaching // > each method individually (e.g. `MetaModel.prototype.find = ()->{}`), just with // > slightly better performance characteristics. _.extend( MetaModel.prototype, { // DQL find: require('./methods/find'), findOne: require('./methods/find-one'), findOrCreate: require('./methods/find-or-create'), stream: require('./methods/stream'), count: require('./methods/count'), sum: require('./methods/sum'), avg: require('./methods/avg'), // DML create: require('./methods/create'), createEach: require('./methods/create-each'), update: require('./methods/update'), updateOne: require('./methods/update-one'), destroy: require('./methods/destroy'), destroyOne: require('./methods/destroy-one'), archive: require('./methods/archive'), archiveOne: require('./methods/archive-one'), addToCollection: require('./methods/add-to-collection'), removeFromCollection: require('./methods/remove-from-collection'), replaceCollection: require('./methods/replace-collection'), // Misc. validate: require('./methods/validate'), } ); // SPECIAL STATIC METAMODEL METHODS // // Now add properties to the MetaModel constructor itself. // (i.e. static properties) /** * MetaModel.extend() * * Build & return a new constructor based on the existing constructor in the * current runtime context (`this`) -- which happens to be our base model * constructor (MetaModel). This also attaches the specified properties to * the new constructor's prototype. * * * > Originally taken from `.extend()` in Backbone source: * > http://backbonejs.org/docs/backbone.html#section-189 * > * > Although this is called `extend()`, note that it does not actually modify * > the original MetaModel constructor. Instead, it first builds a shallow * > clone of the original constructor and then extends THAT. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * * @param {Dictionary?} protoProps * Optional extra set of properties to attach to the new ctor's prototype. * (& possibly a brand of breakfast cereal) * * @param {Dictionary?} staticProps * NO LONGER SUPPORTED: An optional, extra set of properties to attach * directly to the new ctor. * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @returns {Function} [The new constructor -- e.g. `SomeCustomizedMetaModel`] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @this {Function} [The original constructor -- BaseMetaModel] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ MetaModel.extend = function (protoProps, staticProps) { var thisConstructor = this; // Sanity checks: // If a prototypal properties were provided, and one of them is under the `constructor` key, // then freak out. This is no longer supported, and shouldn't still be in use anywhere. if (protoProps && _.has(protoProps, 'constructor')) { throw new Error('Consistency violation: The first argument (`protoProps`) provided to Waterline.Model.extend() should never have a `constructor` property. (This kind of usage is no longer supported.)'); } // If any additional custom static properties were specified, then freak out. // This is no longer supported, and shouldn't still be in use anywhere. if (!_.isUndefined(staticProps)) { throw new Error('Consistency violation: Unrecognized extra argument provided to Waterline.Model.extend() (`staticProps` is no longer supported.)'); } //--• // Now proceed with the classical, Backbone-flavor extending. var newConstructor = function() { return thisConstructor.apply(this, arguments); }; // Shallow-copy all of the static properties (top-level props of original constructor) // over to the new constructor. _.extend(newConstructor, thisConstructor, staticProps); // Create an ad hoc "Surrogate" -- a short-lived, bionic kind of a constructor // that serves as an intermediary... or maybe more of an organ donor? Surrogate // is probably still best. Anyway it's some dark stuff, that's for sure. Because // what happens next is that we give it a reference to our original ctor's prototype // and constructor, then "new up" an instance for us-- but only so that we can cut out // that newborn instance's `prototype` and put it where the prototype for our new ctor // is supposed to go. // // > Why? Well for one thing, this is important so that our new constructor appears // > to "inherit" from our original constructor. But likely a more prescient motive // > is so that our new ctor is a proper clone. That is, it's no longer entangled with // > the original constructor. // > (More or less anyway. If there are any deeply nested things, like an `attributes` // > dictionary -- those could still contain deep, entangled references to stuff from the // > original ctor's prototype. var Surrogate = function() { this.constructor = newConstructor; }; Surrogate.prototype = thisConstructor.prototype; newConstructor.prototype = new Surrogate(); // If extra `protoProps` were provided, merge them onto our new ctor's prototype. // (now that it's a legitimately separate thing that we can safely modify) if (protoProps) { _.extend(newConstructor.prototype, protoProps); } // Set a proprietary `__super__` key to keep track of the original ctor's prototype. // (see http://stackoverflow.com/questions/8596861/super-in-backbone#comment17856929_8614228) newConstructor.__super__ = thisConstructor.prototype; // Return our new ctor. return newConstructor; };