data.js

(c) 2011 Michael Aufreiter
Data.js is freely distributable under the MIT license.
Portions of Data.js are inspired or borrowed from Underscore.js,
Backbone.js and Google's Visualization API.
For all details and documentation:
http://substance.io/#michael/data-js
(function(){

Initial Setup

The top-level namespace. All public Data.js classes and modules will be attached to this. Exported for both CommonJS and the browser.

  var Data;
  if (typeof exports !== 'undefined') {
    Data = exports;
  } else {
    Data = this.Data = {};
  }
  

Current version of the library. Keep in sync with package.json.

  Data.VERSION = '0.3.0';

Require Underscore, if we're on the server, and it's not already present.

  var _ = this._;
  if (!_ && (typeof require !== 'undefined')) _ = require("underscore");
  
  

Top Level API

  Data.VALUE_TYPES = [
    'string',
    'object',
    'number',
    'boolean',
    'date'
  ];
  
  Data.isValueType = function (type) {
    return _.include(Data.VALUE_TYPES, type);
  };
  

Returns true if a certain object matches a particular query object TODO: optimize!

  Data.matches = function(node, queries) {
    queries = _.isArray(queries) ? queries : [queries];
    var matched = false;

Matches at least one query

    _.each(queries, function(query) {
      if (matched) return;
      var rejected = false;
      _.each(query, function(value, key) {
        if (rejected) return;
        var condition;

Extract operator

        var matches = key.match(/^([a-z_]{1,30})(!=|>|>=|<|<=|\|=|&=)?$/),
            property = matches[1],
            operator = matches[2] || '==';

        if (operator === "|=") { // one of operator
          var values = _.isArray(value) ? value : [value];
          var objectValues = _.isArray(node[property]) ? node[property] : [node[property]];
          condition = false;
          _.each(values, function(val) {
            if (_.include(objectValues, val)) {
              condition = true;
            }
          });
        } else if (operator === "&=") {
          var values = _.isArray(value) ? value : [value];
          var objectValues = _.isArray(node[property]) ? node[property] : [node[property]];
          condition = _.intersect(objectValues, values).length === values.length;
        } else { // regular operators
          switch (operator) {
            case "!=": condition = !_.isEqual(node[property], value); break;
            case ">": condition = node[property] > value; break;
            case ">=": condition = node[property] >= value; break;
            case "<": condition = node[property] < value; break;
            case "<=": condition = node[property] <= value; break;
            default : condition = _.isEqual(node[property], value); break;
          }
        }

TODO: Make sure we exit the loop and return immediately when a condition is not met

        if (!condition) return rejected = true;
      });
      if (!rejected) return matched = true;
    });
    return matched;
  };
  
  
  /*!
  Math.uuid.js (v1.4)
  http://www.broofa.com
  mailto:robert@broofa.com

  Copyright (c) 2010 Robert Kieffer
  Dual licensed under the MIT and GPL licenses.
  */

  Data.uuid = function (prefix) {
    var chars = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''),
        uuid = [],
        radix = 16,
        len = 32;

    if (len) {

Compact form

      for (var i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
    } else {

rfc4122, version 4 form

      var r;

rfc4122 requires these characters

      uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
      uuid[14] = '4';

Fill in random data. At i==19 set the high bits of clock sequence as per rfc4122, sec. 4.1.5

      for (var i = 0; i < 36; i++) {
        if (!uuid[i]) {
          r = 0 | Math.random()*16;
          uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
        }
      }
    }
    return (prefix ? prefix : "") + uuid.join('');
  };

Helpers

_.Events (borrowed from Backbone.js)

  

A module that can be mixed in to any object in order to provide it with custom events. You may bind or unbind a callback function to an event; trigger-ing an event fires all callbacks in succession.

var object = {};
_.extend(object, Backbone.Events);
object.bind('expand', function(){ alert('expanded'); });
object.trigger('expand');
  
  _.Events = {

Bind an event, specified by a string name, ev, to a callback function. Passing "all" will bind the callback to all events fired.

    bind : function(ev, callback) {
      var calls = this._callbacks || (this._callbacks = {});
      var list  = this._callbacks[ev] || (this._callbacks[ev] = []);
      list.push(callback);
      return this;
    },

Remove one or many callbacks. If callback is null, removes all callbacks for the event. If ev is null, removes all bound callbacks for all events.

    unbind : function(ev, callback) {
      var calls;
      if (!ev) {
        this._callbacks = {};
      } else if (calls = this._callbacks) {
        if (!callback) {
          calls[ev] = [];
        } else {
          var list = calls[ev];
          if (!list) return this;
          for (var i = 0, l = list.length; i < l; i++) {
            if (callback === list[i]) {
              list.splice(i, 1);
              break;
            }
          }
        }
      }
      return this;
    },

Trigger an event, firing all bound callbacks. Callbacks are passed the same arguments as trigger is, apart from the event name. Listening for "all" passes the true event name as the first argument.

    trigger : function(ev) {
      var list, calls, i, l;
      if (!(calls = this._callbacks)) return this;
      if (list = calls[ev]) {
        for (i = 0, l = list.length; i < l; i++) {
          list[i].apply(this, Array.prototype.slice.call(arguments, 1));
        }
      }
      if (list = calls['all']) {
        for (i = 0, l = list.length; i < l; i++) {
          list[i].apply(this, arguments);
        }
      }
      return this;
    }
  };

Shared empty constructor function to aid in prototype-chain creation.

  var ctor = function(){};

Helper function to correctly set up the prototype chain, for subclasses. Similar to goog.inherits, but uses a hash of prototype properties and class properties to be extended. Taken from Underscore.js (c) Jeremy Ashkenas

  _.inherits = function(parent, protoProps, staticProps) {
    var child;

The constructor function for the new subclass is either defined by you (the "constructor" property in your extend definition), or defaulted by us to simply call super().

    if (protoProps && protoProps.hasOwnProperty('constructor')) {
      child = protoProps.constructor;
    } else {
      child = function(){ return parent.apply(this, arguments); };
    }

Set the prototype chain to inherit from parent, without calling parent's constructor function.

    ctor.prototype = parent.prototype;
    child.prototype = new ctor();

Add prototype properties (instance properties) to the subclass, if supplied.

    if (protoProps) _.extend(child.prototype, protoProps);

Add static properties to the constructor function, if supplied.

    if (staticProps) _.extend(child, staticProps);

Correctly set child's prototype.constructor, for instanceof.

    child.prototype.constructor = child;

Set a convenience property in case the parent's prototype is needed later.

    child.__super__ = parent.prototype;

    return child;
  };
  
  

Data.Hash

A Hash data structure that provides a simple layer of abstraction for managing a sortable data-structure with hash semantics. It's heavily used throughout Data.js.

  
  Data.Hash = function(data) {
    var that = this;
    this.data = {};
    this.keyOrder = [];
    this.length = 0;

    if (data instanceof Array) {
      _.each(data, function(datum, index) {
        that.set(index, datum);
      });
    } else if (data instanceof Object) {
      _.each(data, function(datum, key) {
        that.set(key, datum);
      });
    }
    
    if (this.initialize) this.initialize(attributes, options);
  };

  _.extend(Data.Hash.prototype, _.Events, {

Returns a copy of the Hash Used by transformation methods

    clone: function () {
      var copy = new Data.Hash();
      copy.length = this.length;
      _.each(this.data, function(value, key) {
        copy.data[key] = value;
      });
      copy.keyOrder = this.keyOrder.slice(0, this.keyOrder.length);
      return copy;
    },
    

Set a value at a given key

    set: function (key, value, targetIndex) {
      var index;
      if (key === undefined)
        return this;
      
      if (!this.data[key]) {
        if (targetIndex !== undefined) { // insert at a given index
          var front = this.select(function(item, key, i) {
            return i < targetIndex;
          });

          var back = this.select(function(item, key, i) {
            return i >= targetIndex;
          });

          this.keyOrder = [].concat(front.keyOrder);
          this.keyOrder.push(key);
          this.keyOrder = this.keyOrder.concat(back.keyOrder);
        } else {
          this.keyOrder.push(key);
        }
        index = this.length;
        this.length += 1;
      } else {
        index = this.index(key);
      }
      this.data[key] = value;
      this[index] = this.data[key];
      
      this.trigger('set', key);
      return this;
    },
    

Delete entry at given key

    del: function (key) {
      if (this.data[key]) {
        var l = this.length;
        var index = this.index(key);
        delete this.data[key];
        this.keyOrder.splice(index, 1);
        Array.prototype.splice.call(this, index, 1);
        this.length = l-1;
        this.trigger('del', key);
      }
      return this;
    },
    

Get value at given key

    get: function (key) {
      return this.data[key];
    },
    

Get value at given index

    at: function (index) {
      var key = this.keyOrder[index];
      return this.data[key];
    },
    

Get first item

    first: function () {
      return this.at(0);
    },
    

Returns the rest of the elements. Pass an index to return the items from that index onward.

    rest: function(index) {
      return this.select(function(value, key, i) {
        return i >= index;
      });
    },
    

Get last item

    last: function () {
      return this.at(this.length-1);
    },
    

Returns for an index the corresponding key

    key: function (index) {
      return this.keyOrder[index];
    },
    

Returns for a given key the corresponding index

    index: function(key) {
      return this.keyOrder.indexOf(key);
    },
    

Iterate over values contained in the Data.Hash

    each: function (fn) {
      var that = this;
      _.each(this.keyOrder, function(key, index) {
        fn.call(that, that.data[key], key, index);
      });
      return this;
    },
    

Convert to an ordinary JavaScript Array containing just the values

    values: function () {
      var result = [];
      this.each(function(value, key, index) {
        result.push(value);
      });
      return result;
    },

Returns all keys in current order

    keys: function () {
      return this.keyOrder;
    },
    

Convert to an ordinary JavaScript Array containing key value pairs. Used by sort.

    toArray: function () {
      var result = [];
    
      this.each(function(value, key) {
        result.push({key: key, value: value});
      });
    
      return result;
    },
    

Serialize

    toJSON: function() {
      var result = {};
      
      this.each(function(value, key) {
        result[key] = value.toJSON ? value.toJSON() : value;
      });
      return result;
    },

Map the Data.Hash to your needs

    map: function (fn) {
      var result = this.clone(),
          that = this;
      result.each(function(item, key, index) {
        result.data[that.key(index)] = fn.call(result, item);
      });
      return result;
    },

Select items that match some conditions expressed by a matcher function

    select: function (fn) {
      var result = new Data.Hash(),
          that = this;
    
      this.each(function(value, key, index) {
        if (fn.call(that, value, key, index)) {
          result.set(key, value);
        }
      });
      return result;
    },
    

Performs a sort

    sort: function (comparator) {
      var result = this.clone();
          sortedKeys = result.toArray().sort(comparator);
    

update keyOrder

      result.keyOrder = _.map(sortedKeys, function(k) {
        return k.key;
      });
      return result;
    },
    

Performs an intersection with the given hash

    intersect: function(hash) {
      var that = this,
          result = new Data.Hash();
          
      this.each(function(value, key) {
        hash.each(function(value2, key2) {
          if (key === key2) result.set(key, value);
        });
      });
      return result;
    },
    

Performs an union with the given hash

    union: function(hash) {
      var that = this,
          result = new Data.Hash();
          
      this.each(function(value, key) {
        if (!result.get(key))
          result.set(key, value);
      });
      hash.each(function(value, key) {
        if (!result.get(key))
          result.set(key, value);
      });
      return result;
    },
    

Computes the difference between the current hash and a given hash

    difference: function(hash) {
      var that = this,
          result = this.clone();
          
      hash.each(function(value, key) {
        if (result.get(key)) result.del(key);
      });
      return result;
    }
  });
  
  

Data.Comparators

  Data.Comparators = {};
  
  Data.Comparators.ASC = function(item1, item2) {
    return item1.value === item2.value ? 0 : (item1.value < item2.value ? -1 : 1);
  };

  Data.Comparators.DESC = function(item1, item2) {
    return item1.value === item2.value ? 0 : (item1.value > item2.value ? -1 : 1);
  };
  
  

Data.Aggregators

  Data.Aggregators = {};

  Data.Aggregators.SUM = function (values) {
    var result = 0;
    values.each(function(value, key, index) {
      if (_.isNumber(value)) result += value;
    });
    return result;
  };

  Data.Aggregators.MIN = function (values) {
    var result = Infinity;
    values.each(function(value, key, index) {
      if (_.isNumber(value) && value < result) result = value;
    });
    return result;
  };

  Data.Aggregators.MAX = function (values) {
    var result = -Infinity;
    values.each(function(value, key, index) {
      if (_.isNumber(value) && value > result) result = value;
    });
    return result;
  };

  Data.Aggregators.AVG = function (values) {
    var sum = 0,
        count = 0;
    values.each(function(value, key, index) {
      if (_.isNumber(value)) {
        sum += value;
        count += 1;
      }
    });
    return sum / count;
  };

  Data.Aggregators.COUNT = function (values) {
    return values.length;
  };
  
  

Data.Modifiers

  
  Data.Modifiers = {};

The default modifier simply does nothing

  Data.Modifiers.DEFAULT = function (attribute) {
    return attribute;
  };

  Data.Modifiers.MONTH = function (attribute) {
    return attribute.getMonth();
  };

  Data.Modifiers.QUARTER = function (attribute) {
    return Math.floor(attribute.getMonth() / 3) + 1;
  };
  
  

Data.Transformers

  
  Data.Transformers = {
    group: function(g, type, keys, properties) {
      var gspec = {},
          type = g.get(type),
          groups = {},
          count = 0;
      
      gspec[type._id] = {"type": "/type/type", "properties": {}};

Include group keys to the output graph

      _.each(keys, function(key) {
        gspec[type._id].properties[key] = type.properties().get(key).toJSON();
      });
      

Include additional properties

      _.each(properties, function(options, key) {
        var p = type.properties().get(key).toJSON();
        if (options.name) p.name = options.name;
        gspec[type._id].properties[key] = p;
      });

Compute group memberships

      _.each(keys, function(key) {
        groups[key] = type.properties().get(key).all('values');
      });

      function aggregate(key) {
        var members = new Data.Hash();
        _.each(keys, function(k, index) {
          var objects = groups[keys[index]].get(key[index]).referencedObjects;
          members = index === 0 ? members.union(objects) : members.intersect(objects);
        });
        
        var res = {type: type._id};
        _.each(gspec[type._id].properties, function(p, pk) {
          if (_.include(keys, pk)) {
            res[pk] = key[_.indexOf(keys, pk)];
          } else {
            var numbers = members.map(function(obj) {
              return obj.get(pk);
            });
            var aggregator = properties[pk].aggregator || Data.Aggregators.SUM
            res[pk] = aggregator(numbers);
          }
        });
        return res;
      }

      function extractGroups(keyIndex, key) {
        if (keyIndex === keys.length-1) {
          gspec[key.join('::')] = aggregate(key);
        } else {
          keyIndex += 1;
          groups[keys[keyIndex]].each(function(grp, grpkey) {
            extractGroups(keyIndex, key.concat([grpkey]));
          });
        }
      }

      extractGroups(-1, []);
      return new Data.Graph(gspec);
    }
  };
  
  

Data.Node

  

JavaScript Node implementation that hides graph complexity from the interface. It introduces properties, which group types of edges together. Therefore multi-partite graphs are possible without any hassle. Every Node simply contains properties which conform to outgoing edges. It makes heavy use of hashing through JavaScript object properties to allow random access whenever possible. If I've got it right, it should perform sufficiently fast, allowing speedy graph traversals.

  
  Data.Node = function(options) {
    this.nodeId = Data.Node.generateId();
    if (options) {
      this.val = options.value;
    }
    this._properties = {};
    
    if (this.initialize) this.initialize(options);
  };
  
  Data.Node.nodeCount = 0;
  

Generates a unique id for each node

  Data.Node.generateId = function () {
    return Data.Node.nodeCount += 1;
  };
  
  _.extend(Data.Node.prototype, _.Events, {

Node identity, which is simply the node's id

    identity: function() {
      return this.nodeId;
    },
    

Replace a property with a complete Hash

    replace: function(property, hash) {
      this._properties[property] = hash;
    },

Set a Node's property

Takes a property key, a value key and value. Values that aren't instances of Data.Node wrapped are automatically.

    set: function (property, key, value) {
      if (!this._properties[property]) {
        this._properties[property] = new Data.Hash();
      }
      this._properties[property].set(key, value instanceof Data.Node ? value : new Data.Node({value: value}));
      return this;
    },

Get node for given property at given key

    get: function (property, key) {
      if (key !== undefined && this._properties[property] !== undefined) {
        return this._properties[property].get(key);
      }
    },

Get all connected nodes at given property

    all: function(property) {
      return this._properties[property];
    },
    

Get first connected node at given property

Useful if you want to mimic the behavior of unique properties. That is, if you know that there's always just one associated node at a given property.

    first: function(property) {
      var p = this._properties[property];
      return p ? p.first() : null;  
    },

Value of first connected target node at given property

    value: function(property) {
      return this.values(property).first();
    },
    

Values of associated target nodes for non-unique properties

    values: function(property) {
      if (!this.all(property)) return new Data.Hash();
      return this.all(property).map(function(n) {
        return n.val;
      });
    }
  });
  
  

Data.Adapter

  

An abstract interface for writing and reading Data.Graphs.

  
  Data.Adapter = function(config) {

The config object is used to describe database credentials

    this.config = config;
  };
  

Namespace where Data.Adapters can register

  Data.Adapters = {};
  

Data.Property

  

Meta-data (data about data) is represented as a set of properties that belongs to a certain Data.Type. A Data.Property holds a key, a name and an expected type, telling whether the data is numeric or textual, etc.

  
  Data.Property = _.inherits(Data.Node, {
    constructor: function(type, id, options) {
      Data.Node.call(this);
      this.key = id;
      this._id = id;
      this.type = type;
      this.unique = options.unique;
      this.name = options.name;
      this.meta = options.meta || {};
      this.validator = options.validator;
      this.required = options["required"];
      this["default"] = options["default"];
      

TODO: ensure that object and value types are not mixed

      this.expectedTypes = _.isArray(options['type']) ? options['type'] : [options['type']];
      this.replace('values', new Data.Hash());
    },
    

TODO: this desctroys Data.Node#values values: function() { return this.all('values'); },

    
    isValueType: function() {
      return Data.isValueType(this.expectedTypes[0]);
    },
    
    isObjectType: function() {
      return !this.isValueType();
    },
    

Register values of a certain object

    registerValues: function(values, obj) {
      var that = this;
      var res = new Data.Hash();
      
      _.each(values, function(v, index) {
        if (!v) return; // skip
        var val;
        

Check if we can recycle an old value of that object

        if (obj.all(that.key)) val = obj.all(that.key).get(v);
        
        if (!val) { // Can't recycle
          val = that.get('values', v);
          if (!val) {

Well, a new value needs to be created

            if (that.isObjectType()) {

Create on the fly if an object is passed as a value

              if (typeof v === 'object') v = that.type.g.set(null, v)._id;
              val = that.type.g.get('objects', v);
              if (!val) {

Register the object (even if not yet loaded)

                val = new Data.Object(that.type.g, v);
                that.type.g.set('objects', v, val);
              }
            } else {
              val = new Data.Node({value: v});
              val.referencedObjects = new Data.Hash();
            }

Register value on the property

            that.set('values', v, val);
          }
          val.referencedObjects.set(obj._id, obj);
        }
        res.set(v, val);
      });
      

Unregister values that are no longer used on the object

      if (obj.all(that.key)) {
        this.unregisterValues(obj.all(that.key).difference(res), obj);
      }
      return res;
    },
    

Unregister values from a certain object

    unregisterValues: function(values, obj) {
      var that = this;
      values.each(function(val, key) {
        if (val.referencedObjects.length>1) {
          val.referencedObjects.del(obj._id);
        } else {
          that.all('values').del(key);
        }
      });
    },
        

Aggregates the property's values

    aggregate: function (fn) {
      return fn(this.values("values"));
    },
    

Serialize a propery definition

    toJSON: function() {
      return {
        name: this.name,
        type: this.expectedTypes,
        unique: this.unique,
        meta: this.meta,
        validator: this.validator,
        required: this.required,
        "default": this["default"]
      }
    }
  });
  
   

Data.Type

  

A Data.Type denotes an IS A relationship about a Data.Object. For example, if you type the object 'Shakespear' with the type 'Person' you are saying that Shakespeare IS A person. Types are also used to hold collections of properties that belong to a certain group of objects.

  
  Data.Type = _.inherits(Data.Node, {
    constructor: function(g, id, type) {
      var that = this;
      Data.Node.call(this);
  
      this.g = g; // Belongs to the DataGraph
      this.key = id;
      this._id = id;
      this._rev = type._rev;
      this._conflicted = type._conflicted;
      this.type = type.type;
      this.name = type.name;
      this.meta = type.meta || {};
      this.indexes = type.indexes;
      

Extract properties

      _.each(type.properties, function(property, key) {
        that.set('properties', key, new Data.Property(that, key, property));
      });
    },
    

Convenience function for accessing properties

    properties: function() {
      return this.all('properties');
    },
    

Objects of this type

    objects: function() {
      return this.all('objects');
    },
    

Serialize a single type node

    toJSON: function() {
      var result = {
        _id: this._id,
        type: '/type/type',
        name: this.name,
        properties: {}
      };
      
      if (this._rev) result._rev = this._rev;
      if (this.meta && _.keys(this.meta).length > 0) result.meta = this.meta;
      if (this.indexes && _.keys(this.indexes).length > 0) result.indexes = this.indexes;
      
      this.all('properties').each(function(property) {
        var p = result.properties[property.key] = {
          name: property.name,
          unique: property.unique,
          type: property.expectedTypes,
          required: property.required ? true : false
        };
        if (property["default"]) p["default"] = property["default"];
        if (property.validator) p.validator = property.validator;
        if (property.meta && _.keys(property.meta).length > 0) p.meta = property.meta;
      });
      return result;
    }
  });
  

Data.Object

  

Represents a typed data object within a Data.Graph. Provides access to properties, defined on the corresponding Data.Type.

  
  Data.Object = _.inherits(Data.Node, {
    constructor: function(g, id, data) {
      var that = this;
      Data.Node.call(this);
      
      this.g = g;
      

TODO: remove in favor of _id

      this.key = id;
      this._id = id;
      this.html_id = id.replace(/\//g, '_');
      this.dirty = true; // Every constructed node is dirty by default
      
      this.errors = []; // Stores validation errors
      this._types = new Data.Hash();
      

Associated Data.Objects

      this.referencedObjects = new Data.Hash();
      

Memoize raw data for the build process

      if (data) this.data = data;
    },
    

Convenience function for accessing all related types

    types: function() {
      return this._types;
    },
    
    toString: function() {
      return this.get('name') || this.val || this._id;
    },
    

Properties from all associated types

    properties: function() {
      var properties = new Data.Hash();

Prototypal inheritance in action: overriden properties belong to the last type specified

      this._types.each(function(type) {
        type.all('properties').each(function(property) {
          properties.set(property.key, property);
        });
      });
      return properties;
    },
    

After all nodes are recognized the object can be built

    build: function() {
      var that = this;
      var types = _.isArray(this.data.type) ? this.data.type : [this.data.type];
      
      if (!this.data) throw new Error('Object has no data, and cannot be built');
      

Pull off _id and _rev properties

      delete this.data._id;
      this._rev = this.data._rev; // delete this.data._rev;
      this._conflicted = this.data._conflicted;
      this._deleted = this.data._deleted; // delete this.data._deleted;
      

Initialize primary type (backward compatibility)

      this.type = this.g.get('objects', _.last(types));
      

Initialize types

      _.each(types, function(type) {
        that._types.set(type, that.g.get('objects', type));

Register properties for all types

        that._types.get(type).all('properties').each(function(property, key) {        
          function applyValue(value) {
            var values = _.isArray(value) ? value : [value];

Apply property values

            that.replace(property.key, property.registerValues(values, that));
          }
          
          if (that.data[key] !== undefined) {
            applyValue(that.data[key]);
          } else if (property["default"]) {
            applyValue(property["default"]);
          }
        });
      });
      
      if (this.dirty) this.g.trigger('dirty');
    },
    

Validates an object against its type (=schema)

    validate: function() {
      if (this.type.key === '/type/type') return true; // Skip type nodes
      
      var that = this;
      this.errors = [];
      this.properties().each(function(property, key) {

Required property?

        if ((that.get(key) === undefined || that.get(key) === null) || that.get(key) === "") {
          if (property.required) {
            that.errors.push({property: key, message: "Property \"" + property.name + "\" is required"});
          }
        } else {

Correct type?

          var types = property.expectedTypes;

          function validType(value, types) {
            if (_.include(types, typeof value)) return true;

FIXME: assumes that unloaded objects are valid properties

            if (!value.data) return true;
            if (value instanceof Data.Object && _.intersect(types, value.types().keys()).length>0) return true;
            if (typeof value === 'object' && _.include(types, value.constructor.name.toLowerCase())) return true;
            return false;
          }
          

Unique properties

          if (property.unique && !validType(that.get(key), types)) {
            that.errors.push({property: key, message: "Invalid type for property \"" + property.name + "\""});
          }
          

Non unique properties

          if (!property.unique && !_.all(that.get(key).values(), function(v) { return validType(v, types); })) {
            that.errors.push({property: key, message: "Invalid value type for property \"" + property.name + "\""});
          }
        }
        

Validator satisfied?

        function validValue() {
          return new RegExp(property.validator).test(that.get(key));
        }
        
        if (property.validator) {
          if (!validValue()) {
            that.errors.push({property: key, message: "Invalid value for property \"" + property.name + "\""});
          }
        }
      });
      return this.errors.length === 0;
    },
    

There are four different access scenarios for getting a certain property

  • Unique value types
  • Non-unique value types
  • Unique object types
  • Non-Unique object types

For convenience there's a get method, which always returns the right result depending on the schema information. However, internally, every property of a resource is represented as a non-unique Data.Hash of Data.Node objects, even if it's a unique property. So if you want to be explicit you should use the native methods of Data.Node. If two arguments are provided get delegates to Data.Node#get.

    
    get: function(property, key) {
      if (!this.data) return null;
      var p = this.properties().get(property);
      if (!p) return null;
    
      if (arguments.length === 1) {
        if (p.isObjectType()) {
          return p.unique ? this.first(property) : this.all(property);
        } else {
          return p.unique ? this.value(property) : this.values(property);
        }
      } else {
        return Data.Node.prototype.get.call(this, property, key);
      }
    },
    

Sets properties on the object Existing properties are overridden / replaced

    set: function(properties) {
      var that = this;
      
      if (arguments.length === 1) {
        _.each(properties, function(value, key) {
          var p = that.properties().get(key);
          if (!p) return; // Property not found on type
          

Setup values

          that.replace(p.key, p.registerValues(_.isArray(value) ? value : [value], that));
          
          that.dirty = true;
          that.g.trigger('dirty');
        });
      } else {
        return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]);
      }
    },
    

Serialize an Data.Object's properties

    toJSON: function() {
      var that = this;
      result = {};
      _.each(this._properties, function(value, key) {
        var p = that.properties().get(key);
        if (p.isObjectType()) {
          result[key] = p.unique ? that.all(key).keys()[0] : that.all(key).keys()
        } else {
          result[key] = p.unique ? that.value(key) : that.values(key).values();
        }
      });
      result['type'] = this.types().keys();
      result['_id'] = this._id;
      if (this._rev !== undefined) result['_rev'] = this._rev;
      if (this._deleted) result['_deleted'] = this._deleted;
      return result;
    }
  });
  
  _.extend(Data.Object.prototype, _.Events);
  
  

Data.Graph

  

A Data.Graph can be used for representing arbitrary complex object graphs. Relations between objects are expressed through links that point to referred objects. Data.Graphs can be traversed in various ways. See the testsuite for usage.

  

Set a new Data.Adapter and enable Persistence API

  Data.Graph = _.inherits(Data.Node, {
    constructor: function(g, dirty) {
      var that = this;
      Data.Node.call(this);
      
      this.watchers = {};
      this.replace('objects', new Data.Hash());
      if (!g) return;
      this.merge(g, dirty);
    },
    
    connect: function(name, config) {
      if (typeof exports !== 'undefined') {
        var Adapter = require(__dirname + '/adapters/'+name+'_adapter');
        this.adapter = new Adapter(this, config);
      } else {
        if (!Data.Adapters[name]) throw new Error('Adapter "'+name+'" not found');
        this.adapter = new Data.Adapters[name](this, config);
      }
      return this;
    },
    

Called when the Data.Adapter is ready

    connected: function(callback) {
      if (this.adapter.realtime) {
        this.connectedCallback = callback;
      } else {
        callback();
      }
    },
    

Serve graph along with an httpServer instance

    serve: function(server, options) {
      require(__dirname + '/server').initialize(server, this);
    },
    

Watch for graph updates

    watch: function(channel, query, callback) {
      this.watchers[channel] = callback;
      this.adapter.watch(channel, query, function(err) {});
    },
    

Stop watching that channel

    unwatch: function(channel, callback) {
      delete this.watchers[channel];
      this.adapter.unwatch(channel, function() {});
    },
    

Merges in another Graph

    merge: function(g, dirty) {
      var that = this;
      

Process schema nodes

      var types = _.select(g, function(node, key) {
        if (node.type === '/type/type' || node.type === 'type') {
          if (!that.get('objects', key)) {
            that.set('objects', key, new Data.Type(that, key, node));
            that.get(key).dirty = dirty;
          }
          return true;
        }
        return false;
      });
      

Process object nodes

      var objects = _.select(g, function(node, key) {
        if (node.type !== '/type/type' && node.type !== 'type') {
          var res = that.get('objects', key);
          var types = _.isArray(node.type) ? node.type : [node.type];
          if (!res) {
            res = new Data.Object(that, key, node);
            that.set('objects', key, res);
          } else {

Populate existing node with data in order to be rebuilt

            res.data = node;
          }

Check for type existence

          _.each(types, function(type) {
            if (!that.get('objects', type)) {
              throw new Error("Type '"+type+"' not found for "+key+"...");
            }
            that.get('objects', type).set('objects', key, res);
          });
          that.get(key).dirty = dirty;
          return true;
        }
        return false;
      });

Now that all objects are registered we can build them

      this.objects().each(function(r, key, index) {
        if (r.data) r.build();
      });
      return this;
    },
    

Set (add) a new node on the graph

    set: function(id, properties) {
      var that = this;
      var types = _.isArray(properties.type) ? properties.type : [properties.type];
      if (arguments.length === 2) {
        id = id ? id : Data.uuid('/' + _.last(_.last(types).split('/')) + '/');

Recycle existing object if there is one

        var res = that.get(id) ? that.get(id) : new Data.Object(that, id, properties, true);
        res.data = properties;
        res.dirty = true;
        res.build();
        this.set('objects', id, res);
        return this.get('objects', id);
      } else { // Delegate to Data.Node#set
        return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]);
      }
    },
    

API method for accessing objects in the graph space TODO: Ask the datastore if the node is not known in the local graph use async method queues for this!

    get: function(id) {
      if (arguments.length === 1) {
        return this.get('objects', id);
      } else {
        return Data.Node.prototype.get.call(this, arguments[0], arguments[1]);
      }
    },
    

Delete node by id, referenced nodes remain untouched

    del: function(id) {
      var node = this.get(id);
      if (!node) return;
      node._deleted = true;
      node.dirty = true;

Remove registered values

      node.properties().each(function(p, key) {
        var values = node.all(key);
        if (values) p.unregisterValues(values, node);
      });
      this.trigger('dirty');
    },
    

Find objects that match a particular query

    find: function(query) {
      return this.objects().select(function(o) {
        return Data.matches(o.toJSON(), query);
      });
    },
    

Fetches a new subgraph from the adapter and either merges the new nodes into the current set of nodes

    fetch: function(query, options, callback) {
      var that = this,
          nodes = new Data.Hash(); // collects arrived nodes
      

Options are optional

      if (typeof options === 'function' && typeof callback === 'undefined') {
        callback = options;
        options = {};
      }
      
      this.adapter.read(query, options, function(err, graph) {
        if (graph) {          
          that.merge(graph, false);
          _.each(graph, function(node, key) {
            nodes.set(key, that.get(key));
          });
        }
        err ? callback(err) : callback(null, nodes);
      });
    },

Synchronize dirty nodes with the database

    sync: function(callback) {
      callback = callback || function() {};
      var that = this,
          nodes = that.dirtyNodes();
      
      var validNodes = new Data.Hash();
      var invalidNodes = nodes.select(function(node, key) {
        if (!node.validate || (node.validate && node.validate())) {
          validNodes.set(key, node);
          return false;
        } else {
          return true;
        }
      });
      
      this.adapter.write(validNodes.toJSON(), function(err, g) {
        if (err) {
          callback(err);
        } else {
          that.merge(g, false);
          

Check for rejectedNodes

          validNodes.each(function(n, key) {
            if (g[key]) {
              n.dirty = false;
              n._rejected = false;
            } else {
              n._rejected = true;
            }
          });
          
          if (that.conflictedNodes().length > 0) that.trigger('conflicted');
          if (that.rejectedNodes().length > 0) that.trigger('rejected');
          
          callback(invalidNodes.length > 0 ? 'Some invalid nodes' : null, invalidNodes);
        }
      });
    },
    

Perform a group operation on a Data.Graph

    group: function(type, keys, properties) {
      var res = new Data.Collection();
      res.g = Data.Transformers.group(this, type, keys, properties);
      return res;
    },
    

Type nodes

    types: function() {
      return this.all('objects').select(function(node, key) {
        return node.type === '/type/type' || node.type === 'type';
      });
    },
    

Object nodes

    objects: function() {
      return this.all('objects').select(function(node, key) {
        return node.type !== '/type/type' && node.type !== 'type' && node.data && !node._deleted;
      });
    },
    

Get dirty nodes Used by Data.Graph#sync

    dirtyNodes: function() {
      return this.all('objects').select(function(obj, key) {
        return (obj.dirty && (obj.data || obj instanceof Data.Type));
      });
    },
    

Get invalid nodes

    invalidNodes: function() {
      return this.all('objects').select(function(obj, key) {
        return (obj.errors && obj.errors.length > 0);
      });
    },
    

Get conflicting nodes

    conflictedNodes: function() {
      return this.all('objects').select(function(obj, key) {
        return obj._conflicted;
      });
    },
    
    rejectedNodes: function() {
      return this.all('objects').select(function(obj, key) {
        return obj._rejected;
      });
    },
    

Serializes the graph to the JSON-based exchange format

    toJSON: function() {
      var result = {};
      

Serialize object nodes

      this.all('objects').each(function(obj, key) {

Only serialize fetched nodes

        if (obj.data || obj instanceof Data.Type) {
          result[key] = obj.toJSON();
        }
      });
      
      return result;
    }
  });
  
  _.extend(Data.Graph.prototype, _.Events);
  
  

