API Docs for:
Show:

File: lib/index.js

"use strict"

var Q = require("q");
var happens = require("happens");

module.exports = function(){
  return Retain.extend();
};

module.exports.Retain = Retain;

/**
* Retain is a browser (CJS) and node Javascript model with plugins support.
*
* @class Retain
* @constructor
* @example
  ```
  var retain = require("retain");

  Movies = retain();

  //If you want to extend from Retain in Coffescript:
  
  class Movies extends retain.Retain

  ```
*/

function Retain()
{
  happens(this);
}

happens(Retain);

Retain.extend = function()
{
  var __hasProp = {}.hasOwnProperty;
  var Child = function(){};

  for (var key in Retain)
  {
    if (__hasProp.call(Retain, key))
    {
      Child[key] = Retain[key];
    }
  }

  function ctor() {
    this.constructor = Child;
  }

  ctor.prototype = Retain.prototype;
  Child.prototype = new ctor();

  Child.__super__ = Retain.prototype;
  Child._init()

  return Child;
}


Retain._plugins = []

/**
* Changes the name of the 'ID' property.
*
* @attribute id_prop 
* @example
  ```
    Movies.id_prop = "_id"; // Useful when working with data coming from MongoDB (it uses '_id')
  ```
*/
Retain.id_prop = "id";

/**
* Adds a plugin middleware to the Model
*
* @method use
* @static
* @param {Object} plugin Retain plugin middleware
* @param {Object} config Plugin's configuration object
* @example
  ```
    Movie.use(plugin_name, {
      url:"/movies"
    })
  ```
*/

Retain.use = function(plugin, config)
{
  var p = plugin();

  for (var prop in config) 
  {
    p.config[prop] = config[prop];
  }

  this._plugins.push(plugin);
}

/**
* Define the model attributes and the attributes type.
*
* @method attrs
* @static
* @param {Object} props Object containing the attributes and types as key:values.
* @example
  ```
    Movie.attrs({
      name:String,
      watched:Boolean,
      duration: Number,
      categories: Array,
      info: Object,
      year: Date
    })
  ```
*/
Retain.attrs = function(props)
{
  for(var key in props)
  {
    this._attrs[key] = props[key]
  }
}

/**
* Creates a new model instance locally.
* If a callback is suplied, and there is at least one plugin attached to the model, creates the record remotelly.
*
* @method new
* @static
* @param {Function} [callback] If suplied, it will be called when record was saved locally.
* @return {Object} Model instance.
* @example
  ```
    var godfather = Movies.new() //Creates the record locally
    var godfatherRemote = Movies.new(function(movie, err)
    {
      //Creates the record locally, and remotelly (if there is any plugin attached)
    })
  ```
*/
Retain.new = function (callback)
{
  var key, record, value;

  record = new this;
  record._cid = this._records.length;
  record._keys = {};

  this._records.push(record);

  this._run_plugins("new", record, callback);

  return record;
}

/**
* Sets the model instance properties locally.
*
* If a callback is suplied, and there is at least one plugin attached to the model, updates the record remotelly.
*
* @method set
* @param {Object} props The properties to be setted/updated.
* @param {Function} [callback] If suplied, it will be called when the record was saved remotelly.
* @return {Object} Model instance updated.
* @example
  ```
    var factotum = Movies.new(function(movie, err)
    {
      //Creates the record locally and remotelly
    })

    factotum.set({name:"Factotum", watched: false}) // Updates the record locally
    factotum.set({name:"Factotum", watched: false}, function()
    {
      // Updates the record remotelly.
    });

  ```
*/
Retain.prototype.set = function(props, callback)
{

  var args = 1 <= props.length ? [].slice.call(props, 0) : [];

  for(var prop in props)
  {
    this._validate_prop(prop, props[prop]);
  }

  this.constructor._run_plugins("set", this, callback);

  return this;
}

/**
* Get the value of a property.
*
* @method get
* @param {String} prop The property to be retrieved.
* @return The property value.
* @example
  ```
    factotum.set({name:"Factotum", watched: false}) // Updates the record locally
    
    var name = factotum.get("name") //Returns 'Factotum'

  ```
*/
Retain.prototype.get = function(prop)
{
  return this._keys[prop];
}

/**
* Finds a model instance based on the CID or ID.
*
* If a callback is suplied, and there is at least one plugin attached to the model, finds the record remotelly.
* @method find
* @static
* @param {Number} id Instance CID or ID.
* @param {Function} [callback] If suplied, it will be called when the remote record was retrieved.
* @return {Object} Record found.
* @example
  ```
    var eyesWideShut = Movies.new();
    
    Movies.find(0) // Returns a model instance (eyesWideShut)

    // Searchs remotelly for a recod with the ID of 2
    Movies.find(2, function(movie, err)
    {
      
    });

  ```
*/
Retain.find = function(id, callback)
{
  var found = null;
  var record = {id:id};

  // Search by ID
  for(var i = 0, total = this._records.length; i < total; i++)
  {
    if(parseInt(this._records[i][this.id_prop]) === id)
    {
      found = this._records[i];
    }
  }

  // Search by CID
  for(i = 0; i < total; i++)
  {
    if(parseInt(this._records[i]["_cid"]) === id && !found)
    {
      found = this._records[i];
    }
  }

  if(found)
    record = found;

  this._run_plugins("find", record, callback);
  return found;
}

