Comparing version 0.4.1 to 0.6.0
1565
data.js
@@ -1,2 +0,2 @@ | ||
// (c) 2011 Michael Aufreiter | ||
// (c) 2012 Michael Aufreiter | ||
// Data.js is freely distributable under the MIT license. | ||
@@ -6,3 +6,3 @@ // Portions of Data.js are inspired or borrowed from Underscore.js, | ||
// For all details and documentation: | ||
// http://substance.io/#michael/data-js | ||
// http://substance.io/michael/data-js | ||
@@ -24,3 +24,3 @@ (function(){ | ||
// Current version of the library. Keep in sync with `package.json`. | ||
Data.VERSION = '0.4.1'; | ||
Data.VERSION = '0.6.0'; | ||
@@ -31,3 +31,2 @@ // Require Underscore, if we're on the server, and it's not already present. | ||
// Top Level API | ||
@@ -43,56 +42,9 @@ // ------- | ||
]; | ||
Data.isValueType = function (type) { | ||
return _.include(Data.VALUE_TYPES, type); | ||
return _.include(Data.VALUE_TYPES, _.last(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] || (property == "type" || _.isArray(value) ? "|=" : "="); | ||
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; | ||
}; | ||
/*! | ||
@@ -136,747 +88,41 @@ Math.uuid.js (v1.4) | ||
// 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 | ||
// Data.Query | ||
// -------------- | ||
// 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; | ||
// Query module to be mixed into Data.Graph and Data.Collection data structures | ||
// No indexing yet, this has been shifted to Data.js 0.7.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); | ||
}; | ||
Data.Query = { | ||
_.extend(Data.Hash.prototype, _.Events, { | ||
// Returns all objects matching a particular query object | ||
query: function(qry) { | ||
// 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); | ||
function toArray(v) { | ||
return _.isArray(v) ? v : [v]; | ||
} | ||
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.hasOwnProperty(key) ? this.data[key] : undefined; | ||
}, | ||
// 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 a sub-range of the current *hash* | ||
range: function(start, end) { | ||
var result = new Data.Hash(); | ||
for(var i=start; i<=end; i++) { | ||
result.set(this.key(i), this.at(i)); | ||
} | ||
return result; | ||
}, | ||
// Returns the rest of the elements. | ||
// Pass an index to return the items from that index onward. | ||
rest: function(index) { | ||
return this.range(index, this.length-1); | ||
}, | ||
// 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 _.clone(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; | ||
}, | ||
function match(obj, qry) { | ||
var matched = true; | ||
// 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(); | ||
// Ensure that is the smaller one | ||
if (hash.length < that.length) { | ||
that = hash; | ||
hash = this; | ||
} | ||
that.each(function(value,key) { | ||
if (hash.get(key)) 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) { | ||
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 = new Data.Hash(); | ||
this.each(function(value, key) { | ||
if (!hash.get(key)) result.set(key, value); | ||
}); | ||
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 count === 0 ? 0 : (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": {}, indexes: type.indexes}; | ||
// 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(options.property || key).toJSON(); | ||
if (options.name) p.name = options.name; | ||
gspec[type._id].properties[key] = p; | ||
}); | ||
var groupedGraph = new Data.Graph(gspec); | ||
_.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); | ||
}); | ||
// Empty group key | ||
if (key.length === 0) members = g.objects(); | ||
if (members.length === 0) return null; | ||
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(properties[pk].property || pk); | ||
}); | ||
var aggregator = properties[pk].aggregator || Data.Aggregators.SUM; | ||
res[pk] = aggregator(numbers); | ||
_.find(qry, function(value, property) { | ||
var val = property === "type" ? obj.types : obj.properties[property]; | ||
var matchedValues = _.intersect(toArray(value), toArray(val)); | ||
if (matchedValues.length === 0) { | ||
matched = false; | ||
return true; | ||
} | ||
}); | ||
return res; | ||
return matched; | ||
} | ||
function extractGroups(keyIndex, key) { | ||
if (keyIndex === keys.length-1) { | ||
var aggregatedItem = aggregate(key); | ||
if (aggregatedItem) groupedGraph.set(key.join('::'), aggregatedItem); | ||
} else { | ||
keyIndex += 1; | ||
groups[keys[keyIndex]].each(function(grp, grpkey) { | ||
extractGroups(keyIndex, key.concat([grpkey])); | ||
}); | ||
} | ||
} | ||
extractGroups(-1, []); | ||
return groupedGraph; | ||
} | ||
}; | ||
// 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; | ||
var type = this.get(qry.type); | ||
var objects = _.select(this.objects, function(o) { | ||
return match(o, qry); | ||
}); | ||
return Data.Collection.create(type, objects); | ||
} | ||
}); | ||
// 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 === undefined) return; // skip | ||
var val; | ||
// Skip registration for object type values | ||
// TODO: check edge cases! | ||
if (that.isValueType() && that.expectedTypes[0] === 'object') { | ||
val = new Data.Node({value: v}); | ||
res.set(index, val); | ||
return; | ||
} | ||
// 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('nodes', v); | ||
if (!val) { | ||
// Register the object (even if not yet loaded) | ||
val = new Data.Object(that.type.g, v); | ||
that.type.g.set('nodes', 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 && 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 | ||
@@ -890,59 +136,29 @@ // -------------- | ||
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; | ||
Data.Type = function(type) { | ||
this._id = type._id; | ||
this.type = "/type/type"; | ||
this.name = type.name; | ||
this.meta = type.meta || {}; | ||
this.indexes = type.indexes; | ||
that.replace('properties', new Data.Hash); | ||
// 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('nodes'); | ||
}, | ||
this.indexes = type.indexes || {}; | ||
this.properties = type.properties; | ||
_.each(this.properties, _.bind(function(property, key) { | ||
property.type = _.isArray(property.type) ? property.type : [ property.type ]; | ||
property.unique = _.isBoolean(property.unique) ? property.unique : true; | ||
}, this)); | ||
}; | ||
_.extend(Data.Type.prototype, _.Events, { | ||
// Serialize a single type node | ||
toJSON: function() { | ||
var result = { | ||
return { | ||
_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; | ||
properties: this.properties, | ||
meta: this.meta, | ||
indexes: _.map(this.indexes, function(i) { return i.properties }) | ||
} | ||
} | ||
@@ -957,131 +173,27 @@ }); | ||
// 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; | ||
Data.Object = function(object, host) { | ||
this._id = object._id; | ||
this.host = host; | ||
this.properties = {}; | ||
this.set(object); | ||
}; | ||
_.extend(Data.Object.prototype, _.Events, { | ||
// Returns the most specific type | ||
type: function() { | ||
return this.host.get(_.last(this.types)); | ||
}, | ||
// Convenience function for accessing all related types | ||
types: function() { | ||
return this._types; | ||
// Property lookup according to the type chain | ||
property: function(property) { | ||
var p = null; | ||
_.find(this.types.reverse(), _.bind(function(type) { | ||
return p = this.host.get(type).properties[property]; | ||
}, this)); | ||
return p; | ||
}, | ||
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'); | ||
this._rev = this.data._rev; | ||
this._conflicted = this.data._conflicted; | ||
this._deleted = this.data._deleted; | ||
// Initialize primary type (backward compatibility) | ||
this.type = this.g.get('nodes', _.last(types)); | ||
// Initialize types | ||
_.each(types, function(type) { | ||
that._types.set(type, that.g.get('nodes', 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', this); | ||
}, | ||
// 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; | ||
}, | ||
@@ -1103,62 +215,36 @@ // There are four different access scenarios for getting a certain property | ||
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); | ||
} | ||
var p = this.property(property), | ||
value = this.properties[property]; | ||
if (!p || !value) return null; | ||
if (Data.isValueType(p.type)) { | ||
return value; | ||
} else { | ||
return Data.Node.prototype.get.call(this, property, key); | ||
return p.unique ? this.host.get(value) | ||
: _.map(value, _.bind(function(v) { return this.host.get(v); }, this)); | ||
} | ||
}, | ||
// Sets properties on the object | ||
// Existing properties are overridden / replaced | ||
set: function(properties) { | ||
set: function(object) { | ||
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', that); | ||
that.g.snapshot(); | ||
}); | ||
} else { | ||
return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]); | ||
} | ||
if (object.type) this.types = _.isArray(object.type) ? object.type : [object.type]; | ||
if (object.meta) this.meta = this.object.meta; | ||
_.each(object, _.bind(function(value, key) { | ||
if (!that.property(key) || key === "type") return; | ||
that.properties[key] = value; | ||
}, this)); | ||
}, | ||
// 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; | ||
return _.extend(this.properties, {_id: this._id, type: this.types}) | ||
} | ||
}); | ||
_.extend(Data.Object.prototype, _.Events); | ||
// Data.Graph | ||
@@ -1172,150 +258,55 @@ // -------------- | ||
// Set a new Data.Adapter and enable Persistence API | ||
Data.Graph = function(graph, options) { | ||
this.nodes = []; | ||
this.objects = []; | ||
this.types = []; | ||
this.keys = {}; // Lookup objects by key | ||
if (!graph) return; | ||
this.merge(graph); | ||
}; | ||
Data.Graph = _.inherits(Data.Node, { | ||
constructor: function(g, options) { | ||
var that = this; | ||
Data.Node.call(this); | ||
this.watchers = {}; | ||
this.replace('nodes', new Data.Hash()); | ||
if (!g) return; | ||
this.merge(g, options.dirty); | ||
if (options.persistent) { | ||
this.persistent = options.persistent; | ||
this.restore(); // Restore data | ||
} | ||
}, | ||
_.extend(Data.Graph.prototype, Data.Query, _.Events, { | ||
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() {}); | ||
}, | ||
// Empty graph | ||
empty: function() { | ||
var that = this; | ||
_.each(this.objects().keys(), function(id) { | ||
that.del(id); | ||
that.all('nodes').del(id); | ||
}); | ||
}, | ||
// 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('nodes', key)) { | ||
that.set('nodes', key, new Data.Type(that, key, node)); | ||
that.get(key)._dirty = node._dirty ? node._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('nodes', key); | ||
var types = _.isArray(node.type) ? node.type : [node.type]; | ||
if (!res) { | ||
res = new Data.Object(that, key, node); | ||
that.set('nodes', 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('nodes', type)) { | ||
throw new Error("Type '"+type+"' not found for "+key+"..."); | ||
} | ||
that.get('nodes', type).set('nodes', key, res); | ||
}); | ||
that.get(key)._dirty = node._dirty ? node._dirty : dirty; | ||
if (!node._id) node._id = key; | ||
return true; | ||
} | ||
return false; | ||
}); | ||
// Now that all new objects are registered we can build them | ||
_.each(objects, function(o) { | ||
var obj = that.get(o._id); | ||
if (obj.data) obj.build(); | ||
}); | ||
// Create a new snapshot | ||
this.snapshot(); | ||
merge: function(nodes) { | ||
_.each(nodes, _.bind(function(n, key) { this.set(_.extend(n, { _id: key })); }, this)); | ||
return this; | ||
}, | ||
// API method for accessing objects in the graph space | ||
get: function(id) { | ||
return this.nodes[this.keys[id]]; | ||
}, | ||
set: function(node) { | ||
var id, that = this; | ||
// Backward compatibility | ||
if (arguments.length === 2) node = _.extend(arguments[1], {_id: arguments[0]}); | ||
var types = _.isArray(node.type) ? node.type : [node.type]; | ||
node._id = node._id ? node._id : Data.uuid('/' + _.last(_.last(types).split('/')) + '/'); | ||
var types = _.isArray(node.type) ? node.type : [node.type]; | ||
if (arguments.length <= 2) { | ||
node._id = node._id ? node._id : Data.uuid('/' + _.last(_.last(types).split('/')) + '/'); | ||
// Recycle existing object if there is one | ||
var res = that.get(node._id) ? that.get(node._id) : new Data.Object(that, node._id, _.clone(node), true); | ||
res.data = node; | ||
res._dirty = true; | ||
res.build(); | ||
this.set('nodes', node._id, res); | ||
this.snapshot(); | ||
return res; | ||
} else { // Delegate to Data.Node#set | ||
return Data.Node.prototype.set.call(this, arguments[0], arguments[1], arguments[2]); | ||
function createNode() { | ||
return _.last(types) === "/type/type" ? new Data.Type(node) | ||
: new Data.Object(node, this); | ||
} | ||
}, | ||
// API method for accessing objects in the graph space | ||
get: function(id) { | ||
if (arguments.length === 1) { | ||
return this.get('nodes', id); | ||
var n = this.get(node._id); | ||
if (!n) { | ||
n = createNode.apply(this); | ||
this.keys[node._id] = this.nodes.length; | ||
this.nodes.push(n); | ||
// Register | ||
if (_.last(types) === "/type/type") { | ||
this.types.push(n); | ||
} else { | ||
this.objects.push(n); | ||
} | ||
} else { | ||
return Data.Node.prototype.get.call(this, arguments[0], arguments[1]); | ||
n.set(node); | ||
} | ||
return n; | ||
}, | ||
// Return all objects matching a query object | ||
find: function(qry) { | ||
return this.query(qry); | ||
}, | ||
@@ -1327,170 +318,15 @@ // Delete node by id, referenced nodes remain untouched | ||
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', node); | ||
this.snapshot(); | ||
}, | ||
// Find objects that match a particular query | ||
find: function(query) { | ||
return this.objects().select(function(o) { | ||
return Data.matches(o.toJSON(), query); | ||
}); | ||
}, | ||
// Memoize a snapshot of the current graph | ||
snapshot: function() { | ||
if (!this.persistent) return; | ||
localStorage.setItem("graph", JSON.stringify(this.toJSON(true))); | ||
}, | ||
// Restore latest snapshot from localStorage | ||
restore: function() { | ||
var snapshot = JSON.parse(localStorage.getItem("graph")); | ||
if (snapshot) this.merge(snapshot); | ||
}, | ||
// 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 backend | ||
sync: function(callback) { | ||
callback = callback || function() {}; | ||
var that = this, | ||
nodes = that.dirtyNodes(); | ||
var validNodes = new Data.Hash(); | ||
nodes.select(function(node, key) { | ||
if (!node.validate || (node.validate && node.validate())) { | ||
validNodes.set(key, node); | ||
} | ||
}); | ||
this.adapter.write(validNodes.toJSON(), function(err, g) { | ||
if (err) return callback(err); | ||
that.merge(g, false); | ||
// Check for rejectedNodes / conflictedNodes | ||
validNodes.each(function(n, key) { | ||
if (g[key]) { | ||
n._dirty = false; | ||
n._rejected = false; | ||
} else { | ||
n._rejected = true; | ||
} | ||
}); | ||
// Update localStorage | ||
if (this.persistent) that.snapshot(); | ||
if (that.invalidNodes().length > 0) that.trigger('invalid'); | ||
if (that.conflictedNodes().length > 0) that.trigger('conflicted'); | ||
if (that.rejectedNodes().length > 0) that.trigger('rejected'); | ||
var unsavedNodes = that.invalidNodes().union(that.conflictedNodes()) | ||
.union(that.rejectedNodes()).length; | ||
callback(unsavedNodes > 0 ? unsavedNodes+' unsaved nodes' : null); | ||
}); | ||
}, | ||
// 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('nodes').select(function(node, key) { | ||
return node.type === '/type/type' || node.type === 'type'; | ||
}); | ||
}, | ||
// Object nodes | ||
objects: function() { | ||
return this.all('nodes').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('nodes').select(function(obj, key) { | ||
return (obj._dirty && (obj.data || obj instanceof Data.Type)); | ||
}); | ||
}, | ||
// Get invalid nodes | ||
invalidNodes: function() { | ||
return this.all('nodes').select(function(obj, key) { | ||
return (obj.errors && obj.errors.length > 0); | ||
}); | ||
}, | ||
// Get conflicting nodes | ||
conflictedNodes: function() { | ||
return this.all('nodes').select(function(obj, key) { | ||
return obj._conflicted; | ||
}); | ||
}, | ||
// Nodes that got rejected during sync | ||
rejectedNodes: function() { | ||
return this.all('nodes').select(function(obj, key) { | ||
return obj._rejected; | ||
}); | ||
}, | ||
// Serializes the graph to the JSON-based exchange format | ||
toJSON: function(extended) { | ||
var result = {}; | ||
// Serialize object nodes | ||
this.all('nodes').each(function(obj, key) { | ||
// Only serialize fetched nodes | ||
if (obj.data || obj instanceof Data.Type) { | ||
result[key] = obj.toJSON(); | ||
if (extended) { | ||
// include special properties | ||
if (obj._dirty) result[key]._dirty = true; | ||
if (obj._conflicted) result[key]._conclicted = true; | ||
if (obj._rejected) result[key].rejected = true; | ||
} | ||
} | ||
_.each(this.nodes, function(n) { | ||
result[n._id] = n.toJSON() | ||
}); | ||
return result; | ||
} | ||
}); | ||
_.extend(Data.Graph.prototype, _.Events); | ||
// Data.Collection | ||
@@ -1503,72 +339,81 @@ // -------------- | ||
// 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. | ||
// information about the data contained (meta-data). | ||
Data.Collection = function(spec) { | ||
var that = this, | ||
gspec = { "/type/item": { "type": "/type/type", "properties": {}} }; | ||
if (spec) gspec["/type/item"]["indexes"] = spec.indexes || {}; | ||
this.type = new Data.Type(spec.type, this); | ||
this.objects = []; | ||
this.length = 0; | ||
this.keys = {}; | ||
// 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(); | ||
} | ||
_.each(spec.objects, _.bind(function(obj) { | ||
this.add(obj); | ||
}, this)); | ||
}; | ||
// Creates a Data.Collection using a Data.Type, and an array of Data.Objects | ||
Data.Collection.create = function(type, objects) { | ||
var c = new Data.Collection({type: type, objects: []}); | ||
c.objects = objects; | ||
c.length = objects.length; | ||
// Register keys for fast lookup | ||
_.each(objects, function(o, i) { | ||
c.keys[o._id] = i; | ||
}); | ||
return c; | ||
}; | ||
_.extend(Data.Collection.prototype, { | ||
_.extend(Data.Collection.prototype, _.Events, Data.Query, { | ||
// Get an object (item) from the collection | ||
get: function(key) { | ||
return this.g.get.apply(this.g, arguments); | ||
get: function(id) { | ||
if (id.match('^/type/')) return this.type; | ||
return this.objects[this.keys[id]]; | ||
}, | ||
// Set (add) a new object to the collection | ||
set: function(id, properties) { | ||
this.g.set(id, _.extend(properties, {type: "/type/item"})); | ||
// Return object at a given index | ||
at: function(index) { | ||
return this.objects[index]; | ||
}, | ||
// Find objects that match a particular query | ||
find: function(query) { | ||
query["type|="] = "/type/item"; | ||
return this.g.find(query); | ||
// Return index for a given key | ||
index: function(key) { | ||
return this.keys[key]; | ||
}, | ||
// 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() | ||
}); | ||
// Return key for a given index | ||
key: function(index) { | ||
return this.objects[index]._id; | ||
}, | ||
// 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('nodes', '/type/item').all('properties'); | ||
// Add a new object to the collection | ||
add: function(obj) { | ||
obj._id = obj._id ? obj._id : Data.uuid('/' + _.last(this.type._id.split('/')) + '/'); | ||
obj.type = this.type._id; | ||
var o = this.get(obj._id); | ||
if (!o) { | ||
o = new Data.Object(obj, this); | ||
this.keys[o._id] = this.objects.length; | ||
this.objects.push(o); | ||
this.length = this.objects.length; | ||
} else { | ||
o.set(obj); | ||
} | ||
return o; | ||
}, | ||
// Convenience function for accessing items | ||
items: function() { | ||
return this.g.objects(); | ||
// Find objects that match a particular query | ||
find: function(qry) { | ||
qry["type"] = this.type._id; | ||
return this.query(qry); | ||
}, | ||
// Convenience function for accessing indexes defined on the collection | ||
indexes: function() { | ||
return this.g.get('/type/item').indexes; | ||
// Iterate over all objects in the collection | ||
each: function (fn) { | ||
_.each(this.objects, function(object, i) { | ||
fn.call(this, object, object._id, i); | ||
}, this); | ||
return this; | ||
}, | ||
@@ -1579,7 +424,7 @@ | ||
return { | ||
properties: this.g.toJSON()["/type/item"].properties, | ||
items: this.g.objects().toJSON() | ||
} | ||
type: this.type.toJSON(), | ||
objects: _.map(this.objects, function(n) { return n.toJSON(); }) | ||
}; | ||
} | ||
}); | ||
})(); | ||
})(); |
@@ -1,36 +0,8 @@ | ||
(function(){var e;e=typeof exports!=="undefined"?exports:this.Data={};e.VERSION="0.4.1";var g=this._;if(!g&&typeof require!=="undefined")g=require("underscore");e.VALUE_TYPES=["string","object","number","boolean","date"];e.isValueType=function(a){return g.include(e.VALUE_TYPES,a)};e.matches=function(a,b){b=g.isArray(b)?b:[b];var c=false;g.each(b,function(d){if(!c){var f=false;g.each(d,function(h,i){if(!f){var k,l=i.match(/^([a-z_]{1,30})(=|==|!=|>|>=|<|<=|\|=|&=)?$/),j=l[1];l=l[2]||(j=="type"||g.isArray(h)? | ||
"|=":"=");if(l==="|="){l=g.isArray(h)?h:[h];var m=g.isArray(a[j])?a[j]:[a[j]];k=false;g.each(l,function(o){if(g.include(m,o))k=true})}else if(l==="&="){l=g.isArray(h)?h:[h];m=g.isArray(a[j])?a[j]:[a[j]];k=g.intersect(m,l).length===l.length}else switch(l){case "!=":k=!g.isEqual(a[j],h);break;case ">":k=a[j]>h;break;case ">=":k=a[j]>=h;break;case "<":k=a[j]<h;break;case "<=":k=a[j]<=h;break;default:k=g.isEqual(a[j],h);break}if(!k)return f=true}});if(!f)return c=true}});return c};e.uuid=function(a){for(var b= | ||
"0123456789abcdefghijklmnopqrstuvwxyz".split(""),c=[],d=0;d<32;d++)c[d]=b[0|Math.random()*16];return(a?a:"")+c.join("")};g.Events={bind:function(a,b){this._callbacks||(this._callbacks={});(this._callbacks[a]||(this._callbacks[a]=[])).push(b);return this},unbind:function(a,b){var c;if(a){if(c=this._callbacks)if(b){c=c[a];if(!c)return this;for(var d=0,f=c.length;d<f;d++)if(b===c[d]){c.splice(d,1);break}}else c[a]=[]}else this._callbacks={};return this},trigger:function(a){var b,c,d,f;if(!(c=this._callbacks))return this; | ||
if(b=c[a]){d=0;for(f=b.length;d<f;d++)b[d].apply(this,Array.prototype.slice.call(arguments,1))}if(b=c.all){d=0;for(f=b.length;d<f;d++)b[d].apply(this,arguments)}return this}};var q=function(){};g.inherits=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){return a.apply(this,arguments)};q.prototype=a.prototype;d.prototype=new q;b&&g.extend(d.prototype,b);c&&g.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d};e.Hash=function(a){var b=this;this.data= | ||
{};this.keyOrder=[];this.length=0;if(a instanceof Array)g.each(a,function(c,d){b.set(d,c)});else a instanceof Object&&g.each(a,function(c,d){b.set(d,c)});this.initialize&&this.initialize(attributes,options)};g.extend(e.Hash.prototype,g.Events,{clone:function(){var a=new e.Hash;a.length=this.length;g.each(this.data,function(b,c){a.data[c]=b});a.keyOrder=this.keyOrder.slice(0,this.keyOrder.length);return a},set:function(a,b,c){var d;if(a===undefined)return this;if(this.data[a])d=this.index(a);else{if(c!== | ||
undefined){d=this.select(function(h,i,k){return k<c});var f=this.select(function(h,i,k){return k>=c});this.keyOrder=[].concat(d.keyOrder);this.keyOrder.push(a);this.keyOrder=this.keyOrder.concat(f.keyOrder)}else this.keyOrder.push(a);d=this.length;this.length+=1}this.data[a]=b;this[d]=this.data[a];this.trigger("set",a);return this},del:function(a){if(this.data[a]){var b=this.length,c=this.index(a);delete this.data[a];this.keyOrder.splice(c,1);Array.prototype.splice.call(this,c,1);this.length=b-1; | ||
this.trigger("del",a)}return this},get:function(a){return this.data.hasOwnProperty(a)?this.data[a]:undefined},at:function(a){return this.data[this.keyOrder[a]]},first:function(){return this.at(0)},range:function(a,b){for(var c=new e.Hash,d=a;d<=b;d++)c.set(this.key(d),this.at(d));return c},rest:function(a){return this.range(a,this.length-1)},last:function(){return this.at(this.length-1)},key:function(a){return this.keyOrder[a]},index:function(a){return this.keyOrder.indexOf(a)},each:function(a){var b= | ||
this;g.each(this.keyOrder,function(c,d){a.call(b,b.data[c],c,d)});return this},values:function(){var a=[];this.each(function(b){a.push(b)});return a},keys:function(){return g.clone(this.keyOrder)},toArray:function(){var a=[];this.each(function(b,c){a.push({key:c,value:b})});return a},toJSON:function(){var a={};this.each(function(b,c){a[c]=b.toJSON?b.toJSON():b});return a},map:function(a){var b=this.clone(),c=this;b.each(function(d,f,h){b.data[c.key(h)]=a.call(b,d)});return b},select:function(a){var b= | ||
new e.Hash,c=this;this.each(function(d,f,h){a.call(c,d,f,h)&&b.set(f,d)});return b},sort:function(a){var b=this.clone();sortedKeys=b.toArray().sort(a);b.keyOrder=g.map(sortedKeys,function(c){return c.key});return b},intersect:function(a){var b=this,c=new e.Hash;if(a.length<b.length){b=a;a=this}b.each(function(d,f){a.get(f)&&c.set(f,d)});return c},union:function(a){var b=new e.Hash;this.each(function(c,d){b.set(d,c)});a.each(function(c,d){b.get(d)||b.set(d,c)});return b},difference:function(a){result= | ||
new e.Hash;this.each(function(b,c){a.get(c)||result.set(c,b)});return result}});e.Comparators={};e.Comparators.ASC=function(a,b){return a.value===b.value?0:a.value<b.value?-1:1};e.Comparators.DESC=function(a,b){return a.value===b.value?0:a.value>b.value?-1:1};e.Aggregators={};e.Aggregators.SUM=function(a){var b=0;a.each(function(c){if(g.isNumber(c))b+=c});return b};e.Aggregators.MIN=function(a){var b=Infinity;a.each(function(c){if(g.isNumber(c)&&c<b)b=c});return b};e.Aggregators.MAX=function(a){var b= | ||
-Infinity;a.each(function(c){if(g.isNumber(c)&&c>b)b=c});return b};e.Aggregators.AVG=function(a){var b=0,c=0;a.each(function(d){if(g.isNumber(d)){b+=d;c+=1}});return c===0?0:b/c};e.Aggregators.COUNT=function(a){return a.length};e.Modifiers={};e.Modifiers.DEFAULT=function(a){return a};e.Modifiers.MONTH=function(a){return a.getMonth()};e.Modifiers.QUARTER=function(a){return Math.floor(a.getMonth()/3)+1};e.Transformers={group:function(a,b,c,d){function f(j){var m=new e.Hash;g.each(c,function(r,n){var p= | ||
k[c[n]].get(j[n]).referencedObjects;m=n===0?m.union(p):m.intersect(p)});if(j.length===0)m=a.objects();if(m.length===0)return null;var o={type:b._id};g.each(i[b._id].properties,function(r,n){if(g.include(c,n))o[n]=j[g.indexOf(c,n)];else{var p=m.map(function(s){return s.get(d[n].property||n)});o[n]=(d[n].aggregator||e.Aggregators.SUM)(p)}});return o}function h(j,m){if(j===c.length-1){var o=f(m);o&&l.set(m.join("::"),o)}else{j+=1;k[c[j]].each(function(r,n){h(j,m.concat([n]))})}}var i={};b=a.get(b);var k= | ||
{};i[b._id]={type:"/type/type",properties:{},indexes:b.indexes};g.each(c,function(j){i[b._id].properties[j]=b.properties().get(j).toJSON()});g.each(d,function(j,m){var o=b.properties().get(j.property||m).toJSON();if(j.name)o.name=j.name;i[b._id].properties[m]=o});var l=new e.Graph(i);g.each(c,function(j){k[j]=b.properties().get(j).all("values")});h(-1,[]);return l}};e.Node=function(a){this.nodeId=e.Node.generateId();if(a)this.val=a.value;this._properties={};this.initialize&&this.initialize(a)};e.Node.nodeCount= | ||
0;e.Node.generateId=function(){return e.Node.nodeCount+=1};g.extend(e.Node.prototype,g.Events,{identity:function(){return this.nodeId},replace:function(a,b){this._properties[a]=b},set:function(a,b,c){this._properties[a]||(this._properties[a]=new e.Hash);this._properties[a].set(b,c instanceof e.Node?c:new e.Node({value:c}));return this},get:function(a,b){if(b!==undefined&&this._properties[a]!==undefined)return this._properties[a].get(b)},all:function(a){return this._properties[a]},first:function(a){return(a= | ||
this._properties[a])?a.first():null},value:function(a){return this.values(a).first()},values:function(a){if(!this.all(a))return new e.Hash;return this.all(a).map(function(b){return b.val})}});e.Adapter=function(a){this.config=a};e.Adapters={};e.Property=g.inherits(e.Node,{constructor:function(a,b,c){e.Node.call(this);this._id=this.key=b;this.type=a;this.unique=c.unique;this.name=c.name;this.meta=c.meta||{};this.validator=c.validator;this.required=c.required;this["default"]=c["default"];this.expectedTypes= | ||
g.isArray(c.type)?c.type:[c.type];this.replace("values",new e.Hash)},isValueType:function(){return e.isValueType(this.expectedTypes[0])},isObjectType:function(){return!this.isValueType()},registerValues:function(a,b){var c=this,d=new e.Hash;g.each(a,function(f,h){if(f!==undefined){var i;if(c.isValueType()&&c.expectedTypes[0]==="object"){i=new e.Node({value:f});d.set(h,i)}else{if(b.all(c.key))i=b.all(c.key).get(f);if(!i){i=c.get("values",f);if(!i){if(c.isObjectType()){if(typeof f==="object")f=c.type.g.set(null, | ||
f)._id;i=c.type.g.get("nodes",f);if(!i){i=new e.Object(c.type.g,f);c.type.g.set("nodes",f,i)}}else{i=new e.Node({value:f});i.referencedObjects=new e.Hash}c.set("values",f,i)}i.referencedObjects.set(b._id,b)}d.set(f,i)}}});b.all(c.key)&&this.unregisterValues(b.all(c.key).difference(d),b);return d},unregisterValues:function(a,b){var c=this;a.each(function(d,f){d.referencedObjects&&d.referencedObjects.length>1?d.referencedObjects.del(b._id):c.all("values").del(f)})},aggregate:function(a){return a(this.values("values"))}, | ||
toJSON:function(){return{name:this.name,type:this.expectedTypes,unique:this.unique,meta:this.meta,validator:this.validator,required:this.required,"default":this["default"]}}});e.Type=g.inherits(e.Node,{constructor:function(a,b,c){var d=this;e.Node.call(this);this.g=a;this._id=this.key=b;this._rev=c._rev;this._conflicted=c._conflicted;this.type=c.type;this.name=c.name;this.meta=c.meta||{};this.indexes=c.indexes;d.replace("properties",new e.Hash);g.each(c.properties,function(f,h){d.set("properties", | ||
h,new e.Property(d,h,f))})},properties:function(){return this.all("properties")},objects:function(){return this.all("nodes")},toJSON:function(){var a={_id:this._id,type:"/type/type",name:this.name,properties:{}};if(this._rev)a._rev=this._rev;if(this.meta&&g.keys(this.meta).length>0)a.meta=this.meta;if(this.indexes&&g.keys(this.indexes).length>0)a.indexes=this.indexes;this.all("properties").each(function(b){var c=a.properties[b.key]={name:b.name,unique:b.unique,type:b.expectedTypes,required:b.required? | ||
true:false};if(b["default"])c["default"]=b["default"];if(b.validator)c.validator=b.validator;if(b.meta&&g.keys(b.meta).length>0)c.meta=b.meta});return a}});e.Object=g.inherits(e.Node,{constructor:function(a,b,c){e.Node.call(this);this.g=a;this._id=this.key=b;this.html_id=b.replace(/\//g,"_");this._dirty=true;this.errors=[];this._types=new e.Hash;this.referencedObjects=new e.Hash;if(c)this.data=c},types:function(){return this._types},toString:function(){return this.get("name")||this.val||this._id}, | ||
properties:function(){var a=new e.Hash;this._types.each(function(b){b.all("properties").each(function(c){a.set(c.key,c)})});return a},build:function(){var a=this,b=g.isArray(this.data.type)?this.data.type:[this.data.type];if(!this.data)throw Error("Object has no data, and cannot be built");this._rev=this.data._rev;this._conflicted=this.data._conflicted;this._deleted=this.data._deleted;this.type=this.g.get("nodes",g.last(b));g.each(b,function(c){a._types.set(c,a.g.get("nodes",c));a._types.get(c).all("properties").each(function(d, | ||
f){function h(i){i=g.isArray(i)?i:[i];a.replace(d.key,d.registerValues(i,a))}if(a.data[f]!==undefined)h(a.data[f]);else d["default"]&&h(d["default"])})});this._dirty&&this.g.trigger("dirty",this)},validate:function(){if(this.type.key==="/type/type")return true;var a=this;this.errors=[];this.properties().each(function(b,c){if(a.get(c)===undefined||a.get(c)===null||a.get(c)==="")b.required&&a.errors.push({property:c,message:'Property "'+b.name+'" is required'});else{var d=b.expectedTypes,f=function(h, | ||
i){if(g.include(i,typeof h))return true;if(!h.data)return true;if(h instanceof e.Object&&g.intersect(i,h.types().keys()).length>0)return true;if(typeof h==="object"&&g.include(i,h.constructor.name.toLowerCase()))return true;return false};b.unique&&!f(a.get(c),d)&&a.errors.push({property:c,message:'Invalid type for property "'+b.name+'"'});!b.unique&&!g.all(a.get(c).values(),function(h){return f(h,d)})&&a.errors.push({property:c,message:'Invalid value type for property "'+b.name+'"'})}if(b.validator)RegExp(b.validator).test(a.get(c))|| | ||
a.errors.push({property:c,message:'Invalid value for property "'+b.name+'"'})});return this.errors.length===0},get:function(a,b){if(!this.data)return null;var c=this.properties().get(a);if(!c)return null;return arguments.length===1?c.isObjectType()?c.unique?this.first(a):this.all(a):c.unique?this.value(a):this.values(a):e.Node.prototype.get.call(this,a,b)},set:function(a){var b=this;if(arguments.length===1)g.each(a,function(c,d){var f=b.properties().get(d);if(f){b.replace(f.key,f.registerValues(g.isArray(c)? | ||
c:[c],b));b._dirty=true;b.g.trigger("dirty",b);b.g.snapshot()}});else return e.Node.prototype.set.call(this,arguments[0],arguments[1],arguments[2])},toJSON:function(){var a=this;result={};g.each(this._properties,function(b,c){var d=a.properties().get(c);result[c]=d.isObjectType()?d.unique?a.all(c).keys()[0]:a.all(c).keys():d.unique?a.value(c):a.values(c).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}});g.extend(e.Object.prototype,g.Events);e.Graph=g.inherits(e.Node,{constructor:function(a,b){e.Node.call(this);this.watchers={};this.replace("nodes",new e.Hash);if(a){this.merge(a,b.dirty);if(b.persistent){this.persistent=b.persistent;this.restore()}}},connect:function(a,b){if(typeof exports!=="undefined")this.adapter=new (require(__dirname+"/adapters/"+a+"_adapter"))(this,b);else{if(!e.Adapters[a])throw Error('Adapter "'+a+'" not found');this.adapter=new e.Adapters[a](this,b)}return this}, | ||
connected:function(a){if(this.adapter.realtime)this.connectedCallback=a;else a()},serve:function(a){require(__dirname+"/server").initialize(a,this)},watch:function(a,b,c){this.watchers[a]=c;this.adapter.watch(a,b,function(){})},unwatch:function(a){delete this.watchers[a];this.adapter.unwatch(a,function(){})},empty:function(){var a=this;g.each(this.objects().keys(),function(b){a.del(b);a.all("nodes").del(b)})},merge:function(a,b){var c=this;g.select(a,function(f,h){if(f.type==="/type/type"||f.type=== | ||
"type"){if(!c.get("nodes",h)){c.set("nodes",h,new e.Type(c,h,f));c.get(h)._dirty=f._dirty?f._dirty:b}return true}return false});var d=g.select(a,function(f,h){if(f.type!=="/type/type"&&f.type!=="type"){var i=c.get("nodes",h),k=g.isArray(f.type)?f.type:[f.type];if(i)i.data=f;else{i=new e.Object(c,h,f);c.set("nodes",h,i)}g.each(k,function(l){if(!c.get("nodes",l))throw Error("Type '"+l+"' not found for "+h+"...");c.get("nodes",l).set("nodes",h,i)});c.get(h)._dirty=f._dirty?f._dirty:b;if(!f._id)f._id= | ||
h;return true}return false});g.each(d,function(f){f=c.get(f._id);f.data&&f.build()});this.snapshot();return this},set:function(a){if(arguments.length===2)a=g.extend(arguments[1],{_id:arguments[0]});var b=g.isArray(a.type)?a.type:[a.type];if(arguments.length<=2){a._id=a._id?a._id:e.uuid("/"+g.last(g.last(b).split("/"))+"/");b=this.get(a._id)?this.get(a._id):new e.Object(this,a._id,g.clone(a),true);b.data=a;b._dirty=true;b.build();this.set("nodes",a._id,b);this.snapshot();return b}else return e.Node.prototype.set.call(this, | ||
arguments[0],arguments[1],arguments[2])},get:function(a){return arguments.length===1?this.get("nodes",a):e.Node.prototype.get.call(this,arguments[0],arguments[1])},del:function(a){var b=this.get(a);if(b){b._deleted=true;b._dirty=true;b.properties().each(function(c,d){var f=b.all(d);f&&c.unregisterValues(f,b)});this.trigger("dirty",b);this.snapshot()}},find:function(a){return this.objects().select(function(b){return e.matches(b.toJSON(),a)})},snapshot:function(){this.persistent&&localStorage.setItem("graph", | ||
JSON.stringify(this.toJSON(true)))},restore:function(){var a=JSON.parse(localStorage.getItem("graph"));a&&this.merge(a)},fetch:function(a,b,c){var d=this,f=new e.Hash;if(typeof b==="function"&&typeof c==="undefined"){c=b;b={}}this.adapter.read(a,b,function(h,i){if(i){d.merge(i,false);g.each(i,function(k,l){f.set(l,d.get(l))})}h?c(h):c(null,f)})},sync:function(a){a=a||function(){};var b=this,c=b.dirtyNodes(),d=new e.Hash;c.select(function(f,h){if(!f.validate||f.validate&&f.validate())d.set(h,f)}); | ||
this.adapter.write(d.toJSON(),function(f,h){if(f)return a(f);b.merge(h,false);d.each(function(k,l){if(h[l]){k._dirty=false;k._rejected=false}else k._rejected=true});this.persistent&&b.snapshot();b.invalidNodes().length>0&&b.trigger("invalid");b.conflictedNodes().length>0&&b.trigger("conflicted");b.rejectedNodes().length>0&&b.trigger("rejected");var i=b.invalidNodes().union(b.conflictedNodes()).union(b.rejectedNodes()).length;a(i>0?i+" unsaved nodes":null)})},group:function(a,b,c){var d=new e.Collection; | ||
d.g=e.Transformers.group(this,a,b,c);return d},types:function(){return this.all("nodes").select(function(a){return a.type==="/type/type"||a.type==="type"})},objects:function(){return this.all("nodes").select(function(a){return a.type!=="/type/type"&&a.type!=="type"&&a.data&&!a._deleted})},dirtyNodes:function(){return this.all("nodes").select(function(a){return a._dirty&&(a.data||a instanceof e.Type)})},invalidNodes:function(){return this.all("nodes").select(function(a){return a.errors&&a.errors.length> | ||
0})},conflictedNodes:function(){return this.all("nodes").select(function(a){return a._conflicted})},rejectedNodes:function(){return this.all("nodes").select(function(a){return a._rejected})},toJSON:function(a){var b={};this.all("nodes").each(function(c,d){if(c.data||c instanceof e.Type){b[d]=c.toJSON();if(a){if(c._dirty)b[d]._dirty=true;if(c._conflicted)b[d]._conclicted=true;if(c._rejected)b[d].rejected=true}}});return b}});g.extend(e.Graph.prototype,g.Events);e.Collection=function(a){var b=this, | ||
c={"/type/item":{type:"/type/type",properties:{}}};if(a)c["/type/item"].indexes=a.indexes||{};if(a){g.each(a.properties,function(d,f){c["/type/item"].properties[f]=d});this.g=new e.Graph(c);g.each(a.items,function(d,f){b.set(f,d)})}else this.g=new e.Graph};g.extend(e.Collection.prototype,{get:function(){return this.g.get.apply(this.g,arguments)},set:function(a,b){this.g.set(a,g.extend(b,{type:"/type/item"}))},find:function(a){a["type|="]="/type/item";return this.g.find(a)},filter:function(a){return new e.Collection({properties:this.properties().toJSON(), | ||
items:this.find(a).toJSON()})},group:function(a,b){var c=new e.Collection;c.g=e.Transformers.group(this.g,"/type/item",a,b);return c},properties:function(){return this.g.get("nodes","/type/item").all("properties")},items:function(){return this.g.objects()},indexes:function(){return this.g.get("/type/item").indexes},toJSON:function(){return{properties:this.g.toJSON()["/type/item"].properties,items:this.g.objects().toJSON()}}})})(); | ||
(function(){var d;d=typeof exports!=="undefined"?exports:this.Data={};d.VERSION="0.6.0";var b=this._;if(!b&&typeof require!=="undefined")b=require("underscore");d.VALUE_TYPES=["string","object","number","boolean","date"];d.isValueType=function(a){return b.include(d.VALUE_TYPES,b.last(a))};d.uuid=function(a){for(var c="0123456789abcdefghijklmnopqrstuvwxyz".split(""),e=[],f=0;f<32;f++)e[f]=c[0|Math.random()*16];return(a?a:"")+e.join("")};d.Query={query:function(a){function c(g,l){var j=true;b.find(l, | ||
function(h,k){var i=k==="type"?g.types:g.properties[k];if(b.intersect(b.isArray(h)?h:[h],b.isArray(i)?i:[i]).length===0){j=false;return true}});return j}var e=this.get(a.type),f=b.select(this.objects,function(g){return c(g,a)});return d.Collection.create(e,f)}};d.Type=function(a){this._id=a._id;this.type="/type/type";this.name=a.name;this.meta=a.meta||{};this.indexes=a.indexes||{};this.properties=a.properties;b.each(this.properties,b.bind(function(c){c.type=b.isArray(c.type)?c.type:[c.type];c.unique= | ||
b.isBoolean(c.unique)?c.unique:true},this))};b.extend(d.Type.prototype,b.Events,{toJSON:function(){return{_id:this._id,type:"/type/type",properties:this.properties,meta:this.meta,indexes:b.map(this.indexes,function(a){return a.properties})}}});d.Object=function(a,c){this._id=a._id;this.host=c;this.properties={};this.set(a)};b.extend(d.Object.prototype,b.Events,{type:function(){return this.host.get(b.last(this.types))},property:function(a){var c=null;b.find(this.types.reverse(),b.bind(function(e){return c= | ||
this.host.get(e).properties[a]},this));return c},get:function(a){var c=this.property(a);a=this.properties[a];if(!c||!a)return null;return d.isValueType(c.type)?a:c.unique?this.host.get(a):b.map(a,b.bind(function(e){return this.host.get(e)},this))},set:function(a){var c=this;if(a.type)this.types=b.isArray(a.type)?a.type:[a.type];if(a.meta)this.meta=this.object.meta;b.each(a,b.bind(function(e,f){!c.property(f)||f==="type"||(c.properties[f]=e)},this))},toJSON:function(){return b.extend(this.properties, | ||
{_id:this._id,type:this.types})}});d.Graph=function(a){this.nodes=[];this.objects=[];this.types=[];this.keys={};a&&this.merge(a)};b.extend(d.Graph.prototype,d.Query,b.Events,{merge:function(a){b.each(a,b.bind(function(c,e){this.set(b.extend(c,{_id:e}))},this));return this},get:function(a){return this.nodes[this.keys[a]]},set:function(a){function c(){return b.last(e)==="/type/type"?new d.Type(a):new d.Object(a,this)}var e=b.isArray(a.type)?a.type:[a.type];a._id=a._id?a._id:d.uuid("/"+b.last(b.last(e).split("/"))+ | ||
"/");var f=this.get(a._id);if(f)f.set(a);else{f=c.apply(this);this.keys[a._id]=this.nodes.length;this.nodes.push(f);b.last(e)==="/type/type"?this.types.push(f):this.objects.push(f)}return f},find:function(a){return this.query(a)},del:function(a){if(a=this.get(a))a._deleted=true},toJSON:function(){var a={};b.each(this.nodes,function(c){a[c._id]=c.toJSON()});return a}});d.Collection=function(a){this.type=new d.Type(a.type,this);this.objects=[];this.length=0;this.keys={};b.each(a.objects,b.bind(function(c){this.add(c)}, | ||
this))};d.Collection.create=function(a,c){var e=new d.Collection({type:a,objects:[]});e.objects=c;e.length=c.length;b.each(c,function(f,g){e.keys[f._id]=g});return e};b.extend(d.Collection.prototype,b.Events,d.Query,{get:function(a){if(a.match("^/type/"))return this.type;return this.objects[this.keys[a]]},at:function(a){return this.objects[a]},index:function(a){return this.keys[a]},key:function(a){return this.objects[a]._id},add:function(a){a._id=a._id?a._id:d.uuid("/"+b.last(this.type._id.split("/"))+ | ||
"/");a.type=this.type._id;var c=this.get(a._id);if(c)c.set(a);else{c=new d.Object(a,this);this.keys[c._id]=this.objects.length;this.objects.push(c);this.length=this.objects.length}return c},find:function(a){a.type=this.type._id;return this.query(a)},each:function(a){b.each(this.objects,function(c,e){a.call(this,c,c._id,e)},this);return this},toJSON:function(){return{type:this.type.toJSON(),objects:b.map(this.objects,function(a){return a.toJSON()})}}})})(); |
var countries_fixture = { | ||
"items": { | ||
"austria": { | ||
"name": "Austria", | ||
"official_language": "German Language", | ||
"form_of_government": [ | ||
"Federal republic", | ||
"Parliamentary republic" | ||
], | ||
"currency_used": "Euro", | ||
"population": 8356700, | ||
"gdp_nominal": 432400000000.0, | ||
"area": 83872.0, | ||
"date_founded": "1955-07-27" | ||
"type": { | ||
"_id": "/type/country", | ||
"name": "Countries", | ||
"properties": { | ||
"name": {"name": "Country Name", "type": "string" }, | ||
"languages": {"name": "Languages spoken", "type": "string" }, | ||
"population": { "name": "Population", "type": "number" }, | ||
"gdp": { "name": "GDP per capita", "type": "number" } | ||
}, | ||
"uk": { | ||
"name": "United Kingdom", | ||
"official_language": "English Language", | ||
"form_of_government": [ | ||
"Parliamentary system", | ||
"Constitutional monarchy" | ||
], | ||
"currency_used": "UK \u00a3", | ||
"population": 61612300, | ||
"gdp_nominal": 2787000000000.0, | ||
"area": 244820.0, | ||
"date_founded": "1707-05-01" | ||
}, | ||
"usa": { | ||
"name": "United States of America", | ||
"official_language": "English Language", | ||
"form_of_government": [ | ||
"Federal republic", | ||
"Constitution", | ||
"Democracy", | ||
"Republic", | ||
"Presidential system", | ||
"Constitutional republic" | ||
], | ||
"currency_used": "US$", | ||
"population": 306108000, | ||
"gdp_nominal": 14330000000000.0, | ||
"area": 9826675.0, | ||
"date_founded": "1776-07-04" | ||
}, | ||
"ger": { | ||
"name": "Germany", | ||
"official_language": "German Language", | ||
"form_of_government": [ | ||
"Federal republic", | ||
"Democracy", | ||
"Parliamentary republic" | ||
], | ||
"currency_used": "Euro", | ||
"population": 82062200, | ||
"gdp_nominal": 3818000000000.0, | ||
"area": 357092.9, | ||
"date_founded": "1949-05-23" | ||
}, | ||
"fra": { | ||
"name": "France", | ||
"official_language": "French Language", | ||
"form_of_government": [ | ||
"Republic", | ||
"Semi-presidential system" | ||
], | ||
"currency_used": "Euro", | ||
"population": 65073482, | ||
"gdp_nominal": 2987000000000.0, | ||
"area": 674843.0, | ||
"date_founded": "1792" | ||
}, | ||
"ita": { | ||
"name": "Italy", | ||
"official_language": "Italian Language", | ||
"form_of_government": [ | ||
"Parliamentary republic" | ||
], | ||
"currency_used": "Euro", | ||
"population": 60090400, | ||
"gdp_nominal": 2399000000000.0, | ||
"area": 301338.0, | ||
"date_founded": "1861-03-17" | ||
"indexes": { | ||
"by_name": ["name"] | ||
} | ||
}, | ||
"properties": { | ||
"name": { | ||
"name": "Country Name", | ||
"type": "string", | ||
"unique": true | ||
"objects": [ | ||
{ | ||
"_id": "at", | ||
"name": "Austria", | ||
"languages": ["German", "Austrian"], | ||
"population": 8.3, | ||
"gdp": 41.805 | ||
}, | ||
"official_language": { | ||
"name": "Official language", | ||
"type": "string", | ||
"unique": true | ||
{ | ||
"_id": "de", | ||
"name": "Germany", | ||
"languages": ["German"], | ||
"population": 82, | ||
"gdp": 46.860 | ||
}, | ||
"form_of_government": { | ||
"name": "Form of governmennt", | ||
"type": "string", | ||
"unique": false | ||
{ | ||
"_id": "us", | ||
"name": "United States of America", | ||
"languages": ["German", "English", "Spanish", "Chinese", "French"], | ||
"population": 311, | ||
"gdp": 36.081 | ||
}, | ||
"currency_used": { | ||
"name": "Currency used", | ||
"type": "string", | ||
"unique": true | ||
{ | ||
"_id": "uk", | ||
"name": "United Kingdom", | ||
"languages": ["English", "Irish", "Scottish Gaelic"], | ||
"population": 62.3, | ||
"gdp": 36.081 | ||
}, | ||
"population": { | ||
"name": "Population", | ||
"type": "number", | ||
"unique": true | ||
{ | ||
"_id": "es", | ||
"name": "Spain", | ||
"languages": ["Spanish"], | ||
"population": 30.6, | ||
"gdp": 36.081 | ||
}, | ||
"gdp_nominal": { | ||
"name": "GDP nominal", | ||
"type": "number", | ||
"unique": true | ||
{ | ||
"_id": "gr", | ||
"name": "Greece", | ||
"languages": ["Greek"], | ||
"population": 11.0, | ||
"gdp": 36.081 | ||
}, | ||
"area": { | ||
"name": "Area", | ||
"type": "number", | ||
"unique": true | ||
}, | ||
"date_founded": { | ||
"name": "Date founded", | ||
"type": "date", | ||
"unqiue": true | ||
{ | ||
"_id": "ca", | ||
"name": "Canada", | ||
"languages": ["English", "French", "Spanish"], | ||
"population": 40.1, | ||
"gdp": 40.457 | ||
} | ||
} | ||
} | ||
] | ||
}; |
@@ -31,2 +31,5 @@ var documents_fixture = { | ||
} | ||
}, | ||
"indexes": { | ||
"by_authors": ["authors"] | ||
} | ||
@@ -33,0 +36,0 @@ }, |
{ | ||
"name" : "data", | ||
"description" : "A Javascript data manipulation library.", | ||
"description" : "A Javascript data representation library.", | ||
"url" : "http://github.com/michael/data/", | ||
"keywords" : ["util", "functional", "linked-data", "server", "client", "browser"], | ||
"keywords" : ["util", "linked-data", "server", "client", "browser"], | ||
"author" : "Michael Aufreiter", | ||
"contributors" : [], | ||
"dependencies" : ["underscore", "now"], | ||
"dependencies" : ["underscore"], | ||
"lib" : ".", | ||
"main" : "index", | ||
"version" : "0.4.1" | ||
"version" : "0.6.0" | ||
} |
@@ -18,3 +18,2 @@ var _ = require('underscore'); | ||
// AJAX interface | ||
@@ -40,67 +39,7 @@ // -------------- | ||
// NowJS interface | ||
// -------------- | ||
var nowjs = require('now'); | ||
var everyone = nowjs.initialize(server); | ||
// Watcher groups | ||
var channels = {}; | ||
everyone.connected(function() {}); | ||
everyone.disconnected(function(){}); | ||
// Dispatch to all interested parties | ||
function dispatchUpdates(nodes) { | ||
var notifications = {}; | ||
// For each node, check channels | ||
_.each(nodes, function(node, key, index) { | ||
_.each(channels, function(channel) { | ||
if (Data.matches(node, channel.query)) { | ||
notifications[channel.name] = notifications[channel.name] ? notifications[channel.name] : {}; | ||
notifications[channel.name][key] = node; | ||
} | ||
}); | ||
server.post('/graph/pull', function(req, res) { | ||
graph.adapter.pull(req.body, function(err, g) { | ||
err ? res.send(JSON.stringify({error: err})) : res.send(JSON.stringify({"status": "ok", "graph": g})); | ||
}); | ||
// Dispatch | ||
_.each(notifications, function(nodes, groupName) { | ||
nowjs.getGroup(groupName).now.update(groupName, nodes); | ||
}); | ||
}; | ||
// Register a new watcher | ||
everyone.now.watch = function(channel, query, callback) { | ||
var group = nowjs.getGroup(channel); | ||
group.addUser(this.user.clientId); | ||
channels[channel] = { | ||
name: channel, | ||
group: group, | ||
query: query | ||
}; | ||
callback(); | ||
}; | ||
everyone.now.unwatch = function(name, callback) { | ||
var group = nowjs.getGroup(name); | ||
group.removeUser(this.user.clientId); | ||
// TODO: remove the whole group and channel if empty | ||
}; | ||
// Read graph | ||
everyone.now.read = function(query, options, callback) { | ||
graph.adapter.read(query, options, function(err, g) { | ||
callback(err, g); | ||
}, this.user); | ||
}; | ||
// Write graph | ||
everyone.now.write = function(nodes, callback) { | ||
graph.adapter.write(nodes, function(err, g) { | ||
callback(err, g); | ||
// Notify channel users | ||
dispatchUpdates(g); | ||
}, this.user); | ||
}; | ||
}); | ||
}; |
@@ -1,753 +0,245 @@ | ||
// Data.js — A utility belt for data manipulation | ||
// Data.js — A uniform interface to domain data | ||
// ------------- | ||
var items; | ||
// Data.Hash | ||
// ------------- | ||
(function() { | ||
module("Data.Hash", { | ||
setup: function() { | ||
items = new Data.Hash(); | ||
items.set("at", "Austria"); | ||
items.set("de", "Germany"); | ||
}, | ||
teardown: function() { | ||
delete items; | ||
} | ||
}); | ||
// Helpers | ||
// ------- | ||
test("construction from Array", function() { | ||
var numbers = new Data.Hash([1, 2, 5, 10]); | ||
ok(numbers.at(0) === 1); | ||
ok(numbers.at(1) === 2); | ||
ok(numbers.at(2) === 5); | ||
ok(numbers.at(3) === 10); | ||
suite('Data.js API', function() { | ||
var stopwatch, items; | ||
// key equals index | ||
ok(numbers.get(0) === 1); | ||
ok(numbers.get(1) === 2); | ||
ok(numbers.get(2) === 5); | ||
ok(numbers.get(3) === 10); | ||
ok(numbers.length === 4); | ||
}); | ||
// Data.Graph | ||
// ------------- | ||
test("construction from Hash", function() { | ||
var countries = new Data.Hash({ | ||
'at': 'Austria', | ||
'de': 'Germany', | ||
'ch': 'Switzerland' | ||
}); | ||
// please note: order is undetermined since native javascript hashes | ||
// are not sorted. please perform a sort() operation after construction | ||
// if you want to rely on item ordering. | ||
ok(countries.get('at') === 'Austria'); | ||
ok(countries.get('de') === 'Germany'); | ||
ok(countries.get("ch") === "Switzerland"); | ||
ok(countries.length === 3); | ||
}); | ||
suite('Data.Graph', function() { | ||
var graph, | ||
documentType, | ||
entitiesProperty, | ||
protovis, | ||
unveil, | ||
processingjs, | ||
mention, | ||
stanford, | ||
newYork, | ||
anotherMention; | ||
setup(function() { | ||
graph = new Data.Graph(documents_fixture); | ||
protovis = graph.get('/doc/protovis_introduction'); | ||
unveil = graph.get('/doc/unveil_introduction'); | ||
processingjs = graph.get('/doc/processing_js_introduction'); | ||
mention = graph.get('M0000003'); | ||
anotherMention = graph.get('M0000003'); | ||
stanford = graph.get('/location/stanford'); | ||
newYork = graph.get('/location/new_york'); | ||
}); | ||
test("insertion", function() { | ||
items.set("ch", "Switzerland"); | ||
ok(items.length === 3); | ||
}); | ||
test("valid construction", function() { | ||
assert.ok(graph != undefined); | ||
assert.ok(graph.types.length == 3); | ||
test("value overwrite", function() { | ||
items.get("at") === "Austria"; | ||
items.set("at", "Österreich"); | ||
ok(items.length === 2); | ||
ok(items.get("at") === "Österreich"); | ||
}); | ||
assert.ok(graph.types[0] instanceof Data.Type); | ||
assert.ok(graph.types[1] instanceof Data.Type); | ||
assert.ok(graph.types[2] instanceof Data.Type); | ||
}); | ||
test("clone", function() { | ||
// TODO: add some assertions | ||
items.clone(); | ||
}); | ||
test("key", function() { | ||
ok(items.key(0) === "at"); | ||
ok(items.key(1) === "de"); | ||
}); | ||
test("Type inspection", function() { | ||
documentType = graph.get('/type/document'); | ||
assert.ok(Object.keys(documentType.properties).length === 5); | ||
assert.ok(documentType._id === '/type/document'); | ||
assert.ok(documentType.name === 'Document'); | ||
}); | ||
test("Property inspection", function() { | ||
documentType = graph.get('/type/document'); | ||
entitiesProperty = documentType.properties.entities; | ||
assert.ok(entitiesProperty.name === 'Associated Entities'); | ||
assert.ok(_.include(entitiesProperty.type, '/type/entity')); | ||
}); | ||
test("hash semantics", function() { | ||
var keys = []; | ||
var values = []; | ||
ok(items.get("at") === "Austria"); | ||
ok(items.get("de") === "Germany"); | ||
// order is also reflected in eachKey | ||
items.each(function(item, key) { | ||
keys.push(key); | ||
values.push(item); | ||
}); | ||
ok(keys.length === 2 && values.length === 2); | ||
ok(keys[0] === 'at'); | ||
ok(keys[1] === 'de'); | ||
ok(values[0] === 'Austria'); | ||
ok(values[1] === 'Germany'); | ||
}); | ||
test("array semantics", function() { | ||
var values = []; | ||
items.get(0) === "Austria"; | ||
items.get(1) === "Germany"; | ||
items.length === 1; | ||
items.each(function(item, key, index) { | ||
values.push(item); | ||
}); | ||
ok(values.length === 2); | ||
ok(values[0] === 'Austria'); | ||
ok(values[1] === 'Germany'); | ||
ok(items.first() === "Austria"); | ||
ok(items.last() === "Germany"); | ||
}); | ||
test("Object inspection", function() { | ||
assert.ok(protovis instanceof Data.Object); | ||
assert.ok(mention instanceof Data.Object); | ||
assert.ok(anotherMention instanceof Data.Object); | ||
}); | ||
test("Data.Hash#del", function() { | ||
items.set("ch", "Switzerland"); | ||
items.del('de'); | ||
ok(items.length === 2); | ||
ok(items.keyOrder.length === 2); | ||
ok(items.get('de') === undefined); | ||
}); | ||
// There are four different access scenarios: | ||
// 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 Set of Node objects, even if it's a unique property. So if | ||
// you want to be explicit you should use the native methods of the Node API. | ||
test("1. Unique value types", function() { | ||
assert.ok(protovis.get('page_count') === 8); | ||
assert.ok(protovis.get('title') === 'Protovis'); | ||
}); | ||
test("Data.Hash#each", function() { | ||
var enumerated = []; | ||
items.each(function(item, key, index) { | ||
enumerated.push(item); | ||
}); | ||
ok(enumerated[0]==="Austria"); | ||
ok(enumerated[1]==="Germany"); | ||
}); | ||
test("Data.Hash#values", function() { | ||
items.set("ch", "Switzerland"); | ||
var values = items.values(); | ||
test("2. Non-Unique value types", function() { | ||
assert.ok(protovis.get('authors').length === 2); | ||
assert.ok(protovis.get('authors')[0] === 'Michael Bostock'); | ||
assert.ok(protovis.get('authors')[1] === 'Jeffrey Heer'); | ||
}); | ||
ok(values[0] === "Austria"); | ||
ok(values[1] === "Germany"); | ||
ok(values[2] === "Switzerland"); | ||
}); | ||
test("Data.Hash#rest", function() { | ||
items.set("ch", "Switzerland"); | ||
var rest = items.rest(1); | ||
ok(rest.get('de') === 'Germany'); | ||
ok(rest.get('ch') === 'Switzerland'); | ||
ok(rest.length === 2); | ||
}); | ||
test("3. Unique object types (one resource)", function() { | ||
assert.ok(mention.get('entity')._id === '/location/new_york'); | ||
}); | ||
test("Data.Hash#range", function() { | ||
items.set("ch", "Switzerland"); | ||
items.set("uk", "United Kingdom"); | ||
var range = items.range(0,1); | ||
ok(range.length == 2); | ||
ok(range.at(0) === "Austria"); | ||
ok(range.at(1) === "Germany"); | ||
}); | ||
test("4. Non-unique object types (many resources)", function() { | ||
assert.ok(protovis.get('entities').length === 2); | ||
assert.ok(protovis.get('entities')[0]._id === '/location/stanford'); | ||
assert.ok(protovis.get('entities')[1]._id === '/location/new_york'); | ||
}); | ||
test("Data.Hash#sort", function() { | ||
items.set("ch", "Switzerland"); | ||
test("References to the same resource should result in object equality", function() { | ||
assert.ok(mention.get('entity') === anotherMention.get('entity')); | ||
}); | ||
ok(items.at(0)==="Austria"); | ||
ok(items.at(1)==="Germany"); | ||
ok(items.at(2)==="Switzerland"); | ||
// sort descending | ||
var sortedItems = items.sort(Data.Comparators.DESC); | ||
ok(sortedItems.at(0)==="Switzerland"); | ||
ok(sortedItems.at(1)==="Germany"); | ||
ok(sortedItems.at(2)==="Austria"); | ||
}); | ||
test("Graph traversal (navigation)", function() { | ||
// Hop from a document to the second entity, picking the 2nd mention and go | ||
// to the associated document of this mention. | ||
assert.ok(protovis.get('entities')[1] // => Entity#/location/new_york | ||
.get('mentions')[1] // => Mention#M0000003 | ||
.get('document') // => /doc/processing_js_introduction | ||
._id === '/doc/processing_js_introduction'); | ||
}); | ||
test("Data.Hash#map", function() { | ||
var mappedItems = items.map(function (item) { | ||
return item.slice(0, 3); | ||
}); | ||
// leave original Hash untouched | ||
ok(items.get('at') === 'Austria'); | ||
ok(items.get('de') === 'Germany'); | ||
ok(items.at(0) === 'Austria'); | ||
ok(items.at(1) === 'Germany'); | ||
ok(mappedItems.get('at') === 'Aus'); | ||
ok(mappedItems.get('de') === 'Ger'); | ||
// Data.Object Manipulation | ||
ok(mappedItems.at(0) === 'Aus'); | ||
ok(mappedItems.at(1) === 'Ger'); | ||
}); | ||
// Set new nodes on the graph | ||
test("Set new nodes on the graph", function() { | ||
var substance = graph.set({ | ||
"_id": "/document/substance", | ||
"type": "/type/document", | ||
"title": "Substance Introduction", | ||
"authors": ["Michael Aufreiter"], | ||
"page_count": 12, | ||
"entities": ["/location/stanford", "/location/new_york"] | ||
}); | ||
assert.ok(substance.get('title') === 'Substance Introduction'); | ||
assert.ok(substance.get('page_count') === 12); | ||
// Allow null being passed explicitly for an object type property | ||
var mention = graph.set({ | ||
"type": "/type/mention", | ||
"document": null, | ||
"entity": "/location/stanford", | ||
"page": 5 | ||
}); | ||
assert.ok(!mention.get('document')); | ||
}); | ||
test("Set value properties of existing nodes", function() { | ||
// Value properties | ||
protovis.set({ | ||
'title': 'Protovis Introduction', | ||
'page_count': 20 | ||
}); | ||
protovis.set({ | ||
'page_count': 20 | ||
}); | ||
test("Data.Hash#select", function() { | ||
var selectedItems = items.select(function (item, key) { | ||
return item === 'Austria'; | ||
assert.ok(protovis.get('title') === 'Protovis Introduction'); | ||
assert.ok(protovis.get('page_count') === 20); | ||
}); | ||
// leave original Set untouched | ||
ok(items.get('at') === 'Austria'); | ||
ok(items.get('de') === 'Germany'); | ||
ok(items.at(0) === 'Austria'); | ||
ok(items.at(1) === 'Germany'); | ||
ok(selectedItems.at(0) === 'Austria'); | ||
ok(selectedItems.get("at") === 'Austria'); | ||
ok(selectedItems.length === 1); | ||
}); | ||
test("Set value type objects", function() { | ||
protovis.set({ | ||
info: {"foo": "bar"} | ||
}); | ||
assert.ok(protovis.get('info').foo === "bar"); | ||
protovis.set({ | ||
info: {"bar": "baz"} | ||
}); | ||
assert.ok(protovis.get('info').bar === "baz"); | ||
}); | ||
test("Data.Hash#intersect", function() { | ||
var items2 = new Data.Hash(), | ||
intersected; | ||
items2.set('fr', 'France'); | ||
items2.set('at', 'Austria'); | ||
// leave original Setes untouched | ||
ok(items.get('at') === 'Austria'); | ||
ok(items.get('de') === 'Germany'); | ||
ok(items.at(0) === 'Austria'); | ||
ok(items.at(1) === 'Germany'); | ||
ok(items2.get('fr') === 'France'); | ||
ok(items2.get('at') === 'Austria'); | ||
ok(items2.at(0) === 'France'); | ||
ok(items2.at(1) === 'Austria'); | ||
intersected = items.intersect(items2); | ||
ok(intersected.length === 1); | ||
ok(intersected.get('at') === 'Austria'); | ||
}); | ||
test("Set object properties of existing nodes", function() { | ||
assert.ok(_.first(protovis.get('entities')).get('name') === 'Stanford'); | ||
assert.ok(_.last(protovis.get('entities')).get('name') === 'New York'); | ||
protovis.set({ | ||
entities: ['/location/toronto'], | ||
authors: 'Michael Aufreiter' | ||
}); | ||
assert.ok(_.first(protovis.get('entities')) === graph.get('/location/toronto')); | ||
}); | ||
test("Data.Hash#union", function() { | ||
var items2 = new Data.Hash(), | ||
unitedItems; | ||
items2.set('fr', 'France'); | ||
items2.set('at', 'Austria'); | ||
// leave original Setes untouched | ||
ok(items.get('at') === 'Austria'); | ||
ok(items.get('de') === 'Germany'); | ||
ok(items.at(0) === 'Austria'); | ||
ok(items.at(1) === 'Germany'); | ||
ok(items2.get('fr') === 'France'); | ||
ok(items2.get('at') === 'Austria'); | ||
ok(items2.at(0) === 'France'); | ||
ok(items2.at(1) === 'Austria'); | ||
unitedItems = items.union(items2); | ||
ok(unitedItems.length === 3); | ||
ok(unitedItems.get('at') === 'Austria'); | ||
ok(unitedItems.get('de') === 'Germany'); | ||
ok(unitedItems.get('fr') === 'France'); | ||
}); | ||
test("Find objects", function() { | ||
// By type | ||
var documents = graph.find({"type": "/type/document"}); | ||
assert.ok(documents.length === 3); | ||
test("Only consider own properties", function() { | ||
var hsh = new Data.Hash(); | ||
ok(hsh.get('toString') === undefined); | ||
ok(hsh.get('toLocaleString') === undefined); | ||
ok(hsh.get('watch') === undefined); | ||
ok(hsh.get('hasOwnProperty') === undefined); | ||
}); | ||
// By authors | ||
documents = graph.find({ | ||
"type": "/type/document", | ||
"authors": "Michael Aufreiter" | ||
}); | ||
assert.ok(documents.length === 2); | ||
}); | ||
}); | ||
suite('Data.Collection', function() { | ||
var c; | ||
setup(function() { | ||
c = new Data.Collection(countries_fixture); | ||
}); | ||
test("Data.Hash Events", function() { | ||
persons = new Data.Hash(); | ||
persons.bind('set', function(key) { ok(key); }); | ||
persons.bind('del', function(key) { ok(key); }); | ||
persons.set("mi", "Michael"); | ||
persons.set("th", "Thomas"); | ||
persons.del("mi"); | ||
expect(3); | ||
}); | ||
test("valid construction", function() { | ||
assert.ok(c != undefined); | ||
assert.ok(c.type instanceof Data.Type); | ||
}); | ||
test("each", function() { | ||
var count = 0; | ||
c.each(function(obj, key, index) { | ||
assert.ok(count === index); | ||
count ++; | ||
}); | ||
assert.ok(count === c.length); | ||
}); | ||
test("fail prevention", function() { | ||
// null is a valid key | ||
items.set(null, 'Netherlands'); | ||
// undefined is not | ||
items.set(undefined, 'Netherlands'); | ||
items.set('null_value', null); | ||
items.set('undefined_value', undefined); | ||
ok(items.length === 5); | ||
}); | ||
test("at", function() { | ||
assert.ok(c.at(6)._id === 'ca'); | ||
}); | ||
test("get", function() { | ||
assert.ok(c.get('at')._id === 'at'); | ||
}); | ||
// Data.Node | ||
// ------------- | ||
test("index", function() { | ||
assert.ok(c.index('at') === 0); | ||
}); | ||
var austrian, english, german, eu, austria, germany, uk; | ||
test("key", function() { | ||
assert.ok(c.key(0) === "at"); | ||
}); | ||
module("Data.Node", { | ||
setup: function() { | ||
austrian = new Data.Node({value: 'Austrian'}); | ||
english = new Data.Node({value: 'English'}); | ||
german = new Data.Node({value: 'German'}); | ||
// confederations | ||
eu = new Data.Node(); | ||
// countries | ||
austria = new Data.Node(); | ||
germany = new Data.Node(); | ||
uk = new Data.Node(); | ||
// people | ||
barroso = new Data.Node({value: 'Barroso'}); | ||
// connect some nodes | ||
austria.set('languages', 'at', austrian); | ||
austria.set('languages', 'ger', german); | ||
eu.set('president', 'barroso', barroso); | ||
} | ||
}); | ||
test("find objects", function() { | ||
assert.ok(c != undefined); | ||
assert.ok(c.type instanceof Data.Type); | ||
test("get connected nodes", function() { | ||
// order should be preserved | ||
ok(austria.all('languages') instanceof Data.Hash); | ||
ok(austria.all('languages').at(0) === austrian); | ||
ok(austria.all('languages').at(1) === german); | ||
ok(austria.get('languages', 'at') === austrian); | ||
ok(austria.get('languages', 'ger') === german); | ||
}); | ||
var countries = c.find({"name": "Austria"}); | ||
assert.ok(countries.length === 1); | ||
}); | ||
}); | ||
test("get first connected node", function() { | ||
ok(eu.first('president') instanceof Data.Node); | ||
ok(eu.first('president').val === 'Barroso'); | ||
}); | ||
test("iteration of connected nodes", function() { | ||
var nodes = []; | ||
austria.all('languages').each(function(node) { | ||
nodes.push(node); | ||
}); | ||
ok(nodes.length === 2); | ||
}); | ||
test("Node#list", function() { | ||
// non-unqiue property | ||
ok(austria.all('languages').length === 2); | ||
ok(austria.all('languages').get('at') === austrian); | ||
ok(austria.all('languages').get('ger') === german); | ||
// unique property | ||
ok(eu.all('president').length === 1); | ||
ok(eu.values('president').first() === 'Barroso'); | ||
}); | ||
test("Node#values", function() { | ||
var values = austria.values('languages'); | ||
// for non-unique properties | ||
ok(values.at(0) === 'Austrian'); | ||
ok(values.at(1) === 'German'); | ||
ok(values.get('at') === 'Austrian'); | ||
ok(values.get('ger') === 'German'); | ||
// for unique properties | ||
ok(eu.values('president').at(0) === 'Barroso'); | ||
}); | ||
test("Node#value", function() { | ||
var values = austria.values('languages'); | ||
// for non-unique properties | ||
ok(austria.value('languages') === 'Austrian'); | ||
// for unique properties | ||
ok(eu.value('president') === 'Barroso'); | ||
}); | ||
test("Allows null as a key for property values", function() { | ||
var root = new Data.Node({value: 'RootNode'}); | ||
var nullNode = new Data.Node({value: null}); | ||
root.set('values', null, nullNode); | ||
ok(root.value('values', null) === null); | ||
}); | ||
// Data.Graph | ||
// ------------- | ||
var graph, | ||
documentType, | ||
entitiesProperty, | ||
protovis, | ||
unveil, | ||
processingjs, | ||
mention, | ||
stanford, | ||
newYork, | ||
anotherMention; | ||
graph = new Data.Graph(documents_fixture); | ||
protovis = graph.get('objects', '/doc/protovis_introduction'); | ||
unveil = graph.get('objects', '/doc/unveil_introduction'); | ||
processingjs = graph.get('objects', '/doc/processing_js_introduction'); | ||
mention = graph.get('objects', 'M0000003'); | ||
anotherMention = graph.get('objects', 'M0000003'); | ||
stanford = graph.get('/location/stanford'); | ||
newYork = graph.get('/location/new_york'); | ||
module("Data.Graph", { | ||
setup: function() { | ||
// graph = new Data.Graph(documents_fixture); | ||
}, | ||
teardown: function() { | ||
// delete graph; | ||
} | ||
}); | ||
test("valid construction", function() { | ||
ok(graph != undefined); | ||
ok(graph.types().length == 3); | ||
ok(graph.types().get('/type/document') instanceof Data.Type); | ||
ok(graph.types().get('/type/entity') instanceof Data.Type); | ||
ok(graph.types().get('/type/mention') instanceof Data.Type); | ||
}); | ||
test("Type inspection", function() { | ||
documentType = graph.get('objects', '/type/document'); | ||
ok(documentType.all('properties').length === 5); | ||
ok(documentType.key === '/type/document'); | ||
ok(documentType.name === 'Document'); | ||
}); | ||
test("Property inspection", function() { | ||
entitiesProperty = documentType.get('properties', 'entities'); | ||
ok(entitiesProperty.name === 'Associated Entities'); | ||
ok(_.include(entitiesProperty.expectedTypes, '/type/entity')); | ||
}); | ||
test("Object inspection", function() { | ||
ok(protovis instanceof Data.Object); | ||
ok(mention instanceof Data.Object); | ||
ok(anotherMention instanceof Data.Object); | ||
}); | ||
// There are four different access scenarios: | ||
// 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 Set of Node objects, even if it's a unique property. So if | ||
// you want to be explicit you should use the native methods of the Node API. | ||
test("1. Unique value types", function() { | ||
ok(protovis.get('page_count') === 8); | ||
ok(protovis.get('title') === 'Protovis'); | ||
// internally delegates to | ||
ok(protovis.get('page_count') === 8); | ||
}); | ||
test("2. Non-Unique value types", function() { | ||
ok(protovis.get('authors').length === 2); | ||
ok(protovis.get('authors').at(0) === 'Michael Bostock'); | ||
ok(protovis.get('authors').at(1) === 'Jeffrey Heer'); | ||
// internally delegates to | ||
ok(protovis.values('authors').length === 2); | ||
}); | ||
test("3. Unique object types (one resource)", function() { | ||
ok(mention.get('entity').key === '/location/new_york'); | ||
// internally delegates to | ||
ok(mention.first('entity').key === '/location/new_york'); | ||
}); | ||
test("4. Non-unique object types (many resources)", function() { | ||
ok(protovis.get('entities').length === 2); | ||
ok(protovis.get('entities').at(0).key === '/location/stanford'); | ||
ok(protovis.get('entities').at(1).key === '/location/new_york'); | ||
// internally delegates to | ||
ok(protovis.all('entities').length === 2); | ||
}); | ||
test("References to the same resource should result in object equality", function() { | ||
ok(mention.first('entity') === anotherMention.first('entity')); | ||
}); | ||
test("Graph traversal (navigation)", function() { | ||
// Hop from a document to the second entity, picking the 2nd mention and go | ||
// to the associated document of this mention. | ||
ok(protovis.get('entities').at(1) // => Entity#/location/new_york | ||
.get('mentions').at(1) // => Mention#M0000003 | ||
.get('document') // => /doc/processing_js_introduction | ||
.key === '/doc/processing_js_introduction'); | ||
}); | ||
test("Querying information", function() { | ||
var cities = graph.all('objects').select(function(res, key) { | ||
return /or/.test(res.get('name')) | ||
}); | ||
ok(cities.length === 3); | ||
ok(cities.get('/location/new_york')); | ||
ok(cities.get('/location/toronto')); | ||
ok(cities.get('/location/stanford')); | ||
}); | ||
test("Value identity", function() { | ||
// If the values of a property are shared among resources they should have | ||
// the same identity as well. | ||
ok(unveil.all('authors').at(0) === processingjs.all('authors').at(2)); | ||
ok(unveil.get('authors').at(0) === 'Michael Aufreiter'); | ||
ok(processingjs.get('authors').at(2) === 'Michael Aufreiter'); | ||
// This allows questions like: | ||
// Show all unique values of a certain property e.g. /type/document.authors | ||
ok(protovis.type.get('properties', 'authors').all('values').length === 6); | ||
}); | ||
// Data.Object Manipulation | ||
// Set new nodes on the graph | ||
test("Set new nodes on the graph", function() { | ||
var substance = graph.set({ | ||
"_id": "/document/substance", | ||
"type": "/type/document", | ||
"title": "Substance Introduction", | ||
"authors": ["Michael Aufreiter"], | ||
"page_count": 12, | ||
"entities": ["/location/stanford", "/location/new_york"] | ||
}); | ||
ok(substance.get('title') === 'Substance Introduction'); | ||
ok(substance.get('page_count') === 12); | ||
}); | ||
test("Set value properties of existing nodes", function() { | ||
// Value properties | ||
protovis.set({ | ||
'title': 'Protovis Introduction', | ||
'page_count': 20 | ||
}); | ||
protovis.set({ | ||
'page_count': 20 | ||
}); | ||
ok(protovis.get('title') === 'Protovis Introduction'); | ||
ok(protovis.get('page_count') === 20); | ||
}); | ||
test("Set value type objects", function() { | ||
protovis.set({ | ||
info: {"foo": "bar"} | ||
}); | ||
ok(protovis.get('info').foo === "bar"); | ||
protovis.set({ | ||
info: {"bar": "baz"} | ||
}); | ||
ok(protovis.get('info').bar === "baz"); | ||
}); | ||
test("Set object properties of existing nodes", function() { | ||
ok(protovis.get('entities').first().get('name') === 'Stanford'); | ||
ok(protovis.get('entities').last().get('name') === 'New York'); | ||
ok(protovis.type.get('properties', 'entities').get('values', '/location/new_york') === newYork); | ||
ok(protovis.type.get('properties', 'entities').get('values', '/location/stanford') === stanford); | ||
ok(protovis.type.get('properties', 'entities').all('values').length === 2); | ||
ok(protovis.type.get('properties', 'authors').get('values', 'Michael Bostock')); | ||
ok(protovis.type.get('properties', 'authors').get('values', 'Jeffrey Heer')); | ||
protovis.set({ | ||
entities: ['/location/toronto'], | ||
authors: 'Michael Aufreiter' | ||
}); | ||
ok(protovis.get('entities').first() === graph.get('/location/toronto')); | ||
ok(protovis.type.get('properties', 'entities').get('values', '/location/toronto')); | ||
}); | ||
test("Proper value registration / deregistration", function() { | ||
var documentType = graph.types().get('/type/document'); | ||
var values = documentType.properties().get('title').all('values'); | ||
var valueCount = values.length; | ||
unveil.set({ | ||
title: "Unveil Introduction" | ||
}); | ||
ok(values.keys().length === valueCount); | ||
var valueCount = values.length; | ||
// Overwrite existing object | ||
graph.set("/doc/unveil_introduction", { | ||
"type": "/type/document", | ||
"title": "Unveil.js Introduction", | ||
"authors": ["Michael Aufreiter", "Lindsay Kay"], | ||
"page_count": 8, | ||
"entities": [] | ||
}); | ||
ok(values.keys().length === valueCount); | ||
// Proper value unregistration of nodes being deleted | ||
graph.del("/doc/unveil_introduction"); | ||
var type = graph.get('/type/document'); | ||
ok(!type.properties().get("title").all('values').get("Unveil.js Introduction")); | ||
ok(!type.properties().get("authors").all('values').get("Lindsay Kay")); | ||
}); | ||
// Data.Collection | ||
// ------------- | ||
module("Data.Collection"); | ||
var c = new Data.Collection(countries_fixture); | ||
test("has some properties", function() { | ||
ok(c.properties().get('area') instanceof Data.Node); | ||
ok(c.properties().get('currency_used') instanceof Data.Node); | ||
ok(c.properties().get('doesnotexit') === undefined); | ||
}); | ||
test("property is connected to values", function() { | ||
var governmentForms = c.properties().get('form_of_government'); | ||
ok(governmentForms.all('values').length === 10); | ||
}); | ||
test("read item property values", function() { | ||
var item = c.get('austria'); | ||
// Unique properties | ||
ok(item.get('name') === 'Austria'); | ||
ok(item.get('area') === 83872); | ||
// Non-unique properties | ||
ok(item.get('form_of_government').length === 2); | ||
}); | ||
test("get values of a property", function() { | ||
var population = c.properties().get('population'); | ||
ok(population.all('values').length === 6); | ||
}); | ||
test("grouping", function() { | ||
var languages = c.group(["official_language"], { | ||
'area_total': { aggregator: Data.Aggregators.SUM, name: "Total Area", property: "area" }, | ||
'area_avg': {aggregator: Data.Aggregators.AVG, name: "Average Area", property: "area" }, | ||
'population': { aggregator: Data.Aggregators.AVG, name: "Average Population" } | ||
}); | ||
ok(languages.items().get('German Language').get('population') === 45209450); | ||
ok(languages.items().get('English Language').get('area_total') === 10071495); | ||
// Deal with empty group key | ||
var onegroup = c.group([], { | ||
'area_total': { aggregator: Data.Aggregators.SUM, name: "Total Area", property: "area" }, | ||
'area_avg': {aggregator: Data.Aggregators.AVG, name: "Average Area", property: "area" }, | ||
'population': { aggregator: Data.Aggregators.MIN, name: "Smallest Population" } | ||
}); | ||
ok(onegroup.items().first().get('population') === 8356700) | ||
}); | ||
test("Collection#find", function() { | ||
// Test ANY-OF operator | ||
var englishAndGermanCountries = c.find({ | ||
"official_language|=": ["English Language", "German Language"] | ||
}); | ||
ok(englishAndGermanCountries.get('austria')); | ||
ok(englishAndGermanCountries.get('ger')); | ||
ok(englishAndGermanCountries.get('uk')); | ||
ok(englishAndGermanCountries.get('usa')); | ||
// Test ALL-OF operator | ||
var republicsAndDemocracies = c.find({ | ||
"official_language": "English Language", | ||
"form_of_government&=": ["Constitution", "Democracy"] | ||
}); | ||
// Test >= operator | ||
var bigCountries = c.find({ | ||
"area>=": 700000 | ||
}); | ||
ok(bigCountries.length === 1); | ||
}); | ||
test("Collection#filter", function() { | ||
var filteredCollection = c.filter({ | ||
"official_language|=": ["German Language"] | ||
}); | ||
ok(filteredCollection.items().length === 2); | ||
}); | ||
module("Data.Aggregators"); | ||
// Data.Aggregators | ||
// ------------- | ||
test("Aggregators", function() { | ||
var values = new Data.Hash(); | ||
values.set('0', 4); | ||
values.set('1', 5); | ||
values.set('2', -3); | ||
values.set('3', 1); | ||
ok(Data.Aggregators.SUM(values) === 7); | ||
ok(Data.Aggregators.MIN(values) === -3); | ||
ok(Data.Aggregators.MAX(values) === 5); | ||
ok(Data.Aggregators.COUNT(values) === 4); | ||
}); | ||
test("allow aggregation of property values", function() { | ||
var population = c.properties().get("population"); | ||
ok(population.aggregate(Data.Aggregators.MIN) === 8356700); | ||
ok(population.aggregate(Data.Aggregators.MAX) === 306108000); | ||
}); | ||
}).call(this); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Non-existent author
Supply chain riskThe package was published by an npm account that no longer exists.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
1
97
0
4
4435732
21
6768
12
- Removed1@now