Data.Collection

  

A Collection is a simple data abstraction format where a dataset under investigation conforms to a collection of data items that describes all facets of the underlying data in a simple and universal way. You can think of a Collection as a table of data, except it provides precise information about the data contained (meta-data). A Data.Collection just wraps a Data.Graph internally, in order to simplify the interface, for cases where you do not have to deal with linked data.

  
  Data.Collection = function(spec) {
    var that = this,
        gspec = { "/type/item": {"type": "/type/type", "properties": {}}};
        

Convert to Data.Graph serialization format

    if (spec) {
      _.each(spec.properties, function(property, key) {
        gspec["/type/item"].properties[key] = property;
      });
      this.g = new Data.Graph(gspec);
      _.each(spec.items, function(item, key) {
        that.set(key, item);
      });
    } else {
      this.g = new Data.Graph();
    }
  };
  
  _.extend(Data.Collection.prototype, {

Get an object (item) from the collection

    get: function(key) {
      return this.g.get.apply(this.g, arguments);
    },
    

Set (add) a new object to the collection

    set: function(id, properties) {
      this.g.set(id, _.extend(properties, {type: "/type/item"}));
    },
    

Find objects that match a particular query

    find: function(query) {
      return this.g.find(query);
    },
    

Returns a filtered collection containing only items that match a certain query

    filter: function(query) {
      return new Data.Collection({
        properties: this.properties().toJSON(),
        items: this.find(query).toJSON()
      });
    },
        

Perform a group operation on the collection

    group: function(keys, properties) {
      var res = new Data.Collection();
      res.g = Data.Transformers.group(this.g, "/type/item", keys, properties);
      return res;
    },
    

Convenience function for accessing properties

    properties: function() {
      return this.g.get('objects', '/type/item').all('properties');
    },
    

Convenience function for accessing items

    items: function() {
      return this.g.objects();
    },
    

Serialize

    toJSON: function() {
      return {
        properties: this.g.toJSON()["/type/item"].properties,
        items: this.g.objects().toJSON()
      }
    }
  });
})();