Source js/bauplan.model.js

define([
        "lodash",
        "thorax",
        "bauplan.authentication"
    ], function (_, Thorax, BauplanAuthentication) {
/**
 * @module bauplan%model
 * @extends Thorax.Model
 * @return {constructor} BauplanModel
 * @description  ## Generic model
 *
 *     var BauplanModel = require("bauplan.model");
 *
 * or as part of the Bauplan bundle
 *
 *     var Bauplan = require("bauplan");
 *     var BauplanModel = Bauplan.Model;
 *
 * Create and instantiate a new model class
 *
 *     var FooModel = Bauplan.Model.extend({});
 *     var fooinstance = new FooModel();
 *
 * Create a model that requires authentication
 *
 *     var AuthenticatedFooModel = Bauplan.Model.extend({
 *         _model: "authfoo",
 *         authenticated: true
 *     });
 *     
 * Set a model’s endpoint URL
 *
 *     var RestFooModel = Bauplan.Model.extend({
 *         _model: "restfoo",
 *         url: "/foo"
 *     });
 *     
 * Pass properties to a model
 *
 *     var PropsFooModel = Bauplan.Model.extend({
 *         _model: "propsfoo",
 *         properties: {
 *             foo: {
 *                  type: "integer",
 *                  exactLength: 3,
 *                  required: true
 *              }
 *          }
 *     });
 *
 * @listens module:bauplan%authentication~change:loggedIn
 */
    /**
     * @method extend
     * @return {constructor} Model
     * @static
     * @param {object} [options] Constructor options
     * @param {string} [options._model] Model name
     * @param {boolean} [options.authenticated=false] Whether this model requires authentication
     * @param {string|function} [options.url] URL for model endpoint…
     * @param {string} [options.urlRoot] … or URL stub for use with instance _id
     * @param {object} [options.schema] JSON Schema for model to use…
     * @param {object} [options.properties] … or lazily generate schema from properties
     * @param {boolean} [options.nofetch] Never fetch values from the endpoint
     * @param {string} [options.idAttribute=_id] Value to use for the model’s idAttribute. Based on assumption that most communication will be with a MongoDB-backed endpoint
     */
    var BauplanModel = Thorax.Model.extend({
        idAttribute: "_id",
        /**
         * @static
         * @override
         * @description  Overrides Thorax.Model.prototype.fetch
         *
         * Prevented from being called if:
         * - this.nofetch
         * - authentication is required
         */
        fetch: function () {
            if (!this.nofetch && (!this.Authentication || this.Authentication.loggedIn())) {
                Thorax.Model.prototype.fetch.apply(this, arguments);
            }
        },
        /**
         * @method initialize
         * @override
         * @description Passes values and options to the constructor when creating a new instance
         *
         * Creates attribute whitelist on first initialization and fetches unstance values if necesary
         * @param {object} attrs
         * @param {object} options
         * @param {boolean} [options.fetch=false] Whether to fetch instance values from endpoint
         */
        initialize: function (attrs, options) {
            options = options || {};

            if (this.authenticated) {
                var that = this;
                this.Authentication = BauplanAuthentication;
                /**
                 */
                this.Authentication.on("change:loggedIn", function() {
                    if (this.loggedIn()) {
                        // maybe not a good idea to reuse the fetch property (refetch? refetchAfterAuth? but why not?)
                        if (options.fetch) {
                            that.fetch();
                        }
                    } else {
                        that.clear();
                    }
                });
            }

            var proto = this.constructor.prototype;
            if (!proto.schema && proto.properties) {
                proto.schema = {
                    properties: proto.properties
                };
            }
            var schema = proto.schema;
            if (schema && !proto.schemaProcessed) {
                proto.schemaProcessed = true;
                if (!schema.id) {
                    schema.id = proto._model;
                }
                var schemaname = schema.id;
                schema.type = schema.type || "object";
                schema.$schema = schema.$schema || "http://json-schema.org/draft-04/schema#";
                schema.additionalProperties = schema.additionalProperties || false;
                for (var prop in schema.properties) {
                    var property = schema.properties[prop];
                    if (!property.id) {
                        property.id = schemaname + "." + prop;
                    }
                    if (!property.type) {
                        property.type = "string";
                    }
                }
                if (schema.additionalProperties === false) {
                    var whitelist = {};
                    for (var wprop in schema.properties) {
                        whitelist[wprop] = true;
                    }
                    proto.propertyMap = whitelist;
                }
            }

            if (options.fetch) {
                this.fetch();
            } else if (attrs && attrs[this.idAttribute] && _.keys(attrs).length === 1) {
                this.fetch();
            }
        },
        /**
         * @static
         * @override
         * @description Clears the attributes of an instance and any corresponding instance in the memoized register enabling atrributes to be loaded from the server again if necessary
         * 
         * Overrides Thorax.Model.prototype.clear
         * @param {object} args
         */
        clear: function() {
            this.unmemoize("clear", arguments);
        },
        /**
         * @static
         * @override
         * @description Destroys an instance and any corresponding instance in the memoized register
         * 
         * Overrides Thorax.Model.prototype.destroy
         * @param {object} args
         */
        destroy: function() {
            this.unmemoize("destroy", arguments);
        },
        /**
         * @static
         * @description  Generic helper function for syncing instances and the memoization register
         * @param {string} method
         * @param {object} args
         */
        unmemoize: function(method, args) {
            var zapMemo = function (memoname, id, count) {
                count = count || 1;
                if (BauplanModel.memo[memoname] && BauplanModel.memo[memoname][id]) {
                    delete BauplanModel.memo[memoname][id];
                    setTimeout(function() {
                        zapMemo(memoname, id, count + 1);
                    }, 100 * count);
                }
            };
            var id = this.get(this.idAttribute);
            var memoname = this.constructor.prototype._model;
            Thorax.Model.prototype[method].apply(this, args);
            if (BauplanModel.memo[memoname] && BauplanModel.memo[memoname][id]) {
                BauplanModel.memo[memoname][id].attributes = {};
            }
            // fine, but what happens if not undirtied before next actual retrieval?
            /*if (!BauplanModel.dirty[memoname]) {
                BauplanModel.dirty[memoname] = {};
            }
            BauplanModel.dirty[memoname][id] = true;*/
            zapMemo(memoname, id);
        },
        /**
         * @static
         * @description Returns instance’s attributes based on whitelist provided by model’s schema properties
         * @return {object} Whitelisted attributes
         */
        getAttributes: function (dump) {
            var attrs = _.extend({}, this.attributes);
            if (this.propertyMap) {
                for (var prop in attrs) {
                    if (!this.propertyMap[prop]) {
                        delete attrs[prop];
                    }
                }
            }
            return attrs;
        }
    });
    BauplanModel.memo = {};
    BauplanModel.dirty = {};
    /**
     * @instance
     * @param {function} Model
     * @description Returns a memoized version of Bauplan.Model
     * @return {function} MemoizedModel
     */
    BauplanModel.memoize = function (Model) {
        var modelname = Model.prototype._model;
        BauplanModel.memo[modelname] = {};
        var memo = BauplanModel.memo[modelname];
        var memoizedModel = function (attrs, options) {
            var idAttribute = Model.prototype.idAttribute;
            var object;
            options = options || {};
            if (!attrs) {
                object = new Model();
                // is this necessary?
                object.on("sync", function(){
                    memo[this.get(idAttribute)] = this;
                });
                return object;
            }
            var matchOn = options.matchOn || idAttribute;
            if (typeof attrs === "string" || typeof attrs === "number") {
                var stashOptions = attrs;
                attrs = {};
                attrs[matchOn] = stashOptions;
            }
            if (matchOn === idAttribute) {
                object = memo[attrs[idAttribute]];
                if (object) {
                    object.set(attrs);
                }
            } else if (attrs[matchOn]) {
                for (var cached in memo) {
                    var objectCheck = memo[cached];
                    //console.log("matching", matchOn, attrs[matchOn], objectCheck.attributes[matchOn]);
                    if (attrs[matchOn] === objectCheck.attributes[matchOn]) {
                        object = objectCheck;
                        break;
                    }
                }
            }
            /*if (BauplanModel.dirty[modelname] && BauplanModel.dirty[modelname][object.get(idAttribute)]) {
                delete memo[object.get(idAttribute)];
                object = undefined;
            }*/

            if (!object) {
                // if we searched by another key, don't make a new object. Or rather, not until we can get objects by other keys
                if (matchOn === idAttribute) {
                    if (!options.hasOwnProperty("parse")) {
                        options.parse = true;
                    }
                    object = new Model(attrs, options);
                    if (attrs[idAttribute]) {
                        memo[attrs[idAttribute]] = object;
                    }
                    object.on("sync", function(){
                        memo[this.get(idAttribute)] = this;
                    });
                }
            }

            return object;
        };
        if (Model.prototype.authenticated) {
            memoizedModel.authenticated = true;
        }

        return memoizedModel;
    };

    return BauplanModel;
});