/**
* Get all the model instances
*
* If a callback is suplied, and there is at least one plugin attached to the model, fetch the records remotelly.
* @method all
* @static
* @param {Function} [callback] If suplied, it will be called when the remote records were fetched.
* @return {Array} Array of records.
* @example
  ```
    Movies.all() // Returns the locally records

    Movies.all(function(records, err)
    {
      // Returns the remote records
    })

  ```
*/
Retain.all = function(callback)
{
  this._run_plugins("all", this._records, callback);
  return this._records;
}

/**
* Removes/deletes the record locally.
*
* If a callback is suplied, and there is at least one plugin attached to the model, removes the record remotelly.
* @method find
* @param {Function} [callback] If suplied, it will be called when the record was removed/deleted remotelly.
* @example
  ```
    var eyesWideShut = Movies.new(function()
    {
      // Creates a new record remotelly
    });
    
    eyesWideShut.remove() // Removes/deletes the record locally

    eyesWideShut.remove(function(movie, err)
    {
      // Removes/deletes the record remotelly.
    })

  ```
*/
Retain.prototype.remove = function(callback)
{
  var cid = this._cid;
  var record = null;
  var array = this.constructor._records;

  for(var i = 0, total = this.constructor._records.length -1; i < total; i++)
  {
    record = this.constructor._records[i];

    if(record._cid === cid)
    {
      this.constructor._records.splice(i,1);
    }
  }

  this.constructor._run_plugins("remove", this, callback);
}

/**
* Sync the local record with the remote storages.
*
* If a callback is suplied, and there is at least one plugin attached to the model, syncs the record remotelly.
* @method save
* @param {Function} [callback] If suplied, it will be called when the record was synchronized remotelly.
* @example
  ```
    var graveOfTheFireflies = Movie.new();
    
    graveOfTheFireflies.set({name:"Grave of the Fireflies"});

    graveOfTheFireflies.save(function()
    {
      done();
    });

  ```
*/
Retain.prototype.save = function(callback)
{
  var that = this;

  if(this._isNew() && !this._isRemoved())
  {
    that.constructor._run_plugins("new", that, function()
    {
      var keys = Object.keys(that._keys);

      if(keys.length)
      {
        that.constructor._run_plugins("set", that, function()
        {
          if(callback)
          {
            callback()
            return;
          }
        });
      }
      else
      {
        if(callback)
        {
          callback()
          return;
        }
      }

    });
  }
  else if(!this._isRemoved())
  {
    that.constructor._run_plugins("set", that, function()
    {
      if(callback)
        callback()
    });
  }
  else
  {
    if(callback)
      callback()
  }
}


// Private

Retain.prototype._isRemoved = function()
{
  var found = this.constructor.find(this.cid);

  if(found)
    return false;
  else
    return true;
}

Retain.prototype._isNew = function()
{
  if(this.id === undefined)
    return true;
  else
    return false;
}

Retain._run_plugins = function(method, initialValue, callback)
{
  if(callback)
  {
    // Reference https://github.com/kriskowal/q#sequences
    var plugins = this._get_plugins(method);
    var self = this;

    plugins.reduce(Q.when, Q(initialValue))
      .then(function(res)
      {
        self.emit("change", res);
        self.emit(method, res);
        callback(res,null);
      })
      .fail(function(err)
      {
        self.emit("error", res);
        callback(null,err)
      });
  }
}

Retain._init = function()
{
  this._attrs = {}
  this._records = []
}

Retain._get_plugins = function(method)
{
  var promises = [];

  for(var i = 0, total = this._plugins.length; i < total; i++)
  {
    if(this._plugins[i][method])
    {
      promises.push(this._plugins[i][method].bind(this._plugins[i]));
    }
  }

  return promises;
}

Retain.prototype._keys = {}

Retain.prototype._cid = {}

Retain.prototype._validate_type = function(prop, val)
{
  var attr = this.constructor._attrs[prop];

  if(/native\scode/.test(attr))
  {
    switch(attr.toString().match(/function\s(\w+)/)[1])
    {
      case "String":
        return (typeof val === "string");
        break;
      case "Boolean":
        return (typeof val === "boolean");
        break;
      case "Number":
        return (typeof val === "number");
        break;
      case "Array":
      case "Object":
      case "Date":
        return (val instanceof attr);
        break;
    }
  }
}

Retain.prototype._validate_prop = function(prop, value)
{
  if(this.constructor._attrs[prop])
  {
    if(this._validate_type(prop, value))
    {
      this._keys[prop] = value;
    }
    else
    {
      throw new Error("Invalid type for property " + prop + " = " + value);
    }
  }
  else
  {
    this[prop] = value;
  }
}