├── .travis.yml ├── lib ├── seed │ ├── base │ │ ├── async.js │ │ ├── query.js │ │ └── events.js │ ├── graph │ │ ├── commands │ │ │ ├── inEdges.js │ │ │ ├── outEdges.js │ │ │ ├── inVertices.js │ │ │ ├── outVertices.js │ │ │ ├── select.js │ │ │ ├── index.js │ │ │ └── base │ │ │ │ ├── edges.js │ │ │ │ └── vertices.js │ │ ├── edge │ │ │ ├── schema.js │ │ │ └── model.js │ │ ├── traversal.js │ │ └── helpers.js │ ├── errors │ │ ├── store.js │ │ ├── graph.js │ │ └── model.js │ ├── schematypes │ │ ├── objectid.js │ │ ├── object.js │ │ ├── select.js │ │ ├── array.js │ │ ├── index.js │ │ ├── boolean.js │ │ ├── number.js │ │ ├── string.js │ │ ├── email.js │ │ ├── embeddedSchema.js │ │ ├── dbref.js │ │ └── geospatial.js │ ├── utils │ │ ├── flake.js │ │ ├── index.js │ │ ├── object.js │ │ └── crystal.js │ ├── store.js │ ├── stores │ │ └── memory.js │ ├── schema.js │ ├── model.js │ └── graph.js └── seed.js ├── .gitignore ├── index.js ├── .npmignore ├── examples ├── error.js ├── objectid.js ├── schema.js └── relations.js ├── benchmarks ├── hash.js ├── schema.js ├── memorystore.js ├── objectId.js └── common.utils.js ├── test ├── schema.typeEmail.js ├── store.core.js ├── bootstrap │ └── index.js ├── schema.typeSelect.js ├── exports.js ├── schema.typeGeospatial.js ├── graph.edge.js ├── utils.object.js ├── schema.typeDBRef.js ├── schema.typeEmbeddedSchema.js ├── schema.typeNative.js ├── schema.core.js ├── fixtures │ └── countries.json ├── model.js ├── store.memory.js ├── graph.core.js ├── hash.js └── graph.traversal.js ├── package.json ├── README.md └── History.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.11 4 | -------------------------------------------------------------------------------- /lib/seed/base/async.js: -------------------------------------------------------------------------------- 1 | ;module.exports = require('breeze'); 2 | -------------------------------------------------------------------------------- /lib/seed/base/query.js: -------------------------------------------------------------------------------- 1 | module.exports = require('filtr'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib-cov/ 2 | /.settings.xml 3 | /node_modules/ 4 | *.swp 5 | coverage.html 6 | lib-cov 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.SEED_COV 2 | ? require('./lib-cov/seed') 3 | : require('./lib/seed'); 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | support/ 3 | test/ 4 | examples/ 5 | benchmarks/ 6 | .DS_Store 7 | coverage.html 8 | lib-cov 9 | .travis.yml 10 | Makefile 11 | .mailmap 12 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/inEdges.js: -------------------------------------------------------------------------------- 1 | var base = require('./base/edges') 2 | , inherits = require('super'); 3 | 4 | module.exports = inEdges; 5 | 6 | function inEdges () { 7 | base.apply(this,arguments); 8 | this.direction = 'x'; 9 | } 10 | 11 | inherits(inEdges, base); 12 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/outEdges.js: -------------------------------------------------------------------------------- 1 | var base = require('./base/edges') 2 | , inherits = require('super'); 3 | 4 | module.exports = outEdges; 5 | 6 | function outEdges () { 7 | base.apply(this, arguments); 8 | this.direction = 'y'; 9 | } 10 | 11 | inherits(outEdges, base); 12 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/inVertices.js: -------------------------------------------------------------------------------- 1 | var base = require('./base/vertices') 2 | , inherits = require('super'); 3 | 4 | module.exports = inVertices; 5 | 6 | function inVertices () { 7 | base.apply(this, arguments); 8 | this.direction = 'x'; 9 | } 10 | 11 | inherits(inVertices, base); 12 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/outVertices.js: -------------------------------------------------------------------------------- 1 | var base = require('./base/vertices') 2 | , inherits = require('super'); 3 | 4 | module.exports = outVertices; 5 | 6 | function outVertices () { 7 | base.apply(this, arguments); 8 | this.direction = 'y'; 9 | } 10 | 11 | inherits(outVertices, base); 12 | -------------------------------------------------------------------------------- /examples/error.js: -------------------------------------------------------------------------------- 1 | var CustomError = require('..').SeedError; 2 | 3 | function log (err) { 4 | console.error(err); 5 | } 6 | 7 | function a (){ 8 | b(); 9 | } 10 | 11 | function b () { 12 | var err = new CustomError('There was a problem doing something in your app.'); 13 | log(err.toJSON()); 14 | throw err; 15 | } 16 | 17 | a(); -------------------------------------------------------------------------------- /lib/seed/errors/store.js: -------------------------------------------------------------------------------- 1 | 2 | var exports = module.exports = require('dragonfly')('SeedError'); 3 | 4 | exports.register('not found', { 5 | message: 'Record not found in storage' 6 | , code: 'ENOTFOUND' 7 | , ctx: 'Store' 8 | }); 9 | 10 | exports.register('no id', { 11 | message: 'Data missing Id' 12 | , code: 'EBADDATA' 13 | , code: 'Store' 14 | }); 15 | -------------------------------------------------------------------------------- /lib/seed/graph/edge/schema.js: -------------------------------------------------------------------------------- 1 | var Schema = require('../../schema'); 2 | 3 | module.exports = new Schema({ 4 | 5 | x: { 6 | type: Schema.Type.DBRef 7 | , required: true 8 | } 9 | 10 | , y: { 11 | type: Schema.Type.DBRef 12 | , required: true 13 | } 14 | 15 | , rel: { 16 | type: String 17 | , required: true 18 | } 19 | 20 | , attributes: { 21 | type: Object 22 | } 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /lib/seed/schematypes/objectid.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ObjectId; 3 | 4 | function ObjectId (path, value) { 5 | this._path = path; 6 | this._value = value; 7 | } 8 | 9 | ObjectId.prototype.validate = function () { 10 | var type = typeof this._value; 11 | return this._value && ('string' === type || 'number' === type); 12 | }; 13 | 14 | ObjectId.prototype.getValue = function () { 15 | if (!this.validate()) return undefined; 16 | return this._value; 17 | }; 18 | -------------------------------------------------------------------------------- /benchmarks/hash.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..'); 2 | 3 | suite('Hash', function () { 4 | set('iterations', 100); 5 | 6 | var hash = new Seed.Hash(); 7 | 8 | var seti = 0; 9 | bench('#set', function () { 10 | hash.set(++seti, 'hello', true); 11 | }); 12 | 13 | var geti = 0; 14 | bench('#get', function () { 15 | var val = hash.get(++geti); 16 | }); 17 | 18 | var deli = 0; 19 | bench('#del', function () { 20 | hash.del(++deli, true); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /lib/seed/schematypes/object.js: -------------------------------------------------------------------------------- 1 | module.exports = TypeObject; 2 | 3 | function TypeObject (path, value) { 4 | this.name = 'Object'; 5 | this._path = path; 6 | this._value = value; 7 | return this; 8 | } 9 | 10 | TypeObject.prototype.validate = function () { 11 | return this._value 12 | && 'object' === typeof this._value 13 | && !Array.isArray(this._value); 14 | }; 15 | 16 | TypeObject.prototype.getValue = function () { 17 | if (!this.validate()) return undefined; 18 | return this._value; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/seed/errors/graph.js: -------------------------------------------------------------------------------- 1 | 2 | var exports = module.exports = require('dragonfly')('SeedError'); 3 | 4 | exports.register('no store', { 5 | message: 'No storage defined.' 6 | , code: 'ENOSTORE' 7 | , ctx: 'Graph' 8 | }); 9 | 10 | exports.register('no type', { 11 | message: 'Missing model type constructor.' 12 | , code: 'ENOMODELDEF' 13 | , ctx: 'Graph' 14 | }); 15 | 16 | exports.register('no id', { 17 | message: 'Missing parameter for Graph set.' 18 | , code: 'ENOID' 19 | , ctx: 'Graph' 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /lib/seed/schematypes/select.js: -------------------------------------------------------------------------------- 1 | var inherits = require('super') 2 | , SeedString = require('./string'); 3 | 4 | module.exports = Select; 5 | 6 | function Select (path, value) { 7 | this._path = path; 8 | this._value = value; 9 | return this; 10 | } 11 | 12 | inherits(Select, SeedString); 13 | 14 | Select.prototype.validate = function () { 15 | return this._path.allowed.indexOf(this._value) != -1; 16 | }; 17 | 18 | Select.prototype.getValue = function () { 19 | if (!this.validate()) return undefined; 20 | return this._value; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/select.js: -------------------------------------------------------------------------------- 1 | 2 | var Hash = require('../../base/hash') 3 | , Promise = require('../../base/promise'); 4 | 5 | module.exports = Select; 6 | 7 | function Select (traversal, model) { 8 | this.traversal = traversal; 9 | this.model = model; 10 | } 11 | 12 | Select.prototype.exec = function (model) { 13 | model = model || this.model; 14 | var defer = new Promise() 15 | , hash = new Hash(null, { findRoot: '_attributes' }) 16 | , key = '/' + model.type + '/' + model.id; 17 | hash.set(key, model) 18 | defer.resolve(hash); 19 | return defer.promise; 20 | } 21 | -------------------------------------------------------------------------------- /lib/seed/schematypes/array.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = TypeArray; 4 | 5 | function TypeArray (path, value) { 6 | var array = new Array(value); 7 | array.__proto__ = TypeArray.prototype; 8 | array._path = path; 9 | array._value = value; 10 | return array; 11 | } 12 | 13 | TypeArray.prototype = new Array(); 14 | 15 | TypeArray.prototype.validate = function () { 16 | return this._value && Array.isArray(this._value); 17 | }; 18 | 19 | TypeArray.prototype.getValue = function () { 20 | if (!this.validate()) return undefined; 21 | return this._value; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/objectid.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..') 2 | 3 | console.log('\nFLAKE'); 4 | var Flake = new Seed.Flake() 5 | for (var i = 0; i < 10; i ++) { 6 | var id = Flake.gen(); 7 | console.log(id); 8 | } 9 | 10 | console.log('\nCRYSTAL'); 11 | var Crystal = new Seed.Crystal(); 12 | for (var i = 0; i < 10; i++) { 13 | var id = Crystal.gen(); 14 | console.log(id); 15 | } 16 | 17 | console.log('\nCRYSTAL SHORT'); 18 | var ShortCrystal = new Seed.Crystal({ bits: 16, base: 36 }); 19 | for (var i = 0; i < 10; i++) { 20 | var id = ShortCrystal.gen(); 21 | console.log(id); 22 | } 23 | 24 | console.log(''); 25 | -------------------------------------------------------------------------------- /lib/seed/schematypes/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * native type support 4 | */ 5 | 6 | exports.Boolean = require('./boolean'); 7 | exports.Number = require('./number'); 8 | exports.String = require('./string'); 9 | exports.Array = require('./array'); 10 | exports.Object = require('./object'); 11 | 12 | /** 13 | * Seed specials 14 | */ 15 | 16 | exports.ObjectId = require('./objectid'); 17 | exports.Select = require('./select'); 18 | exports.Email = require('./email'); 19 | exports.Geospatial = require('./geospatial'); 20 | exports.EmbeddedSchema = require('./embeddedSchema'); 21 | exports.DBRef = require('./dbref'); 22 | -------------------------------------------------------------------------------- /lib/seed/schematypes/boolean.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = TypeBoolean; 4 | 5 | function TypeBoolean (path, value) { 6 | var boolean = new Boolean(value); 7 | boolean.__proto__ = TypeBoolean.prototype; 8 | boolean._path = path; 9 | boolean._value = value; 10 | return boolean; 11 | } 12 | 13 | TypeBoolean.prototype = new Boolean(); 14 | 15 | TypeBoolean.prototype.validate = function () { 16 | return 'boolean' === typeof this._value; 17 | }; 18 | 19 | TypeBoolean.prototype.getValue = function () { 20 | if (!this.validate()) return undefined; 21 | return this._value; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/seed/schematypes/number.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = TypeNumber; 4 | 5 | function TypeNumber (path, value) { 6 | var number = new Number(value); 7 | number.__proto__ = TypeNumber.prototype; 8 | number._path = path; 9 | number._value = value; 10 | return number; 11 | } 12 | 13 | TypeNumber.prototype = new Number(); 14 | 15 | TypeNumber.prototype.validate = function () { 16 | return this._value && 'number' === typeof this._value; 17 | }; 18 | 19 | TypeNumber.prototype.getValue = function () { 20 | if (!this.validate()) return undefined; 21 | return this._value; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/seed/schematypes/string.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = TypeString; 4 | 5 | function TypeString (path, value) { 6 | var string = new String(value); 7 | string.__proto__ = TypeString.prototype; 8 | string._path = path; 9 | string._value = value; 10 | return string; 11 | } 12 | 13 | TypeString.prototype = new String(); 14 | 15 | TypeString.prototype.validate = function () { 16 | return this._value && 'string' === typeof this._value; 17 | }; 18 | 19 | TypeString.prototype.getValue = function () { 20 | if (!this.validate()) return undefined; 21 | return this._value; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/seed/base/events.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: EventEmitter 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var Drip = require('drip') 12 | , inherits = require('super'); 13 | 14 | /*! 15 | * main export 16 | */ 17 | 18 | module.exports = EventEmitter; 19 | 20 | /** 21 | * # EventEmitter (constructor) 22 | * 23 | * Provide a consistent Drip implemntation 24 | * accross all components. 25 | */ 26 | 27 | function EventEmitter() { 28 | Drip.call(this, { delimeter: ':' }); 29 | } 30 | 31 | inherits(EventEmitter, Drip); 32 | -------------------------------------------------------------------------------- /test/schema.typeEmail.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('Email', function () { 5 | var s = new Schema({ 6 | email: Schema.Type.Email 7 | }); 8 | 9 | it('should validate with a proper email', function () { 10 | s.validate({ _id: 'test', email: 'jake@alogicalparadox.com' }).should.be.ok; 11 | }); 12 | 13 | it('should not validate with a bad email', function () { 14 | s.validate({ _id: 'test', email: '@jakeluer' }).should.not.be.ok; 15 | s.validate({ _id: 'test', email: 'hello world' }).should.not.be.ok; 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /lib/seed/graph/edge/model.js: -------------------------------------------------------------------------------- 1 | var async = require('../../base/async') 2 | , Model = require('../../model'); 3 | 4 | var EdgeSchema = require('./schema'); 5 | 6 | module.exports = Model.extend({ 7 | 8 | schema: EdgeSchema 9 | 10 | , load: function (cb) { 11 | async.parallel([ 12 | this.loadX.bind(this) 13 | , this.loadY.bind(this) 14 | ], function (err) { 15 | if (err) return cb(err); 16 | cb(null); 17 | }); 18 | } 19 | 20 | , loadX: function (cb) { 21 | this.loadRef('x', cb); 22 | } 23 | 24 | , loadY: function (cb) { 25 | this.loadRef('y', cb); 26 | } 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /lib/seed/schematypes/email.js: -------------------------------------------------------------------------------- 1 | var inherits = require('super') 2 | , SeedString = require('./string'); 3 | 4 | module.exports = Email; 5 | 6 | function Email (path, value) { 7 | this._path = path; 8 | this._value = value; 9 | return this; 10 | } 11 | 12 | inherits(Email, SeedString); 13 | 14 | Email.prototype.validate = function () { 15 | var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 16 | return re.test(this._value); 17 | }; 18 | 19 | Email.prototype.getValue = function () { 20 | if (!this.validate()) return undefined; 21 | return this._value; 22 | }; 23 | -------------------------------------------------------------------------------- /benchmarks/schema.js: -------------------------------------------------------------------------------- 1 | var Seed = require('../lib/seed'); 2 | 3 | var schema = new Seed.Schema({ 4 | string: String, 5 | number: Number, 6 | array: Array, 7 | nested: { 8 | string: String 9 | } 10 | }); 11 | 12 | var model = Seed.Model.extend({ 13 | schema: schema 14 | }); 15 | 16 | var data = { 17 | string: 'hello seed', 18 | number: 123456789, 19 | array: [ 4, [], {}, 'seed' ], 20 | nested: { 21 | string: 'hello universe' 22 | } 23 | } 24 | 25 | suite('Schema', function () { 26 | bench('Schema#validate', function () { 27 | var valid = schema.validate(data); 28 | }); 29 | 30 | bench('Model#constructor', function () { 31 | var m = new model(data); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /benchmarks/memorystore.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..'); 2 | 3 | suite('MemoryStore', function () { 4 | set('iterations', 50000); 5 | set('type', 'static'); 6 | 7 | var store = new Seed.MemoryStore(); 8 | var Person = Seed.Model.extend({ 9 | store: store 10 | }); 11 | 12 | var iset = 0; 13 | bench('Set', function (done) { 14 | var m = new Person({ _id: ++iset }); 15 | m.save(done); 16 | }); 17 | 18 | var iget = 0; 19 | bench('Get', function (done) { 20 | var m = new Person({ _id: ++iget }); 21 | m.fetch(done); 22 | }); 23 | 24 | var idel = 0; 25 | bench('Save/Destroy', function (done) { 26 | var m = new Person({ _id: ++idel }); 27 | m.destroy(done); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /benchmarks/objectId.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..'); 2 | 3 | suite('Common Utilities', function () { 4 | suite('Unique ID Generation', function () { 5 | var crystal = new Seed.Crystal() 6 | , flake = new Seed.Flake(); 7 | 8 | bench('crystal generate', function () { 9 | var _id = crystal.gen(); 10 | }); 11 | 12 | bench('flake generator', function () { 13 | var _id = flake.gen(); 14 | }); 15 | }); 16 | 17 | suite('Key/Value Flags', function () { 18 | var m = new Seed.Model({ name: 'Hello Universe' }); 19 | 20 | bench('flag string', function () { 21 | m.flag('test', true); 22 | }); 23 | 24 | bench('flag array', function () { 25 | m.flag([ 'test', 'two', 'three' ], true); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /benchmarks/common.utils.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..'); 2 | 3 | suite('Common Utilities', function () { 4 | suite('Unique ID Generation', function () { 5 | var crystal = new Seed.Crystal() 6 | , flake = new Seed.Flake(); 7 | 8 | bench('crystal generate', function () { 9 | var _id = crystal.gen(); 10 | }); 11 | 12 | bench('flake generator', function () { 13 | var _id = flake.gen(); 14 | }); 15 | }); 16 | 17 | suite('Key/Value Flags', function () { 18 | var m = new Seed.Model({ name: 'Hello Universe' }); 19 | 20 | bench('flag string', function () { 21 | m.flag('test', true); 22 | }); 23 | 24 | bench('flag array', function () { 25 | m.flag([ 'test', 'two', 'three' ], true); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/seed/utils/flake.js: -------------------------------------------------------------------------------- 1 | var exports = module.exports = {}; 2 | 3 | // based on twitter snowflake 4 | exports.Flake = function () { 5 | this.counter = 0; 6 | this.last = 0; 7 | }; 8 | 9 | exports.Flake.prototype.gen = function () { 10 | var protect = false 11 | , m = new Date().getTime() - 1262304000000; 12 | 13 | if (this.last >= m) { 14 | this.counter++; 15 | 16 | if (this.counter == 4095) { 17 | protect = true; 18 | this.counter = 0; 19 | setTimeout(function() { 20 | arguments.callee; 21 | }, 1); 22 | } 23 | } else { 24 | this.last = m; 25 | this.counter = 0; 26 | } 27 | 28 | if (protect === false) { 29 | m = m * Math.pow(2, 12); 30 | var uid = m + this.counter; 31 | return uid; 32 | } 33 | }; -------------------------------------------------------------------------------- /test/store.core.js: -------------------------------------------------------------------------------- 1 | describe('Store', function () { 2 | var Store = seed.Store; 3 | 4 | it('should have the proper drip settings', function () { 5 | var store = new Store(); 6 | store._drip.should.be.a('object'); 7 | store._drip.wildcard.should.be.true; 8 | store._drip.delimeter.should.equal(':'); 9 | store.emit.should.be.a('function'); 10 | }); 11 | 12 | it('should provide a functioning extend method', function () { 13 | Store.extend.should.be.a('function'); 14 | var NewStore = Store.extend({}); 15 | var store = new NewStore(); 16 | store.should.be.instanceof(Store); 17 | }); 18 | 19 | it('should provide a default sync options', function () { 20 | var store = new Store(); 21 | store.sync.should.be.a('function'); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /lib/seed/errors/model.js: -------------------------------------------------------------------------------- 1 | 2 | var exports = module.exports = require('dragonfly')('SeedError'); 3 | 4 | exports.register('no store', { 5 | message: 'No storage defined.' 6 | , code: 'ENOSTORE' 7 | , ctx: 'Model' 8 | }); 9 | 10 | exports.register('not valid', { 11 | message: 'Attributes did not validate.' 12 | , code: 'ENOTVALID' 13 | , ctx: 'Model' 14 | }); 15 | 16 | exports.register('no data', { 17 | message: 'Fetch returned returned no data.' 18 | , code: 'ENOTFOUND' 19 | , ctx: 'Model' 20 | }); 21 | 22 | exports.register('no dbref', { 23 | message: 'DBRef undefined.' 24 | , code: 'ENODBREF' 25 | , ctx: 'Model' 26 | }); 27 | 28 | exports.register('no type', { 29 | message: 'Missing model type constructor.' 30 | , code: 'ENOMODELDEF' 31 | , ctx: 'Model' 32 | }); 33 | -------------------------------------------------------------------------------- /test/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Attach chai to global should 3 | */ 4 | 5 | global.chai = require('chai'); 6 | global.should = global.chai.should(); 7 | 8 | /*! 9 | * Chai Plugins 10 | */ 11 | 12 | global.chai.use(require('chai-spies')); 13 | //global.chai.use(require('chai-http')); 14 | 15 | /*! 16 | * Import project 17 | */ 18 | 19 | global.seed = require('../..'); 20 | 21 | /*! 22 | * Helper to load internals for cov unit tests 23 | */ 24 | 25 | function req (name) { 26 | return process.env.seed_COV 27 | ? require('../../lib-cov/seed/' + name) 28 | : require('../../lib/seed/' + name); 29 | } 30 | 31 | /*! 32 | * Load unexposed modules for unit tests 33 | */ 34 | 35 | global.__seed = { 36 | 37 | graph: { 38 | Edge: req('graph/edge/model') 39 | , Traversal: req('graph/traversal') 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Utilities Loader 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var fs = require('fs') 12 | , path = require('path'); 13 | 14 | /*! 15 | * Local variables 16 | */ 17 | 18 | module.exports = {}; 19 | 20 | /*! 21 | * Get all utilities from `utils` folder and provide 22 | * access as a getter onto our exported variable. 23 | */ 24 | 25 | fs.readdirSync(__dirname).forEach(function(filename){ 26 | if (!/\.js$/.test(filename)) return; 27 | if (filename == 'index.js') return; 28 | var name = path.basename(filename, '.js'); 29 | Object.defineProperty(module.exports, name, 30 | { get: function () { 31 | return require('./' + name); 32 | } 33 | , enumerable: true 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/schema.typeSelect.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('Select', function () { 5 | var states = ['AL','AK','AZ','AR','CA','CO','CT','DE', 6 | 'FL','GA','HI','ID','IL','IN','IA','KS','KY','LA','ME','MD','MA','MI', 7 | 'MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','OH','OK', 8 | 'OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY' ]; 9 | 10 | var s = new Schema({ 11 | state: { 12 | type: Schema.Type.Select, 13 | allowed: states 14 | } 15 | }); 16 | 17 | it('should validate with a state', function () { 18 | s.validate({ _id: 'test', state: 'CA' }).should.be.ok; 19 | }); 20 | 21 | it('should not validate with a non-included value', function () { 22 | s.validate({ _id: 'test', state: 'UK' }).should.not.be.ok; 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /lib/seed/schematypes/embeddedSchema.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = EmbeddedSchema; 3 | 4 | function EmbeddedSchema (spec, value) { 5 | this.name = 'EmbeddedSchema'; 6 | this._path = spec; 7 | this._value = value; 8 | return this; 9 | } 10 | 11 | EmbeddedSchema.prototype.validate = function () { 12 | if (!Array.isArray(this._value)) return false; 13 | var schema = this._path.schema; 14 | for (var i = 0; i < this._value.length; i++) { 15 | var value = this._value[i]; 16 | if ('object' !== typeof value) return false; 17 | if (!schema.validate(value)) return false; 18 | } 19 | return true; 20 | }; 21 | 22 | EmbeddedSchema.prototype.getValue = function () { 23 | if (!this.validate()) return undefined; 24 | var arr = [] 25 | , schema = this._path.schema; 26 | for (var i = 0; i < this._value.length; i++) { 27 | var value = this._value[i]; 28 | arr.push(schema.getValue(value)); 29 | } 30 | return arr; 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seed", 3 | "version": "0.4.4", 4 | "author": "Jake Luer ", 5 | "description": "Storage-agnostic, event emitting datasets: schemas, models, hashes, and graphs.", 6 | "keyword": [ 7 | "database", 8 | "db", 9 | "storage", 10 | "schema", 11 | "model", 12 | "collection" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/qualiancy/seed.git" 17 | }, 18 | "main": "index", 19 | "scripts": { 20 | "test": "node_modules/goodwin/bin/goodwin --harmony" 21 | }, 22 | "engines": { 23 | "node": ">= v0.6.0" 24 | }, 25 | "dependencies": { 26 | "breeze": "0.4.x", 27 | "drip": "~1.3.1", 28 | "dragonfly": "0.2.x", 29 | "oath": "~1.0.0", 30 | "super": "0.2.x", 31 | "co": "~3.0.1" 32 | }, 33 | "devDependencies": { 34 | "goodwin": "https://github.com/logicalparadox/goodwin/tarball/master", 35 | "matcha": "~0.4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/seed/utils/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Utilities Loader 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var fs = require('fs') 12 | , path = require('path'); 13 | 14 | /*! 15 | * Local variables 16 | */ 17 | 18 | var _utils = {}; 19 | 20 | /*! 21 | * Get all utilities from `utils` folder and provide 22 | * access as a getter onto our exported variable. 23 | */ 24 | 25 | fs.readdirSync(__dirname).forEach(function(filename){ 26 | if (!/\.js$/.test(filename)) return; 27 | if (filename == '/index.js') return; 28 | var name = path.basename(filename, '.js'); 29 | Object.defineProperty(_utils, name, 30 | { get: function () { 31 | return require('./' + name); 32 | } 33 | , enumerable: true 34 | }); 35 | }); 36 | 37 | /*! 38 | * Mount the utilities to the export 39 | */ 40 | 41 | for (var exp in _utils) { 42 | var util = _utils[exp]; 43 | exports = _utils.object.merge(exports, util); 44 | } 45 | -------------------------------------------------------------------------------- /lib/seed.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * seed 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | var Seed = module.exports = {}; 8 | 9 | Seed.version = '0.4.4'; 10 | 11 | // Utility Constructors 12 | Seed.Promise = require('./seed/base/promise'); 13 | Seed.EventEmitter = require('./seed/base/events'); 14 | Seed.Query = require('./seed/base/query'); 15 | Seed.Hash = require('./seed/base/hash'); 16 | 17 | // Custom Errors 18 | Seed.errors = {}; 19 | Seed.errors.model = require('./seed/errors/model'); 20 | Seed.errors.graph = require('./seed/errors/graph'); 21 | Seed.errors.store = require('./seed/errors/store'); 22 | 23 | // Seed Constructors 24 | Seed.Schema = require('./seed/schema'); 25 | Seed.Model = require('./seed/model'); 26 | Seed.Graph = require('./seed/graph'); 27 | 28 | // Utils 29 | Seed.utils = require('./seed/utils'); 30 | Seed.async = require('./seed/base/async'); 31 | 32 | // Unique ID Strategies 33 | Seed.ObjectId = Seed.utils.Flake; 34 | Seed.Flake = Seed.utils.Flake; 35 | Seed.Crystal = Seed.utils.Crystal; 36 | 37 | // Expose storage prototype for extending 38 | Seed.Store = require('./seed/store'); 39 | 40 | // Build-in storage modules 41 | Seed.MemoryStore = require('./seed/stores/memory.js'); 42 | -------------------------------------------------------------------------------- /test/exports.js: -------------------------------------------------------------------------------- 1 | describe('Exports', function () { 2 | 3 | it('should have a valid version', function () { 4 | seed.version.should.match(/^\d+\.\d+\.\d+$/); 5 | }); 6 | 7 | it('should respond to utility constructors', function () { 8 | seed.should.respondTo('Promise'); 9 | seed.should.respondTo('EventEmitter'); 10 | seed.should.respondTo('Query'); 11 | }); 12 | 13 | it('should respond to main constructors', function () { 14 | seed.should.respondTo('Schema'); 15 | seed.should.respondTo('Hash'); 16 | seed.should.respondTo('Model'); 17 | seed.should.respondTo('Graph'); 18 | }); 19 | 20 | it('should respond to utilities', function () { 21 | seed.should.have.property('utils'); 22 | seed.utils.should.respondTo('merge'); 23 | seed.utils.should.respondTo('Flake'); 24 | }); 25 | 26 | it('should respond to unique id strategies', function () { 27 | seed.should.respondTo('Flake'); 28 | seed.should.respondTo('Crystal'); 29 | var flake = new seed.Flake(); 30 | flake.should.respondTo('gen'); 31 | var crystal = new seed.Crystal(); 32 | crystal.should.respondTo('gen'); 33 | }); 34 | 35 | it('should respond to storage', function () { 36 | seed.should.respondTo('Store'); 37 | seed.should.respondTo('MemoryStore'); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /examples/schema.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..') 2 | , Type = Seed.Schema.Type; 3 | 4 | var states = ['AL','AK','AZ','AR','CA','CO','CT','DE', 5 | 'FL','GA','HI','ID','IL','IN','IA','KS','KY','LA','ME','MD','MA','MI', 6 | 'MN','MS','MO','MT','NE','NV','NH','NJ','NM','NY','NC','ND','OH','OK', 7 | 'OR','PA','RI','SC','SD','TN','TX','UT','VT','VA','WA','WV','WI','WY']; 8 | 9 | var Person = new Seed.Schema({ 10 | name: { 11 | type: String, 12 | require: true 13 | }, 14 | age: { 15 | type: Number, 16 | require: true 17 | }, 18 | skills: Array, 19 | address: { 20 | street: String, 21 | city: String, 22 | state: { 23 | type: Type.Select, 24 | allowed: states 25 | }, 26 | zipcode: Number 27 | } 28 | }); 29 | 30 | var traveller_1 = { 31 | name: ':) Jim Doe', 32 | age: 26, 33 | skills: ['nodejs', 'javascript'], 34 | address: { 35 | street: 'Somewhere', 36 | city: 'San Francisco', 37 | state: 'CA', 38 | zipcode: 94106 39 | } 40 | }; 41 | 42 | var traveller_2 = { 43 | name: ':( Jim Doe', 44 | skills: 'nodejs', 45 | address: { 46 | street: 'Somewhere', 47 | city: 'London', 48 | state: 'UK' 49 | } 50 | }; 51 | 52 | var valid_person = Person.validate(traveller_1); 53 | var invalid_person = Person.validate(traveller_2); 54 | 55 | console.log(traveller_1.name + ' valid: ' + valid_person); // true 56 | console.log(traveller_2.name + ' valid: ' + invalid_person); // false 57 | -------------------------------------------------------------------------------- /lib/seed/schematypes/dbref.js: -------------------------------------------------------------------------------- 1 | 2 | var Model = require('../model'); 3 | 4 | module.exports = DBRef; 5 | 6 | function DBRef (spec, value) { 7 | this.name = 'DBRef'; 8 | this._path = spec; 9 | this._value = value; 10 | return this; 11 | } 12 | 13 | DBRef.prototype.validate = function () { 14 | var ref = this._value 15 | , type = this._path.model 16 | ? (this._path.model.prototype.__type || 'model') 17 | : null; 18 | 19 | if (ref instanceof Model) 20 | return type 21 | ? (ref.type === type) 22 | : true; 23 | else if (ref 24 | && ('undefined' !== typeof ref.$ref) 25 | && ('undefined' !== typeof ref.$id)) 26 | return type 27 | ? (ref.$ref === type) 28 | : true; 29 | else if (ref 30 | && ('undefined' !== typeof ref._bsontype) 31 | && (ref._bsontype === 'DBRef')) 32 | return type 33 | ? (ref.namespace === type) 34 | : true; 35 | 36 | return false; 37 | }; 38 | 39 | DBRef.prototype.getValue = function (opts) { 40 | if (this._path && !this.validate()) 41 | return undefined; 42 | 43 | opts = opts || {}; 44 | var model = this._value 45 | , obj = {}; 46 | 47 | if (model._bsontype === 'DBRef') { 48 | obj.$ref = model.namespace; 49 | obj.$id = model.oid; 50 | } else if (model instanceof Model && !opts.preserve) { 51 | obj.$ref = model.type; 52 | obj.$id = model.id; 53 | } else { 54 | obj = this._value; 55 | } 56 | 57 | return obj; 58 | }; 59 | -------------------------------------------------------------------------------- /test/schema.typeGeospatial.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('Geospatial', function () { 5 | var s = new Schema({ 6 | location: Schema.Type.Geospatial 7 | }); 8 | 9 | it('should validate with an array', function () { 10 | s.validate({ location: [ 40, 40 ] }).should.be.true; 11 | s.validate({ location: [ 40, 40, 40 ] }).should.be.true; 12 | }); 13 | 14 | it('should not validate with an improper array', function () { 15 | s.validate({ location: [ 40 ] }).should.be.false; 16 | // s.validate({ location: [ 40, 40, 40, 40 ] }).should.be.false; 17 | }); 18 | 19 | it('should validate with an object', function () { 20 | s.validate({ location: { x: 40, y: 40 } }).should.be.true; 21 | s.validate({ location: { x: 40, y: 40, z: 40 } }).should.be.true; 22 | s.validate({ location: { lon: 40, lat: 40 } }).should.be.true; 23 | s.validate({ location: { lon: 40, lat: 40 , alt: 40 } }).should.be.true; 24 | }); 25 | 26 | it('should not validate with an improper object', function () { 27 | s.validate({ location: { } }).should.be.false; 28 | s.validate({ location: { x: 40 } }).should.be.false; 29 | s.validate({ location: { y: 40 } }).should.be.false; 30 | s.validate({ location: { lon: 40 } }).should.be.false; 31 | s.validate({ location: { lat: 40 } }).should.be.false; 32 | }); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /lib/seed/utils/object.js: -------------------------------------------------------------------------------- 1 | var exports = module.exports = {}; 2 | 3 | exports.merge = function (a, b){ 4 | if (a && b) { 5 | for (var key in b) { 6 | a[key] = b[key]; 7 | } 8 | } 9 | return a; 10 | }; 11 | 12 | exports.defaults = function (a, b) { 13 | if (a && b) { 14 | for (var key in b) { 15 | if ('undefined' == typeof a[key]) a[key] = b[key]; 16 | } 17 | } 18 | return a; 19 | }; 20 | 21 | // a = target 22 | // b = source 23 | exports.deepMerge = function (a, b) { 24 | var arr = Array.isArray(b) 25 | , dest = arr ? [] : {}; 26 | 27 | // if source is array 28 | if (arr) { 29 | a = a || []; 30 | dest = dest.concat(a); 31 | for (var i = 0; i < b.length; i++) { 32 | var v = b[i]; 33 | if ('object' == typeof v) 34 | dest[i] = exports.deepMerge(a[i], v); 35 | else if (!~a.indexOf(v)) 36 | dest.push(v); 37 | } 38 | 39 | // everything else (objects too) 40 | } else { 41 | // if target is and object 42 | if (a && 'object' === typeof a) { 43 | var ak = Object.keys(a); 44 | for (var i = 0; i < ak.length; i++) 45 | dest[ak[i]] = a[ak[i]]; 46 | } 47 | 48 | var bk = Object.keys(b); 49 | for (var ii = 0; ii < bk.length; ii++) { 50 | var k = bk[ii]; 51 | if ('object' !== typeof b[k] || !b[k]) { 52 | dest[k] = b[k]; 53 | } else { 54 | if (a && !a[k]) a[k] = Array.isArray(b[k]) ? [] : {}; 55 | dest[k] = exports.deepMerge(a[k], b[k]); 56 | } 57 | } 58 | } 59 | 60 | return dest; 61 | }; 62 | -------------------------------------------------------------------------------- /test/graph.edge.js: -------------------------------------------------------------------------------- 1 | describe('Graph Edges', function () { 2 | var Edge = __seed.graph.Edge 3 | , Graph = seed.Graph 4 | , Model = seed.Model 5 | , Person = Model.extend('person') 6 | , people = new Graph({ type: 'people' }); 7 | 8 | var smith = new Person({ name: 'John Smith' }) 9 | , pond = new Person({ name: 'Amy Pond' }); 10 | 11 | before(function () { 12 | people.define(Person); 13 | people.set(smith); 14 | people.set(pond); 15 | }); 16 | 17 | it('should provide the graph theory methods', function () { 18 | Graph.should.respondTo('relate'); 19 | Graph.should.respondTo('unrelate'); 20 | }); 21 | 22 | it('should able to be created', function () { 23 | people.should.have.length(2); 24 | people._edges.should.have.length(0); 25 | 26 | people.relate(pond, smith, 'companion'); 27 | people.should.have.length(2); 28 | people._edges.should.have.length(1); 29 | }); 30 | 31 | it('should be a valid edge model', function () { 32 | var edge = people._edges.at(0); 33 | edge.should.be.instanceof(Edge); 34 | edge.parent.should.eql(people); 35 | edge.validate().should.be.true; 36 | edge.type.should.equal('people_edge'); 37 | edge.get('x').should.eql(pond); 38 | edge.get('y').should.eql(smith); 39 | edge.get('rel').should.equal('companion'); 40 | }); 41 | 42 | it('should be able to be removed', function () { 43 | people.should.have.length(2); 44 | people._edges.should.have.length(1); 45 | 46 | people.unrelate(pond, smith, 'companion'); 47 | people.should.have.length(2); 48 | people._edges.should.have.length(0); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/seed/utils/crystal.js: -------------------------------------------------------------------------------- 1 | // attributes: substack/node-hat 2 | // https://github.com/substack/node-hat 3 | 4 | var exports = module.exports = {}; 5 | 6 | exports.Crystal = function (opts) { 7 | opts = opts || {}; 8 | this._stack = {}; 9 | this.bits = opts.bits || 128; 10 | this.base = opts.base || 16; 11 | this.expandBy = opts.expandBy || 32; 12 | } 13 | 14 | exports.Crystal.prototype.get = function (key) { 15 | return this._stack[key]; 16 | } 17 | 18 | exports.Crystal.prototype.set = function (key, value) { 19 | this._stack[key] = value; 20 | return this; 21 | } 22 | 23 | exports.Crystal.prototype.rand = function () { 24 | var bits = this.bits 25 | , base = this.base; 26 | var digits = Math.log(Math.pow(2, bits)) / Math.log(base); 27 | for (var i = 2; digits === Infinity; i *= 2) { 28 | digits = Math.log(Math.pow(2, (bits/i) )) / Math.log(base) * i; 29 | } 30 | var rem = digits - Math.floor(digits) 31 | , res = ''; 32 | for (var i = 0; i < Math.floor(digits); i++) { 33 | var x = Math.floor(Math.random() * base).toString(base); 34 | res = x + res; 35 | } 36 | if (res) { 37 | var b = Math.pow(base, rem) 38 | , x = Math.floor(Math.random() * base).toString(base); 39 | res = x + res; 40 | } 41 | 42 | var parsed = parseInt(res, base); 43 | if (parsed !== Infinity && parsed >= Math.pow(2, bits)) { 44 | return this.rand(); 45 | } else { 46 | return res; 47 | } 48 | } 49 | 50 | exports.Crystal.prototype.gen = function (obj) { 51 | var iters = 0; 52 | do { 53 | if (iters ++ > 10) this.bits += this.expandBy; 54 | var id = this.rand(); 55 | } while (Object.hasOwnProperty(this._stack, id)); 56 | this.set(id, obj); 57 | return id; 58 | } 59 | -------------------------------------------------------------------------------- /lib/seed/store.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Storage 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * Module dependancies 9 | */ 10 | 11 | var inherits = require('super') 12 | , Promise = require('./base/promise') 13 | 14 | /*! 15 | * Seed component dependacies 16 | */ 17 | 18 | var _ = require('./utils') 19 | , EventEmitter = require('./base/events'); 20 | 21 | /*! 22 | * Main exports 23 | */ 24 | 25 | module.exports = Store; 26 | 27 | /** 28 | * # Store (constructor) 29 | * 30 | * The default store template that can be extended 31 | * by storage engines. Declares default sync option. 32 | * 33 | * Options are passed through to underlying engine. 34 | * 35 | * @api public 36 | */ 37 | 38 | function Store (options) { 39 | EventEmitter.call(this); 40 | this.initialize(options); 41 | } 42 | 43 | /** 44 | * Provide a way to extend Store 45 | */ 46 | 47 | Store.extend = inherits.extend; 48 | 49 | /*! 50 | * Merge with `drip` event emitter. 51 | */ 52 | 53 | inherits(Store, EventEmitter); 54 | 55 | /** 56 | * # .initialize() 57 | * 58 | * Default initialize function called on construction. 59 | * Alternative storage methods should replace this. 60 | * 61 | * @api public 62 | */ 63 | 64 | Store.prototype.initialize = function () {}; 65 | 66 | /** 67 | * # .sync() 68 | * 69 | * The default store template that can be extended 70 | * by storage engines. Declares default sync option. 71 | * 72 | * @param {String} action (get | set | destroy) 73 | * @param {Object} model instance of seed.model 74 | * @api public 75 | */ 76 | 77 | Store.prototype.sync = function (action, model, query) { 78 | var data = (model) ? model._attributes : null 79 | , collection = model.type; 80 | 81 | if (model.schema) 82 | data = model.schema.getValue(data); 83 | 84 | var promise = this[action]({ 85 | data: data 86 | , collection: collection 87 | , query: query || {} 88 | }); 89 | 90 | return promise; 91 | }; 92 | -------------------------------------------------------------------------------- /test/utils.object.js: -------------------------------------------------------------------------------- 1 | describe('Object Utilities', function () { 2 | 3 | describe('deepMerge', function () { 4 | var dm = seed.utils.deepMerge; 5 | 6 | it('should work for simple objects', function () { 7 | var a = { 8 | foo: 'bar' 9 | , baz: { 10 | bing: 'beep' 11 | } 12 | }; 13 | 14 | var b = { 15 | foo: 'zoo' 16 | , baz: { 17 | you: 'too' 18 | } 19 | }; 20 | 21 | var c = dm(a, b); 22 | c.should.eql({ 23 | foo: 'zoo' 24 | , baz: { 25 | bing: 'beep' 26 | , you: 'too' 27 | } 28 | }); 29 | }); 30 | 31 | it('should work for arrays', function () { 32 | var a = [ { 'a': 1 }, 2 ] 33 | , b = [ { 'b': 3 }, 4 ]; 34 | 35 | var c = dm(a, b); 36 | c.should.eql([ 37 | { a: 1, b: 3 } 38 | , 2 39 | , 4 40 | ]); 41 | }); 42 | 43 | it('should work for nested arrays', function () { 44 | var a = { 45 | foo: 46 | [ { bar: 'baz' } 47 | , 2 48 | , { you: 'too' } ] 49 | , bing: 'beep' 50 | }; 51 | 52 | var b = { 53 | foo: 54 | [ { too: 'you' } 55 | , 4 56 | , { bing: 'beep' } ] 57 | , bing: 'bop' 58 | , beep: 'boop' 59 | }; 60 | 61 | var c = dm(a, b); 62 | c.should.eql({ 63 | foo: 64 | [ { bar: 'baz' 65 | , too: 'you' } 66 | , 2 67 | , { you: 'too' 68 | , bing: 'beep' } 69 | , 4 ] 70 | , bing: 'bop' 71 | , beep: 'boop' 72 | }); 73 | }); 74 | 75 | it('should work for complex nested objects', function () { 76 | var a = { 77 | b: { 78 | c: { 79 | d: 'hello universe' 80 | } 81 | } 82 | } 83 | 84 | var b = dm({}, a); 85 | b.should.eql({ b: { c: { d: 'hello universe' } } }); 86 | }); 87 | 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /lib/seed/schematypes/geospatial.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = Geospatial; 4 | 5 | /** 6 | * # Geospacial 7 | * 8 | * Used for geospatial data entry. 9 | * 10 | * - Longitute is easting is `x` 11 | * - Latitude is northing is `y` 12 | * - Altitude is `z` (optional) 13 | * 14 | * Value can be in the form of an Array with 15 | * the format `[ x, y ]` or `[ x, y, z ]`. 16 | * 17 | * Value can be in the form of an object with 18 | * the properties of `{ x: n, y: n, z: n }` or 19 | * `{ lon: n, lat: n, alt: n }`. 20 | */ 21 | 22 | function Geospatial (path, value) { 23 | this.name = 'Geospatial'; 24 | this._path = path; 25 | this._value = {}; 26 | 27 | // For array values 28 | if (Array.isArray(value)) { 29 | this._value.x = value[0]; 30 | this._value.y = value[1]; 31 | if (value[2]) this._value.z = value[2]; 32 | return this; 33 | } 34 | 35 | if ('object' !== typeof value) return this; 36 | 37 | // For objects 38 | this._value.x = value.x || value.lon || null; 39 | this._value.y = value.y || value.lat || null; 40 | if (value.z || value.alt) 41 | this._value.z = value.z || value.alt; 42 | 43 | return this; 44 | } 45 | 46 | /** 47 | * # toArray 48 | * 49 | * Returns a GeoJSON formatted array for the 50 | * current location. 51 | * 52 | * @returns {Array} 53 | * @see http://geojson.org/geojson-spec.html#positions 54 | * @api public 55 | */ 56 | 57 | Geospatial.prototype.toArray = function () { 58 | var res = [ this._value.x, this._value.y ]; 59 | if (this._value.z) res.push(this._value.z); 60 | return res; 61 | }; 62 | 63 | /** 64 | * # validate 65 | * 66 | * Determine if the data currently stored in 67 | * the casted Schema Type is valid. 68 | * 69 | * @returns {Boolean} 70 | * @api public 71 | */ 72 | 73 | Geospatial.prototype.validate = function () { 74 | if (!this._value.x) return false; 75 | if (!this._value.y) return false; 76 | return true; 77 | }; 78 | 79 | Geospatial.prototype.getValue = function () { 80 | if (!this.validate()) return undefined; 81 | return this.toArray(); 82 | }; 83 | -------------------------------------------------------------------------------- /test/schema.typeDBRef.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Model = seed.Model 3 | , Schema = seed.Schema; 4 | 5 | describe('DBRef', function () { 6 | var Address = Model.extend('address') 7 | , PersonSchema = new Schema({ 8 | name: String 9 | , address: { 10 | type: Schema.Type.DBRef 11 | , model: Address 12 | } 13 | }); 14 | 15 | it('should have the proper definition', function () { 16 | PersonSchema.paths.should.have.length(2); 17 | PersonSchema.paths.keys.should.include('name', 'address'); 18 | 19 | PersonSchema.paths.get('address') 20 | .should.have.property('type') 21 | .to.eql(Schema.Type.DBRef); 22 | PersonSchema.paths.get('address') 23 | .should.have.property('model') 24 | .to.be.eql(Address); 25 | }); 26 | 27 | it('should validate with the proper values', function () { 28 | var address = new Address({ _id: 123, zip: 12345 }); 29 | 30 | PersonSchema.validate({ 31 | name: 'John Smith' 32 | , address: address 33 | }).should.be.true; 34 | 35 | PersonSchema.validate({ 36 | name: 'John Smith' 37 | , address: { 38 | $ref: 'address' 39 | , $id: 123 40 | } 41 | }).should.be.true; 42 | }); 43 | 44 | it('should not validate with improper values', function () { 45 | var address = new Model({ zip: 12345 }); 46 | 47 | PersonSchema.validate({ 48 | name: 'John Smith' 49 | , address: address 50 | }).should.be.false; 51 | 52 | PersonSchema.validate({ 53 | name: 'John Smith' 54 | , address: {} 55 | }).should.be.false; 56 | 57 | PersonSchema.validate({ 58 | name: 'John Smith' 59 | , address: { 60 | $ref: 'address' 61 | } 62 | }).should.be.false; 63 | 64 | PersonSchema.validate({ 65 | name: 'John Smith' 66 | , address: { 67 | $ref: 'person' 68 | , $id: 123 69 | } 70 | }).should.be.false; 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/schema.typeEmbeddedSchema.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('EmbeddedSchema', function () { 5 | var s_inner = new Schema({ 6 | name: { 7 | type: String 8 | , required: true 9 | } 10 | }); 11 | 12 | var s = new Schema({ 13 | id: Number 14 | , names: [ s_inner ] 15 | , tags: [ { tag: String } ] 16 | }); 17 | 18 | it('should have the proper definition', function () { 19 | s.paths.should.have.length(3); 20 | s.paths.keys.should.include('id', 'names', 'tags'); 21 | 22 | s.paths.get('names') 23 | .should.have.property('type') 24 | .to.eql(Schema.Type.EmbeddedSchema); 25 | 26 | s.paths.get('names') 27 | .should.have.property('schema') 28 | .to.be.instanceof(Schema) 29 | .and.eql(s_inner); 30 | 31 | s.paths.get('tags') 32 | .should.have.property('type') 33 | .to.eql(Schema.Type.EmbeddedSchema); 34 | 35 | s.paths.get('tags') 36 | .should.have.property('schema') 37 | .to.be.instanceof(Schema) 38 | .and.have.property('_definition') 39 | .to.eql({ tag: String }); 40 | }); 41 | 42 | it('should validate with the proper values', function () { 43 | s.validate({ 44 | id: 1 45 | , names: 46 | [ { name: 'hello' } 47 | , { name: 'universe' } ] 48 | , tags: 49 | [ { tag: 'worldy' } 50 | , { tag: 'validation' } ] 51 | }).should.be.true; 52 | }); 53 | 54 | it('should not validate with non-object values', function () { 55 | s.validate({ 56 | id: 2 57 | , names: [ 'hello', 'universe' ] 58 | , tags: [ 'worldly', 'validation' ] 59 | }).should.be.false; 60 | }); 61 | 62 | it('should not validate with nested bad types', function () { 63 | s.validate({ 64 | id: 3 65 | , names: [ { name: 1 } ] 66 | }).should.be.false; 67 | }); 68 | 69 | it('should not validate with nested missing required fields', function () { 70 | s.validate({ 71 | id: 4 72 | , names: [ { non_name: 'string' } ] 73 | }).should.be.false; 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/base/edges.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('../../../utils') 3 | , async = require('../../../base/async') 4 | , Hash = require('../../../base/hash') 5 | , helper = require('../../helpers') 6 | , Promise = require('../../../base/promise'); 7 | 8 | module.exports = Edges; 9 | 10 | function Edges (traversal) { 11 | this.traversal = traversal; 12 | this.rel = null; 13 | } 14 | 15 | Edges.prototype.exec = function (input) { 16 | var self = this 17 | , defer = new Promise() 18 | , graph = this.traversal.flag('parent') 19 | , hash = new Hash(null, { findRoot: '_attributes' }) 20 | , isLive = this.traversal.flag('live') 21 | , rel = this.rel || null; 22 | 23 | function push (queue) { 24 | return function doPush (model) { 25 | queue.push(model); 26 | } 27 | } 28 | 29 | function doFetchEdges (cb) { 30 | if (!isLive) return cb(null); 31 | var fetchFn = (self.direction === 'y') 32 | ? helper.fetchOutEdges 33 | : helper.fetchInEdges; 34 | 35 | var queue = async.queue(function (model, next) { 36 | fetchFn.call(graph, model, rel, next); 37 | }, 10); 38 | 39 | input.each(push(queue)); 40 | if (queue.length === 0) cb(null); 41 | queue.drain = cb; 42 | queue.onerror = cb; 43 | queue.process(); 44 | } 45 | 46 | function doEdgeFilter (cb) { 47 | var queue = async.queue(function (model, next) { 48 | edgeFilter.call(self, model, hash, next); 49 | }); 50 | 51 | input.each(push(queue)); 52 | if (queue.length === 0) cb(null); 53 | queue.drain = cb; 54 | queue.onerror = cb; 55 | queue.process(); 56 | } 57 | 58 | async.series([ 59 | doFetchEdges 60 | , doEdgeFilter 61 | ], function (err) { 62 | if (err) return defer.reject(err); 63 | defer.resolve(hash); 64 | }); 65 | 66 | return defer.promise; 67 | }; 68 | 69 | function edgeFilter (model, hash, cb) { 70 | var graph = this.traversal.flag('parent') 71 | , query = { $and: [ { $or: [] } ] } 72 | , which = this.direction; 73 | 74 | if (which === 'y') { 75 | query.$and[0].$or.push({ 'x.id': model.id }); 76 | query.$and[0].$or.push({ 'x.$id': model.id }); 77 | } else { 78 | query.$and[0].$or.push({ 'y.id': model.id }); 79 | query.$and[0].$or.push({ 'y.$id': model.id }); 80 | } 81 | 82 | if (this.rel) query.$and.push({ 'rel': this.rel }); 83 | 84 | graph 85 | ._edges 86 | .find(query) 87 | .each(function (edge) { 88 | var key = '/' + edge.type + '/' + edge.id; 89 | hash.set(key, edge); 90 | }); 91 | 92 | // setting up for possible async in near future 93 | cb(null); 94 | } 95 | -------------------------------------------------------------------------------- /test/schema.typeNative.js: -------------------------------------------------------------------------------- 1 | describe('Schema Type', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('Boolean', function () { 5 | var s = new Schema({ 6 | bln: Boolean 7 | }); 8 | 9 | it('should validate with a boolean', function () { 10 | s.validate({ bln: true }).should.be.true; 11 | s.validate({ bln: false }).should.be.true; 12 | }); 13 | 14 | it('should not validate with non booleans', function () { 15 | s.validate({ bln: 0 }).should.be.false; 16 | s.validate({ bln: 1 }).should.be.false; 17 | s.validate({ bln: 12 }).should.be.false; 18 | s.validate({ bln: 'true' }).should.be.false; 19 | s.validate({ bln: { bln: true }}).should.be.false; 20 | }); 21 | }); 22 | 23 | describe('Number', function () { 24 | var s = new Schema({ 25 | num: Number 26 | }); 27 | 28 | it('should validate with a number', function () { 29 | s.validate({ num: 10 }).should.be.true; 30 | }); 31 | 32 | it('should not validate with other types', function () { 33 | s.validate({ num: 'hello' }).should.be.false; 34 | s.validate({ num: [ ] }).should.be.false; 35 | }); 36 | }); 37 | 38 | describe('String', function () { 39 | var s = new Schema({ 40 | str: String 41 | }); 42 | 43 | it('should validate with a string', function () { 44 | s.validate({ str: 'hello' }).should.be.true; 45 | }); 46 | 47 | it('should not validate with other types', function () { 48 | s.validate({ str: 1 }).should.be.false; 49 | s.validate({ str: [ 'hello' ] }).should.be.false; 50 | }); 51 | }); 52 | 53 | describe('Array', function () { 54 | var s = new Schema({ 55 | arr: Array 56 | }); 57 | 58 | it('should validate with a array', function () { 59 | s.validate({ arr: [ 'hello', 1 ] }).should.be.true; 60 | }); 61 | 62 | it('should not validate with other types', function () { 63 | s.validate({ arr: 1 }).should.be.false; 64 | s.validate({ arr: 'hello' }).should.be.false; 65 | }); 66 | }); 67 | 68 | describe('Object', function () { 69 | var s = new Schema({ 70 | obj: Object 71 | }); 72 | 73 | it('should validate with an object', function () { 74 | s.validate({ obj: { testing: 1 } }).should.be.true; 75 | }); 76 | 77 | it('should not validate with other types', function () { 78 | s.validate({ obj: [ 1, 2 ] }).should.be.false; 79 | s.validate({ obj: 'hello world' }).should.be.false; 80 | }); 81 | 82 | it('should be able to get the value', function () { 83 | s.getValue({ obj: { testing: 1 }}).should.eql({ obj: { testing: 1 }}); 84 | }); 85 | }); 86 | 87 | }); 88 | -------------------------------------------------------------------------------- /examples/relations.js: -------------------------------------------------------------------------------- 1 | var Seed = require('..'); 2 | 3 | var store = new Seed.MemoryStore; 4 | 5 | var Person = Seed.Model.extend('person', { 6 | schema: new Seed.Schema({ 7 | 'name': String 8 | }) 9 | }); 10 | 11 | var People = Seed.Graph.extend({ 12 | initialize: function () { 13 | this.store = store; 14 | this.define(Person); 15 | } 16 | }); 17 | 18 | var smith = new Person({ 19 | name: 'The Doctor' 20 | }); 21 | 22 | var song = new Person({ 23 | name: 'River Song' 24 | }); 25 | 26 | var pond = new Person({ 27 | name: 'Amy Pond' 28 | }); 29 | 30 | var williams = new Person({ 31 | name: 'Rory Williams' 32 | }); 33 | 34 | var tardis = new People(); 35 | 36 | tardis.set(smith); 37 | tardis.set(song); 38 | tardis.set(pond); 39 | tardis.set(williams); 40 | 41 | // make edge from x to y with relation optionally repriocated 42 | // this.relate(x, y, relation, reciprocate); 43 | // read: x is relation to y 44 | 45 | tardis.relate(smith, song, 'married'); 46 | tardis.relate(song, smith, 'married'); 47 | 48 | tardis.relate(pond, williams, 'married'); 49 | tardis.relate(williams, pond, 'married'); 50 | 51 | tardis.relate(pond, smith, 'companion'); 52 | tardis.relate(williams, pond, 'companion'); 53 | 54 | tardis._edges.each(function (rel, key) { 55 | var namex = rel.get('x').get('name') 56 | , namey = rel.get('y').get('name') 57 | , relation = rel.get('rel'); 58 | console.log(namex + ' is ' + relation + ' to ' + namey); 59 | }); 60 | 61 | console.log(''); 62 | 63 | var trav1 = tardis.traverse(); 64 | 65 | trav1 66 | .select(pond) 67 | .out 68 | .end(function (err, hash) { 69 | console.log('Amy\'s has relationships with..'); 70 | hash.each(function (model) { 71 | console.log(model.get('name')); 72 | }); 73 | }); 74 | 75 | console.log(''); 76 | 77 | var trav2 = tardis.traverse(); 78 | 79 | trav2 80 | .select(pond) 81 | .outE('married') 82 | .end(function (err, hash) { 83 | hash.each(function (edge) { 84 | var x = edge.get('x').get('name') 85 | , y = edge.get('y').get('name'); 86 | console.log(x + ' is ' + edge.get('rel') + ' to ' + y); 87 | }); 88 | }); 89 | 90 | console.log(''); 91 | 92 | var trav3 = tardis.traverse(); 93 | 94 | trav3 95 | .select(smith) 96 | .in 97 | .end(function (err, hash) { 98 | hash.each(function (model) { 99 | console.log(model.get('name')); 100 | }); 101 | }); 102 | 103 | console.log(''); 104 | 105 | var trav4 = tardis.traverse(); 106 | 107 | trav4 108 | .select(smith) 109 | .inE 110 | .end(function (err, hash) { 111 | hash.each(function (edge) { 112 | var x = edge.get('x').get('name') 113 | , y = edge.get('y').get('name'); 114 | console.log(x + ' is ' + edge.get('rel') + ' to ' + y); 115 | }); 116 | }); 117 | 118 | -------------------------------------------------------------------------------- /lib/seed/graph/commands/base/vertices.js: -------------------------------------------------------------------------------- 1 | var _ = require('../../../utils') 2 | , async = require('../../../base/async') 3 | , Edge = require('../../edge/model') 4 | , Hash = require('../../../base/hash') 5 | , helper = require('../../helpers') 6 | , Promise = require('../../../base/promise'); 7 | 8 | module.exports = Vertices; 9 | 10 | function Vertices (traversal) { 11 | this.traversal = traversal; 12 | this.rel = null; 13 | } 14 | 15 | Vertices.prototype.exec = function (input) { 16 | var self = this 17 | , defer = new Promise() 18 | , graph = this.traversal.flag('parent') 19 | , hash = new Hash(null, { findRoot: '_attributes' }) 20 | , isLive = this.traversal.flag('live') 21 | , rel = this.rel || null; 22 | 23 | function push (queue) { 24 | return function doPush(model) { 25 | queue.push(model); 26 | } 27 | } 28 | 29 | function doFetchEdges (cb) { 30 | if (!isLive) return cb(null); 31 | var fetchFn = (self.direction === 'y') 32 | ? helper.fetchOutEdges 33 | : helper.fetchInEdges; 34 | 35 | var queue = async.queue(function (mod, next) { 36 | fetchFn.call(graph, mod, rel, next); 37 | }, 10); 38 | 39 | input.each(push(queue)); 40 | if (queue.length === 0) cb(null); 41 | queue.drain = cb; 42 | queue.onerror = cb; 43 | queue.process(); 44 | } 45 | 46 | function doFetchVertices (cb) { 47 | var queue = async.queue(function (mod, next) { 48 | fetchVertices.call(self, mod, hash, next); 49 | }, 10); 50 | 51 | input.each(push(queue)); 52 | if (queue.length === 0) cb(null); 53 | queue.drain = cb; 54 | queue.onerror = cb; 55 | queue.process(); 56 | } 57 | 58 | async.series([ 59 | doFetchEdges 60 | , doFetchVertices 61 | ], function (err) { 62 | if (err) return defer.reject(err); 63 | defer.resolve(hash); 64 | }); 65 | 66 | return defer.promise; 67 | }; 68 | 69 | function fetchVertices (model, hash, cb) { 70 | if (model instanceof Edge) 71 | return loadVertice.call(this, hash)(model, cb); 72 | 73 | var graph = this.traversal.flag('parent') 74 | , query = { $and: [ { $or: [] } ] } 75 | , queue = new async.queue(loadVertices.call(this, hash), 10) 76 | , which = this.direction; 77 | 78 | if (which === 'y') { 79 | query.$and[0].$or.push({ 'x.id': model.id }); 80 | query.$and[0].$or.push({ 'x.$id': model.id }); 81 | } else { 82 | query.$and[0].$or.push({ 'y.id': model.id }); 83 | query.$and[0].$or.push({ 'y.$id': model.id }); 84 | } 85 | 86 | if (this.rel) query.$and.push({ 'rel': this.rel }); 87 | 88 | graph 89 | ._edges 90 | .find(query) 91 | .each(function (model) { 92 | queue.push(model); 93 | }); 94 | 95 | if (queue.length === 0) cb(null); 96 | queue.drain = cb; 97 | queue.onerror = cb; 98 | queue.process(); 99 | }; 100 | 101 | function loadVertices (hash) { 102 | var which = this.direction 103 | , isLive = this.traversal.flag('live'); 104 | 105 | return function iterator (edge, next) { 106 | function setHash () { 107 | var vert = edge.get(which) 108 | , key = '/' + vert.type + '/' + vert.id; 109 | hash.set(key, vert); 110 | next(null); 111 | } 112 | 113 | if (!isLive) return setHash() 114 | edge.load(function (err) { 115 | if (err) return next(err); 116 | setHash(); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/seed/stores/memory.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed - MemoryStore Constructor 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | var async = require('../base/async') 8 | , errors = require('../errors/store') 9 | , Hash = require('../base/hash') 10 | , Promise = require('../base/promise') 11 | , Store = require('../store') 12 | , utils = require('../utils') 13 | , uid = new utils.Flake(); 14 | 15 | /** 16 | * # Store 17 | * 18 | * The default store template that can be extended 19 | * by storage engines. Declares default sync option. 20 | * 21 | * @api prototype 22 | */ 23 | 24 | var MemoryStore = module.exports = Store.extend({ 25 | 26 | name: 'MemoryStore' 27 | 28 | , initialize: function () { 29 | this.store = Object.create(null); 30 | } 31 | 32 | /** 33 | * # .set() 34 | * 35 | * Create or update: Set the whole value of an entry 36 | * in the store. Creates an ID if one does not exist. 37 | * 38 | * @param {Object} seed prepared model from sync. 39 | * @api public 40 | */ 41 | 42 | , set: function (seed) { 43 | var self = this 44 | , promise = new Promise() 45 | , id = seed.data._id 46 | , type = seed.collection; 47 | 48 | if (!this.store[type]) this.store[type] = new Hash(); 49 | var group = this.store[type]; 50 | 51 | if (!id) { 52 | id = uid.gen(); 53 | seed.data._id = id; 54 | } 55 | 56 | async.nextTick(function () { 57 | group.set(id, seed.data); 58 | promise.resolve(group.get(id)); 59 | }); 60 | 61 | return promise.promise; 62 | } 63 | 64 | /** 65 | * # .get() 66 | * 67 | * Read: Get the value of an entry in the store 68 | * given an ID. 69 | * 70 | * @param {Object} seed prepared model from sync. 71 | * @api public 72 | */ 73 | 74 | , get: function (seed) { 75 | var self = this 76 | , promise = new Promise() 77 | , id = seed.data._id 78 | , type = seed.collection; 79 | 80 | if (!this.store[type]) this.store[type] = new Hash(); 81 | var group = this.store[type]; 82 | 83 | async.nextTick(function () { 84 | var res = group.get(id); 85 | if (res) promise.resolve(res); 86 | else promise.reject(errors.create('not found')); 87 | }); 88 | 89 | return promise.promise; 90 | } 91 | 92 | /** 93 | * # .destroy() 94 | * 95 | * Delete: Remove an entry from the database. Emits 96 | * an error message if no Id in object or object doesn't 97 | * exist. 98 | * 99 | * @public {Object} seed prepared model from sync. 100 | * @api public 101 | */ 102 | 103 | , destroy: function (seed) { 104 | var promise = new Promise() 105 | , id = seed.data._id 106 | , type = seed.collection; 107 | 108 | if (!this.store[type]) this.store[type] = new Hash(); 109 | var group = this.store[type]; 110 | 111 | async.nextTick(function () { 112 | if (!id) return promise.reject(errors.create('no id')); 113 | group.del(id); 114 | promise.resolve(id); 115 | }); 116 | 117 | return promise.promise; 118 | } 119 | 120 | , fetch: function (seed) { 121 | var promise = new Promise() 122 | , type = seed.collection 123 | , query = seed.query 124 | , res = []; 125 | 126 | if (!this.store[type]) this.store[type] = new Hash(); 127 | var group = this.store[type]; 128 | 129 | async.nextTick(function () { 130 | group.find(query).each(function (value) { res.push(value); }); 131 | promise.resolve(res); 132 | }); 133 | 134 | return promise.promise; 135 | } 136 | 137 | }); 138 | -------------------------------------------------------------------------------- /lib/seed/graph/traversal.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Graph :: Traversal Chain API 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var inherits = require('super'); 12 | 13 | /*! 14 | * Seed component dependancies 15 | */ 16 | 17 | var _ = require('../utils') 18 | , EventEmitter = require('../base/events') 19 | , Hash = require('../base/hash'); 20 | 21 | /*! 22 | * Graph specific dependancies 23 | */ 24 | 25 | var Command = require('./commands'); 26 | 27 | /*! 28 | * Main export 29 | */ 30 | 31 | module.exports = Traversal; 32 | 33 | function Traversal (parent, options) { 34 | // setup 35 | EventEmitter.call(this); 36 | options = options || {}; 37 | 38 | // config 39 | this._stack = []; 40 | this.flag('parent', parent); 41 | 42 | var live = ('boolean' === typeof options.live) ? options.live : false; 43 | this.flag('live', live); 44 | } 45 | 46 | inherits(Traversal, EventEmitter); 47 | 48 | Traversal.prototype.flag = function (key, value, silent) { 49 | silent = ('boolean' === typeof silent) ? silent : false; 50 | var self = this 51 | , flags = this._flags || (this._flags = new Hash()); 52 | if (arguments.length === 1) { 53 | return flags.get(key); 54 | } else if (arguments.length > 1 && Array.isArray(key)) { 55 | key.forEach(function (elem) { 56 | flags.set(elem, value); 57 | if (!silent) self.emit([ 'flag', elem ], value); 58 | }); 59 | } else if (arguments.length > 1) { 60 | flags.set(key, value); 61 | if (!silent) this.emit([ 'flag', key ], value); 62 | } 63 | }; 64 | 65 | Traversal.prototype.end = function (cb) { 66 | var self = this 67 | , stack = this._stack 68 | , rejected = function (err) { cb(err) } 69 | , completed = function (result) { cb(null, result); }; 70 | 71 | function next (i, last) { 72 | var promise = stack[i]; 73 | 74 | function fulfilled (res) { 75 | if (!stack[++i]) return completed(res); 76 | next(i, res); 77 | } 78 | 79 | promise.exec(last).then(fulfilled, rejected); 80 | } 81 | 82 | next(0); 83 | }; 84 | 85 | function addCommand (cmd, opts) { 86 | var command = new Command[cmd](this, opts); 87 | this._stack.push(command); 88 | return command; 89 | }; 90 | 91 | Traversal.prototype.select = function (obj) { 92 | addCommand.call(this, 'select', obj); 93 | return this; 94 | }; 95 | 96 | Traversal.prototype.find = function (query) { 97 | 98 | }; 99 | 100 | Traversal.prototype.exec = function (fn) { 101 | 102 | }; 103 | 104 | Object.defineProperty(Traversal.prototype, 'out', 105 | { get: function () { 106 | var cmd = addCommand.call(this, 'outVertices') 107 | , addRelation = function (rel) { 108 | cmd.rel = rel; 109 | return this; 110 | } 111 | 112 | addRelation.__proto__ = this; 113 | return addRelation; 114 | } 115 | }); 116 | 117 | Object.defineProperty(Traversal.prototype, 'outE', 118 | { get: function () { 119 | var cmd = addCommand.call(this, 'outEdges') 120 | , addRelation = function (rel) { 121 | cmd.rel = rel; 122 | return this; 123 | } 124 | 125 | addRelation.__proto__ = this; 126 | return addRelation; 127 | } 128 | }); 129 | 130 | Object.defineProperty(Traversal.prototype, 'in', 131 | { get: function () { 132 | var cmd = addCommand.call(this, 'inVertices') 133 | , addRelation = function (rel) { 134 | cmd.rel = rel; 135 | return this; 136 | } 137 | 138 | addRelation.__proto__ = this; 139 | return addRelation; 140 | } 141 | }); 142 | 143 | Object.defineProperty(Traversal.prototype, 'inE', 144 | { get: function () { 145 | var cmd = addCommand.call(this, 'inEdges') 146 | , addRelation = function (rel) { 147 | cmd.rel = rel; 148 | return this; 149 | } 150 | 151 | addRelation.__proto__ = this; 152 | return addRelation; 153 | } 154 | }); 155 | -------------------------------------------------------------------------------- /test/schema.core.js: -------------------------------------------------------------------------------- 1 | describe('Schema', function () { 2 | var Schema = seed.Schema; 3 | 4 | describe('required data', function () { 5 | var s = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | age: Number 11 | }); 12 | 13 | it('should know what fields are required', function () { 14 | s.required.should.have.length(1); 15 | s.required.has('name').should.be.true; 16 | s.required.has('age').should.be.false; 17 | }); 18 | 19 | it('should validate when required field included', function () { 20 | s.validate({ _id: 'test', name: 'seed' }).should.be.ok; 21 | }); 22 | 23 | it('should not validate when required field is missing', function () { 24 | s.validate({ _id: 'test', age: 1 }).should.not.be.ok; 25 | }); 26 | }); 27 | 28 | describe('nested data', function () { 29 | var s = new Schema({ 30 | name: { 31 | first: { 32 | type: String, 33 | required: true 34 | }, 35 | last: String 36 | } 37 | }); 38 | 39 | it('should know what fields are required', function () { 40 | s.required.should.have.length(1); 41 | s.required.has('name.first').should.be.true; 42 | s.required.has('name').should.be.false; 43 | s.required.has('name.last').should.be.false; 44 | }); 45 | 46 | it('should validate when required fields included', function () { 47 | var p = { 48 | _id: 'test' 49 | , name: { 50 | first: 'Node', 51 | last: 'js' 52 | } 53 | }; 54 | 55 | s.validate(p).should.be.ok; 56 | s.validatePath('name.first', 'Node').should.be.true; 57 | s.validatePath('name.first', 12).should.be.false; 58 | s.validatePath('name.first', undefined).should.be.false; 59 | }); 60 | 61 | it('should not validate if data for parent not object', function () { 62 | var p = { _id: 'test', name: 'Node.js' }; 63 | s.validate(p).should.not.be.ok; 64 | }); 65 | 66 | it('should not validate when required field is missing', function () { 67 | var p = { 68 | _id: 'test' 69 | , name: { 70 | last: 'js' 71 | } 72 | }; 73 | 74 | s.validate(p).should.not.be.ok; 75 | }); 76 | }); 77 | 78 | describe('indexes', function () { 79 | var s = new Schema({ 80 | _id: { 81 | type: String 82 | , index: true 83 | } 84 | , name: String 85 | , location: { 86 | type: Schema.Type.Geospatial 87 | , index: '2d' 88 | } 89 | }); 90 | 91 | it('should recognize the index', function () { 92 | s.indexes.should.have.length(2); 93 | s.indexes.keys.should.include('_id', 'location'); 94 | }); 95 | 96 | it('should validate if an index is provided', function () { 97 | s.validate({ _id: 'hello'}).should.be.true 98 | }); 99 | 100 | it('should not validate if the wrong type of index is provided', function() { 101 | s.validate({ _id: 42 }).should.be.false; 102 | }); 103 | }); 104 | 105 | describe('model integration', function () { 106 | 107 | it('can be set to a model', function () { 108 | var m = new seed.Model() 109 | , s = new Schema(); 110 | s.indexes.should.have.length(0); 111 | m.schema = s; 112 | m.schema.should.deep.equal(s); 113 | s.indexes.should.have.length(1); 114 | }); 115 | 116 | it('can validate a models data', function () { 117 | var s = new Schema({ name: { type: String, required: true }}) 118 | , M = seed.Model.extend('test', { schema: s }) 119 | , m = new M({ name: 1234 }); 120 | m.get('name').should.equal(1234); 121 | m.validate().should.be.false; 122 | var notvalid = m.set('name', 1111); 123 | notvalid.should.be.false; 124 | m.get('name').should.equal(1234); 125 | var valid = m.set('name', 'Arthur Dent'); 126 | valid.should.be.true; 127 | m.get('name').should.equal('Arthur Dent'); 128 | }); 129 | 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seed [![Build Status](https://secure.travis-ci.org/qualiancy/seed.png)](http://travis-ci.org/qualiancy/seed) 2 | 3 | > A storage-agnostic graphing database framework. 4 | 5 | Seed is a suite of components (Graph, Model, Schema, Hash) that provide a common API for working with JSON-style 6 | documents, regardless of the storage engine. Additionally, Seed can layer a graph structure to facilitate the 7 | traversal of data relationships between documents of the same or different model types. 8 | 9 | - **Hash**: A thorough API for non-persistent key:value sets. These sets can be simple string key to number value, or 10 | string key to object/document value. Hashs are used extensively througout the internals of the other components. 11 | - **Schema**: A schema is a definition of data structure expecatations and are currently used for validation. Those 12 | who have used Mongoose will find the API familiar. 13 | - **Model**: A model defines all aspects and behavior related to a single data object in a set. More specifically, 14 | when used in conjunction with storage, a model represents one document in a collection or one row in a table. 15 | - **Graph**: A graph is a collection of instantiated models. A graph allows for logical groupings of several 16 | types of models, querying of storage, and traversal of model relationships. 17 | 18 | _Note: Documentation website in progress. Hold tight._ 19 | 20 | ### Projects Using Seed 21 | 22 | - [Kinetik](http://kinetik.qualiancy.com) is tag centric job queue for distributed applications. 23 | 24 | Other possible implementations: 25 | 26 | - Realtime notifications of status updates on social networks. 27 | - KPI monitoriing and alerts based on realtime sales data for shopping sites. 28 | - Evented ETL network 29 | 30 | ## Installation 31 | 32 | Package is available through [npm](http://npmjs.org). 33 | 34 | $ npm install seed 35 | 36 | ## Storage Engines 37 | 38 | Seed comes with a Memory based storage engine. Need an alternative? 39 | 40 | - [seed-redis](http://github.com/qualiancy/seed-redis) - Store your datasets in a 41 | [Redis](http://redis.io) database. 42 | - [seed-mongodb](https://github.com/qualiancy/seed-mongodb) - Store your datasets 43 | in a [MongoDB](http://www.mongodb.org/) database. 44 | - [seed-riak](https://github.com/qualiancy/seed-riak) - Store your datasets in 45 | a [Riak](http://wiki.basho.com/Riak.html) database. (Seed 0.3.x only) 46 | 47 | CouchDB coming very soon. 48 | 49 | ## Roadmap 50 | 51 | The current release (0.4.x) is production ready for small to medium size projects. The next release will 52 | focus on expanding on schema validation options and the graph traversal language. Also, be on the 53 | lookout for a kick-ass documentation website. 54 | 55 | ## Tests 56 | 57 | Tests are writting in [Mocha](http://github.com/visionmedia/mocha) using the [Chai](http://chaijs.com) 58 | `should` BDD assertion library. Make sure you have that installed, clone this repo, install dependacies 59 | using `npm install`. 60 | 61 | $ make test 62 | 63 | ## Contributors 64 | 65 | Interested in contributing? Fork to get started. Contact [@logicalparadox](http://github.com/logicalparadox) 66 | if you are interested in being regular contributor. 67 | 68 | * Jake Luer ([@logicalparadox](http://github.com/logicalparadox)) 69 | 70 | ## License 71 | 72 | (The MIT License) 73 | 74 | Copyright (c) 2011-2012 Jake Luer 75 | 76 | Permission is hereby granted, free of charge, to any person obtaining a copy 77 | of this software and associated documentation files (the "Software"), to deal 78 | in the Software without restriction, including without limitation the rights 79 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 80 | copies of the Software, and to permit persons to whom the Software is 81 | furnished to do so, subject to the following conditions: 82 | 83 | The above copyright notice and this permission notice shall be included in 84 | all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 87 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 88 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 89 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 90 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 91 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 92 | THE SOFTWARE. 93 | -------------------------------------------------------------------------------- /test/fixtures/countries.json: -------------------------------------------------------------------------------- 1 | {"Afghanistan":29835392,"Akrotiri":15700,"Albania":2994667,"Algeria":34994937,"American Samoa":67242,"Andorra":84825,"Angola":13338541,"Anguilla":15094,"Antigua and Barbuda":87884,"Argentina":41769726,"Armenia":2967975,"Aruba":106113,"Australia":21766711,"Austria":8217280,"Azerbaijan":8372373,"Bahamas, The":313312,"Bahrain":1214705,"Bangladesh":158570535,"Barbados":286705,"Belarus":9577552,"Belgium":10431477,"Belize":321115,"Benin":9325032,"Bermuda":68679,"Bhutan":708427,"Bolivia":10118683,"Bosnia and Herzegovina":4622163,"Botswana":2065398,"Brazil":203429773,"British Virgin Islands":25383,"Brunei":401890,"Bulgaria":7093635,"Burkina Faso":16751455,"Burma":53999804,"Burundi":10216190,"Cambodia":14701717,"Cameroon":19711291,"Canada":34030589,"Cape Verde":516100,"Cayman Islands":51384,"Central African Republic":4950027,"Chad":10758945,"Chile":16888760,"China":1336718015,"Christmas Island":1402,"Cocos (Keeling) Islands":596,"Colombia":44725543,"Comoros":794683,"Congo, Democratic Republic of the":71712867,"Congo, Republic of the":4243929,"Cook Islands":11124,"Costa Rica":4576562,"Cote d'Ivoire":21504162,"Croatia":4483804,"Cuba":11087330,"Curacao":142180,"Cyprus":1120489,"Czech Republic":10190213,"Denmark":5529888,"Dhekelia":15700,"Djibouti":757074,"Dominica":72969,"Dominican Republic":9956648,"Ecuador":15007343,"Egypt":82079636,"El Salvador":6071774,"Equatorial Guinea":668225,"Eritrea":5939484,"Estonia":1282963,"Ethiopia":90873739,"Falkland Islands (Islas Malvinas)":3140,"Faroe Islands":49267,"Fiji":883125,"Finland":5259250,"France":65312249,"French Polynesia":294935,"Gabon":1576665,"Gambia, The":1797860,"Gaza Strip":1657155,"Georgia":4585874,"Germany":81471834,"Ghana":24791073,"Gibraltar":28956,"Greece":10760136,"Greenland":57670,"Grenada":108419,"Guatemala":13824463,"Guernsey":65068,"Guinea":10601009,"Guinea-Bissau":1596677,"Guyana":744768,"Haiti":9719932,"Holy See (Vatican City)":832,"Honduras":8143564,"Hong Kong":7122508,"Hungary":9976062,"Iceland":311058,"India":1189172906,"Indonesia":245613043,"Iran":77891220,"Iraq":30399572,"Ireland":4670976,"Isle of Man":84655,"Israel":7473052,"Italy":61016804,"Jamaica":2868380,"Japan":126475664,"Jersey":94161,"Jordan":6508271,"Kazakhstan":15522373,"Kenya":41070934,"Kiribati":100743,"Korea, North":24457492,"Korea, South":48754657,"Kosovo":1825632,"Kuwait":2595628,"Kyrgyzstan":5587443,"Laos":6477211,"Latvia":2204708,"Lebanon":4143101,"Lesotho":1924886,"Liberia":3786764,"Libya":6597960,"Liechtenstein":35236,"Lithuania":3535547,"Luxembourg":503302,"Macau":573003,"Macedonia":2077328,"Madagascar":21926221,"Malawi":15879252,"Malaysia":28728607,"Maldives":394999,"Mali":14159904,"Malta":408333,"Marshall Islands":67182,"Mauritania":3281634,"Mauritius":1303717,"Mexico":113724226,"Micronesia, Federated States of":106836,"Moldova":4314377,"Monaco":30539,"Mongolia":3133318,"Montenegro":661807,"Montserrat":5140,"Morocco":31968361,"Mozambique":22948858,"Namibia":2147585,"Nauru":9322,"Nepal":29391883,"Netherlands":16847007,"New Caledonia":256275,"New Zealand":4290347,"Nicaragua":5666301,"Niger":16468886,"Nigeria":155215573,"Niue":1311,"Norfolk Island":2169,"Northern Mariana Islands":46050,"Norway":4691849,"Oman":3027959,"Pakistan":187342721,"Palau":20956,"Panama":3460462,"Papua New Guinea":6187591,"Paraguay":6459058,"Peru":29248943,"Philippines":101833938,"Pitcairn Islands":48,"Poland":38441588,"Portugal":10760305,"Puerto Rico":3989133,"Qatar":848016,"Romania":21904551,"Russia":138739892,"Rwanda":11370425,"Saint Barthelemy":7367,"Saint Helena, Ascension, and Tristan da Cunha":7700,"Saint Kitts and Nevis":50314,"Saint Lucia":161557,"Saint Martin":30615,"Saint Pierre and Miquelon":5888,"Saint Vincent and the Grenadines":103869,"Samoa":193161,"San Marino":31817,"Sao Tome and Principe":179506,"Saudi Arabia":26131703,"Senegal":12643799,"Serbia":7310555,"Seychelles":89188,"Sierra Leone":5363669,"Singapore":4740737,"Sint Maarten":37429,"Slovakia":5477038,"Slovenia":2000092,"Solomon Islands":571890,"Somalia":9925640,"South Africa":49004031,"South Sudan":8260490,"Spain":46754784,"Sri Lanka":21283913,"Sudan":45047502,"Suriname":491989,"Svalbard":2019,"Swaziland":1370424,"Sweden":9088728,"Switzerland":7639961,"Syria":22517750,"Taiwan":23071779,"Tajikistan":7627200,"Tanzania":42746620,"Thailand":66720153,"Timor-Leste":1177834,"Togo":6771993,"Tokelau":1384,"Tonga":105916,"Trinidad and Tobago":1227505,"Tunisia":10629186,"Turkey":78785548,"Turkmenistan":4997503,"Turks and Caicos Islands":44819,"Tuvalu":10544,"Uganda":34612250,"Ukraine":45134707,"United Arab Emirates":5148664,"United Kingdom":62698362,"United States":313232044,"Uruguay":3308535,"Uzbekistan":28128600,"Vanuatu":224564,"Venezuela":27635743,"Vietnam":90549390,"Virgin Islands":109666,"Wallis and Futuna":15398,"West Bank":2568555,"Western Sahara":507160,"Yemen":24133492,"Zambia":13881336,"Zimbabwe":12084304} -------------------------------------------------------------------------------- /test/model.js: -------------------------------------------------------------------------------- 1 | describe('Model', function () { 2 | var Model = seed.Model; 3 | 4 | describe('configuration', function () { 5 | 6 | it('should be an event emitter', function () { 7 | var spy = chai.spy(function (d) { 8 | d.should.eql({ hello: 'universe' }); 9 | }); 10 | 11 | Model.should.respondTo('on'); 12 | Model.should.respondTo('emit'); 13 | var model = new Model(); 14 | model.should.have.property('_drip'); 15 | model.on('test', spy); 16 | model.emit('test', { hello: 'universe' }); 17 | spy.should.have.been.called.once; 18 | }); 19 | 20 | it('should be constructed correctly', function () { 21 | var model = new Model({ hello: 'universe' }); 22 | model.should.have.property('_attributes') 23 | .and.have.property('hello', 'universe'); 24 | model._flags.should.be.instanceof(seed.Hash); 25 | model.should.respondTo('flag'); 26 | model.flag('new').should.be.true; 27 | model.flag('dirty').should.be.true; 28 | model.should.respondTo('initialize'); 29 | should.exist(model.id); 30 | }); 31 | 32 | it('should be `initialized`', function () { 33 | var spy = chai.spy() 34 | , M = Model.extend({ 35 | initialize: spy 36 | }); 37 | 38 | var model = new M(); 39 | spy.should.have.been.called.once; 40 | }); 41 | 42 | it('should understand types', function () { 43 | var model = new Model(null, { type: 'person' }); 44 | model.type.should.equal('person'); 45 | var Person = Model.extend('person') 46 | , person = new Person(); 47 | person.type.should.equal('person'); 48 | person.type = 'alien'; 49 | person.type.should.not.equal('alien'); 50 | person.type.should.equal('person'); 51 | }); 52 | 53 | it('should provide correct toString value', function () { 54 | Model.should.respondTo('toString'); 55 | Model.toString().should.equal('[object Model]'); 56 | var m = new Model(); 57 | m.should.respondTo('toString'); 58 | m.toString().should.equal('[object Model]'); 59 | var p = new Model(null, { type: 'person' }); 60 | p.should.respondTo('toString'); 61 | p.toString().should.equal('[object Person]'); 62 | }); 63 | }); 64 | 65 | describe('flags', function () { 66 | var model = new Model(); 67 | 68 | beforeEach(function () { 69 | model.off(); 70 | }); 71 | 72 | it('should be able to set a single flag', function () { 73 | var spy = chai.spy(function (val) { 74 | val.should.equal('universe'); 75 | }); 76 | model.on([ 'flag', 'hello' ], spy); 77 | model.flag('hello', 'universe'); 78 | model._flags.get('hello').should.equal('universe'); 79 | spy.should.have.been.called.once; 80 | }); 81 | 82 | it('should be able to set a single flag silently', function () { 83 | var spy = chai.spy(function (val) { 84 | val.should.equal('universe'); 85 | }); 86 | model.on([ 'flag', 'hello' ], spy); 87 | model.flag('hello', 'universe', true); 88 | model._flags.get('hello').should.equal('universe'); 89 | spy.should.have.been.not_called; 90 | }); 91 | 92 | it('should be able to set a flag array', function () { 93 | var spy = chai.spy(function (val) { 94 | val.should.equal('universe'); 95 | }); 96 | model.on([ 'flag', '*' ], spy); 97 | model.flag([ 'world', 'universe' ], 'universe'); 98 | model._flags.get('hello').should.equal('universe'); 99 | spy.should.have.been.called.twice; 100 | }); 101 | 102 | it('should be able to set a flag array silently', function () { 103 | var spy = chai.spy(function (val) { 104 | val.should.equal('universe'); 105 | }); 106 | model.on([ 'flag', '*' ], spy); 107 | model.flag([ 'world', 'universe' ], 'universe', true); 108 | model._flags.get('hello').should.equal('universe'); 109 | spy.should.have.been.not_called; 110 | }); 111 | }); 112 | 113 | describe('managing attributes', function () { 114 | var model = new Model(); 115 | it('should allow for attributes to be `set`', function () { 116 | model.set('hello', 'universe'); 117 | model._attributes.hello.should.equal('universe'); 118 | }); 119 | 120 | it('should allow for attributed to be `get`', function () { 121 | model.get('hello').should.equal('universe'); 122 | }); 123 | 124 | it('should allow for attributes to be `merged`', function () { 125 | model.merge({ hello: 'world' }); 126 | model.get('hello').should.equal('world'); 127 | }); 128 | 129 | it('should allow for attributes to be `get` by path', function () { 130 | model.merge({ open: { source: 'always' }}); 131 | model.get('open.source').should.equal('always'); 132 | }); 133 | 134 | it('should allow for id to be set/get via helper', function () { 135 | should.not.exist(model.get('_id')); 136 | model.id = 'testing'; 137 | model.get('_id').should.equal('testing'); 138 | model.id.should.equal('testing'); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /lib/seed/schema.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Schema 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var inherits = require('super'); 12 | 13 | /*! 14 | * Seed component dependancies 15 | */ 16 | 17 | var _ = require('./utils') 18 | , Hash = require('./base/hash') 19 | , Query = require('./base/query') 20 | , EventEmitter = require('./base/events'); 21 | 22 | /*! 23 | * main export 24 | */ 25 | 26 | module.exports = Schema; 27 | 28 | /** 29 | * # Schema 30 | * 31 | * Create a schema instance. 32 | * 33 | * @param {Object} definition 34 | * @api public 35 | * @name constructor 36 | */ 37 | 38 | function Schema (definition) { 39 | EventEmitter.call(this); 40 | this.paths = new Hash(); 41 | this._definition = definition; 42 | iteratePaths.call(this, '', definition); 43 | } 44 | 45 | /*! 46 | * expose types 47 | */ 48 | 49 | Schema.Type = require('./schematypes'); 50 | 51 | /*! 52 | * Inherit from EventEmitter 53 | */ 54 | 55 | inherits(Schema, EventEmitter); 56 | 57 | /** 58 | * # indexes (getter) 59 | * 60 | * Retrieve a list of all indexes for the current schema 61 | * 62 | * @api public 63 | * @returns Seed.Hash 64 | */ 65 | 66 | Object.defineProperty(Schema.prototype, 'indexes', 67 | { get: function () { 68 | return this.paths.find({ 'index' : { $and: 69 | [ { $ne: undefined } 70 | , { $ne: false } 71 | , { $ne: null } 72 | , { $ne: 0 } ] 73 | }}); 74 | } 75 | }); 76 | 77 | /** 78 | * # required 79 | * 80 | * Retrieve a list of all required fields for the current schema 81 | * 82 | * @api public 83 | * @returns Seed.Hash 84 | */ 85 | 86 | Object.defineProperty(Schema.prototype, 'required', 87 | { get: function () { 88 | return this.paths.find({ 'required': { $and: 89 | [ { $ne: undefined } 90 | , { $ne: false } 91 | , { $ne: null } 92 | , { $ne: 0 } ] 93 | }}); 94 | } 95 | }); 96 | 97 | /** 98 | * # validate 99 | * 100 | * Check to see if a given set of data is valid 101 | * for the current schema. 102 | * 103 | * @param {Object} data 104 | * @returns Boolean validity 105 | * @api public 106 | */ 107 | 108 | Schema.prototype.validate = function (data) { 109 | var self = this 110 | , valid = true 111 | , required = this.required; 112 | 113 | this.paths.each(function (def, path) { 114 | var datum = Query.getPathValue(path, data); 115 | 116 | if (required.has(path) && 'undefined' === typeof datum) 117 | return valid = false; 118 | 119 | if ('undefined' !== typeof datum && def.type) { 120 | var casted = self.castAsType(path, datum); 121 | if (!casted.validate()) return valid = false; 122 | } 123 | }); 124 | 125 | return valid; 126 | }; 127 | 128 | /** 129 | * # getValue 130 | * 131 | * Given a set of data, get the schema's appropriate data 132 | * values for each of the paths. 133 | * 134 | * @param {Object} data 135 | * @api public 136 | */ 137 | 138 | Schema.prototype.getValue = function (data) { 139 | var self = this 140 | , dest = {}; 141 | 142 | this.paths.each(function (def, path) { 143 | var datum = Query.getPathValue(path, data); 144 | if ('undefined' !== typeof datum && def.type) { 145 | var casted = self.castAsType(path, datum) 146 | , val = casted.getValue(); 147 | Query.setPathValue(path, val, dest); 148 | } 149 | }); 150 | 151 | return dest; 152 | }; 153 | 154 | /** 155 | * # validatePath 156 | * 157 | * Given a point of data at a given path, determine 158 | * if it is valid for the current schema. 159 | * 160 | * @param {String} path 161 | * @param {Mixed} data 162 | * @api public 163 | */ 164 | 165 | Schema.prototype.validatePath = function (path, value) { 166 | if (!this.paths.has(path)) return true; 167 | if (this.required.has(path) && !value) return false; 168 | var casted = this.castAsType(path, value); 169 | return casted.validate(); 170 | }; 171 | 172 | /*! 173 | * # iteratePaths 174 | * 175 | * Called on constructor to convert a given schema 176 | * definition to a Seed.Hash of path:spec values. 177 | * 178 | * Will continually iterate until all paths are covered. 179 | * 180 | * @param {String} path 181 | * @param {Object} definition 182 | * @api private 183 | */ 184 | 185 | function iteratePaths (path, definition) { 186 | for (var name in definition) { 187 | var def = definition[name]; 188 | 189 | // basic def: { first_name: String } 190 | if ((def && 'function' === typeof def) || def.name) { 191 | this.paths.set(path + name, { type: def }); 192 | 193 | // complex def: { first_name: { type: String, index: true } [..] 194 | } else if (def.type && 'function' == typeof def.type) { 195 | var spec = { type: def.type }; 196 | _.merge(spec, def); 197 | this.paths.set(path + name, spec); 198 | 199 | // embedded schemas 200 | } else if (Array.isArray(def) && 'undefined' !== def[0]) { 201 | var spec = { type: Schema.Type.EmbeddedSchema }; 202 | spec.schema = (def[0] instanceof Schema) ? def[0] : new Schema(def[0]); 203 | this.paths.set(path + name, spec); 204 | 205 | // nested definition 206 | } else { 207 | iteratePaths.call(this, path + name + '.', def); 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * # castAsType 214 | * 215 | * Take a specific data point and path spec, cast the 216 | * data into the type constructor to access Seed.Type 217 | * helper methods. See each type for a list methods. 218 | * 219 | * @param {Object} path specification 220 | * @param {Mixed} data element 221 | * @api public 222 | */ 223 | 224 | Schema.prototype.castAsType = function (path, data) { 225 | var def = this.paths.get(path) 226 | , type = new Schema.Type[def.type.name](def, data); 227 | return type; 228 | }; 229 | -------------------------------------------------------------------------------- /test/store.memory.js: -------------------------------------------------------------------------------- 1 | describe('MemoryStore', function () { 2 | var MemoryStore = seed.MemoryStore; 3 | 4 | describe('constructor', function () { 5 | 6 | it('should call initialize', function () { 7 | var Store = new MemoryStore(); 8 | Store.should.have.property('store').a('object'); 9 | }); 10 | 11 | }); 12 | 13 | describe('CRUD from Model', function () { 14 | 15 | // store uses hash so we can compare expectations. 16 | var store = new MemoryStore(); 17 | 18 | var Person = seed.Model.extend('person', { 19 | store: store 20 | }); 21 | 22 | var arthur = new Person({ 23 | _id: 'arthur' 24 | , name: 'Arthur Dent' 25 | }); 26 | 27 | it('should allow a new object to be created', function (done) { 28 | arthur.save(function (err) { 29 | should.not.exist(err); 30 | store.store.person.should.have.length(1); 31 | arthur._attributes.should.eql(store.store.person.get('arthur')); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('should allow an already written object to be retrieved', function (done) { 37 | var dent = new Person({ 38 | _id: 'arthur' 39 | }); 40 | 41 | dent.fetch(function (err) { 42 | should.not.exist(err); 43 | dent._attributes.should.eql(arthur._attributes); 44 | done(); 45 | }) 46 | }); 47 | 48 | it('should allow an already written object to be modified', function (done) { 49 | 50 | arthur.set('location', 'earth'); 51 | arthur.save(function (err) { 52 | should.not.exist(err); 53 | 54 | var confirm = new Person({ 55 | _id: 'arthur' 56 | }); 57 | 58 | confirm.fetch(function (err) { 59 | should.not.exist(err); 60 | confirm._attributes.should.eql({ 61 | _id: 'arthur' 62 | , name: 'Arthur Dent' 63 | , location: 'earth' 64 | }); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | 70 | it('should allow an already existing item to be removed', function (done) { 71 | 72 | arthur.destroy(function (err) { 73 | should.not.exist(err); 74 | 75 | var confirm = new Person({ 76 | _id: 'arthur' 77 | }); 78 | 79 | confirm.fetch(function (err) { 80 | should.exist(err); 81 | err.should.be.instanceof(seed.errors.store._proto); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | }); 87 | 88 | 89 | describe('CRUD from Graph', function () { 90 | var store = new MemoryStore() 91 | , graph = new seed.Graph({ 92 | store: store 93 | }); 94 | 95 | var Person = seed.Model.extend('person', {}) 96 | , Location = seed.Model.extend('location', {}); 97 | 98 | graph.define(Person); 99 | graph.define(Location); 100 | 101 | var arthur = { 102 | _id: 'arthur' 103 | , name: 'Arthur Dent' 104 | , stats: { 105 | origin: 'Earth' 106 | , species: 'human' 107 | } 108 | }; 109 | 110 | var ford = { 111 | _id: 'ford' 112 | , name: 'Ford Prefect' 113 | , stats: { 114 | origin: 'Betelgeuse-ish' 115 | , species: 'writer' 116 | } 117 | }; 118 | 119 | var earth = { 120 | _id: 'earth' 121 | , name: 'Dent\'s Planet Earth' 122 | }; 123 | 124 | var ship = { 125 | _id: 'gold' 126 | , name: 'Starship Heart of Gold' 127 | }; 128 | 129 | beforeEach(function () { 130 | graph.flush(); 131 | }); 132 | 133 | it('should allow new objects to be created', function (done) { 134 | graph.set('person', arthur._id, arthur); 135 | graph.set('person', ford._id, ford); 136 | graph.set('location', earth._id, earth); 137 | graph.set('location', ship._id, ship); 138 | 139 | graph.push(function (err) { 140 | should.not.exist(err); 141 | store.store.person.should.be.instanceof(seed.Hash); 142 | store.store.location.should.be.instanceof(seed.Hash); 143 | store.store.person.should.have.length(2); 144 | store.store.location.should.have.length(2); 145 | done(); 146 | }); 147 | 148 | }); 149 | 150 | it('should allow already existing objects to be read', function (done) { 151 | graph.set('person', arthur._id); 152 | graph.set('person', ford._id); 153 | graph.set('location', earth._id); 154 | graph.set('location', ship._id); 155 | 156 | graph.pull({ force: true }, function (err) { 157 | should.not.exist(err); 158 | graph.should.have.length(4); 159 | 160 | var arthur2 = graph.get('person', 'arthur'); 161 | arthur2._attributes.should.eql(arthur); 162 | arthur2.flag('dirty').should.be.false; 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should allow all records of a specific type to be fetched', function (done) { 168 | graph.fetch('person', function (err) { 169 | should.not.exist(err); 170 | graph.should.have.length(2); 171 | 172 | var arthur2 = graph.get('person' ,'arthur'); 173 | arthur2._attributes.should.eql(arthur); 174 | arthur2.flag('dirty').should.be.false; 175 | done(); 176 | }); 177 | }); 178 | 179 | it('should allow a subset of existing objects to be selected', function (done) { 180 | graph.fetch('person', { 'name': { $eq: 'Arthur Dent' } }, function (err) { 181 | should.not.exist(err); 182 | graph.should.have.length(1); 183 | 184 | var arthur2 = graph.get('person', 'arthur'); 185 | arthur2._attributes.should.eql(arthur); 186 | arthur2.flag('dirty').should.be.false; 187 | done(); 188 | }); 189 | }); 190 | 191 | it('show allow an already existing object to be updated', function (done) { 192 | graph.fetch('person', function (err) { 193 | should.not.exist(err); 194 | graph.should.have.length(2); 195 | 196 | var arthur2 = graph.get('person', 'arthur'); 197 | arthur2._attributes.should.eql(arthur); 198 | arthur2.flag('dirty').should.be.false; 199 | arthur2.set('name', 'The Traveler'); 200 | arthur2.flag('dirty').should.be.true; 201 | 202 | graph.push(function (err) { 203 | should.not.exist(err); 204 | store.store.person.get('arthur').should.have.property('name', 'The Traveler'); 205 | done(); 206 | }); 207 | }); 208 | }) 209 | 210 | it('should allow an already existing object to be deleted', function (done) { 211 | // deletion is handled through the model interface. this is not currently needed. 212 | // perhaps in time, if mass deletion of purging is required. 213 | done(); 214 | }); 215 | 216 | }); 217 | 218 | }); 219 | -------------------------------------------------------------------------------- /lib/seed/graph/helpers.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Graph :: Private Helpers 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * Seed specific dependancies 9 | */ 10 | 11 | var Edge = require('./edge/model') 12 | , errors = require('../errors/graph') 13 | , DBRef = require('../schematypes/dbref'); 14 | 15 | /** 16 | * # refreshLookups 17 | * 18 | * Ensure that the hash of model has all the 19 | * correct ID lookups. 20 | * 21 | * @param {Seed.Hash} hash to refresh 22 | * @api private 23 | */ 24 | 25 | exports.refreshLookups = function (hash) { 26 | var arr = hash.toArray(); 27 | hash.flush(true); 28 | for (var i = 0; i < arr.length; i++) { 29 | var key = '/' + arr[i].value.type + '/' + arr[i].value.id 30 | , value = arr[i].value; 31 | hash.set(key, value, true); 32 | } 33 | }; 34 | 35 | /** 36 | * # buildEdge 37 | * 38 | * Given two models and relation tag, construct 39 | * a new Edge model and return it. Must be called 40 | * with a graph's context. 41 | * 42 | * @context {Seed.Graph} 43 | * @param {Seed.Model|DBRef} vertice x 44 | * @param {Seed.Model|DBRef} vertice y 45 | * @param {String} relation tag 46 | * @param {Object} attributes to attach to edge 47 | * @returns {Seed.Model} instance of Graph#Edge 48 | * @api private 49 | */ 50 | 51 | exports.buildEdge = function (vertX, vertY, rel, attrs) { 52 | var type = this.type + '_edge' 53 | , edge = new Edge({ 54 | x: vertX 55 | , y: vertY 56 | , rel: rel 57 | , attributes: attrs 58 | }, { parent: this, type: type }); 59 | return edge; 60 | }; 61 | 62 | /** 63 | * # getEdge 64 | * 65 | * Given two vertices and a relation tag, 66 | * determine if there is an edge model constructed 67 | * in a constructed graph's hash of edges. Must be 68 | * called with the graph's context. 69 | * 70 | * @context {Seed.Graph} 71 | * @param {Seed.Model|DBRef} vertice x 72 | * @param {Seed.Model|DBRef} vertice y 73 | * @param {String} relation tag 74 | * @returns {Seed.Model} instance of Graph#Edge (or undefined) 75 | * @api private 76 | */ 77 | 78 | exports.getEdge = function (vertX, vertY, rel) { 79 | var x = 80 | { id: vertX.id || vertX.$id 81 | , type: vertX.type || vertX.$rel }; 82 | 83 | var y = 84 | { id: vertY.id || vertY.$id 85 | , type: vertY.type || vertY.$rel }; 86 | 87 | return this._edges.find({ 88 | $and: 89 | [ { $or: 90 | [ { 'x.id': x.id, 'x.type': x.type } 91 | , { 'x.$id': x.id, 'x.$rel': x.type } ] } 92 | , { $or: 93 | [ { 'y.id': y.id, 'y.type': y.type } 94 | , { 'y.$id': y.id, 'y.$rel': y.type } ] } 95 | , { 'rel': rel } ] 96 | }).at(0); 97 | }; 98 | 99 | /** 100 | * # fetchEdges 101 | * 102 | * Given a vertices (either x or y), connect to the graph's 103 | * store and fetch all edges related to that model. The vertice 104 | * can either be a constructed model or a DBRef object. The fetch 105 | * can further be filtered by the relation tag. All fetched edges 106 | * will be constructed into an Edge model and placed in the graph's 107 | * hash of edges. Must be called with the graph's context. 108 | * 109 | * @context {Seed.Graph} 110 | * @param {Seed.Model|DBRef} vertice (can be x or y) 111 | * @param {String} relation tag (optional) 112 | * @param {Function} callback to execute on completion 113 | * @cbparam {Object} error if issue with fetch or parse 114 | * @api private 115 | */ 116 | 117 | exports.fetchEdges = function (vert, rel, cb) { 118 | if ('function' === typeof rel) { 119 | cb = rel; 120 | rel = null 121 | } 122 | 123 | // query builder 124 | var query = { $and: 125 | [ { $or: 126 | [ { 'x.$id': vert.id 127 | , 'x.$ref': vert.type } 128 | , { 'y.$id': vert.id 129 | , 'y.$ref': vert.type } 130 | ] } 131 | ] 132 | }; 133 | 134 | if (rel && 'string' === typeof rel) 135 | query.$and.push({ 'rel': rel }); 136 | 137 | pullEdges.call(this, query, cb); 138 | }; 139 | 140 | /** 141 | * # fetchOutEdges 142 | * 143 | * Same as `fetchEdges` execept expects vertice supplied 144 | * to be the `x` vertice in the edge. 145 | * 146 | * @context {Seed.Graph} 147 | * @param {Seed.Model|DBRef} x vertice 148 | * @param {String} relation tag (optional) 149 | * @param {Function} callback to execute on completion 150 | * @cbparam {Object} error if issue with fetch or parse 151 | * @api private 152 | */ 153 | 154 | exports.fetchOutEdges = function (vert, rel, cb) { 155 | // check for rel param 156 | if ('function' === typeof rel) { 157 | cb = rel; 158 | rel = null 159 | } 160 | 161 | // query builder 162 | var query = { 163 | 'x.$id': vert.id 164 | , 'x.$ref': vert.type 165 | }; 166 | 167 | if (rel && 'string' === typeof rel) 168 | query.rel = rel; 169 | 170 | pullEdges.call(this, query, cb); 171 | }; 172 | 173 | /** 174 | * # fetchInEdges 175 | * 176 | * Same as `fetchEdges` execept expects vertice supplied 177 | * to be the `y` vertice in the edge. 178 | * 179 | * @context {Seed.Graph} 180 | * @param {Seed.Model|DBRef} y vertice 181 | * @param {String} relation tag (optional) 182 | * @param {Function} callback to execute on completion 183 | * @cbparam {Object} error if issue with fetch or parse 184 | * @api private 185 | */ 186 | 187 | exports.fetchInEdges = function (vert, rel, cb) { 188 | // check for rel param 189 | if ('function' === typeof rel) { 190 | cb = rel; 191 | rel = null 192 | } 193 | 194 | // query builder 195 | var query = { 196 | 'y.$id': vert.id 197 | , 'y.$ref': vert.type 198 | }; 199 | 200 | if (rel && 'string' === typeof rel) 201 | query.rel = rel; 202 | 203 | pullEdges.call(this, query, cb); 204 | }; 205 | 206 | /*! 207 | * # pullEdges 208 | * 209 | * Powers `fetchEdges` and equivalant helper methods. 210 | * Performs a query on the storage engine. 211 | * 212 | * @context {Seed.Graph} 213 | * @param {Object} query 214 | * @param {Function} callback 215 | * @api double-secret 216 | */ 217 | 218 | function pullEdges (query, cb) { 219 | // base checks and reassociation 220 | if (!this.store) return cb(errors.create('no store')); 221 | 222 | // empty callback 223 | if ('function' !== typeof cb) 224 | cb = function () {}; 225 | 226 | // variables 227 | var self = this 228 | , type = this.type + '_edge'; 229 | 230 | // given array fetched, load edges into graph 231 | function parseEdges (data) { 232 | for (var i = 0; i < data.length; i++) { 233 | var rawEdge = data[i] 234 | , rawX = new DBRef(null, rawEdge.x).getValue() 235 | , rawY = new DBRef(null, rawEdge.y).getValue() 236 | , vertX = (self.has(rawX.$ref, rawX.$id)) 237 | ? self.get(rawX.$ref, rawX.$id) 238 | : rawX 239 | , vertY = (self.has(rawY.$ref, rawY.$id)) 240 | ? self.get(rawY.$ref, rawY.$id) 241 | : rawY 242 | , edge = exports.getEdge.call(self, vertX, vertY, rawEdge.rel); 243 | 244 | if (!edge) { 245 | edge = exports.buildEdge.call(self, vertX, vertY, rawEdge.rel, rawEdge.attributes); 246 | edge.set('_id', rawEdge._id, { silent: true }); 247 | edge.flag('new', false, true); 248 | edge.flag('dirty', false, true); 249 | self._edges.set(edge.id, edge); 250 | } 251 | } 252 | 253 | exports.refreshLookups.call(self, self._edges); 254 | cb(null); 255 | } 256 | 257 | // begin the query 258 | this.store 259 | .sync('fetch', { type: type }, query) 260 | .then(parseEdges, function rejected (err) { cb(err) }); 261 | } 262 | -------------------------------------------------------------------------------- /test/graph.core.js: -------------------------------------------------------------------------------- 1 | describe('Graph Core', function () { 2 | var Graph = seed.Graph 3 | , Person = seed.Model.extend('person', {}) 4 | , Location = seed.Model.extend('location', {}); 5 | 6 | var arthur = { 7 | _id: 'arthur' 8 | , name: 'Arthur Dent' 9 | , stats: { 10 | origin: 'Earth' 11 | , occupation: 'traveller' 12 | , species: 'human' 13 | } 14 | }; 15 | 16 | var ford = { 17 | _id: 'ford' 18 | , name: 'Ford Prefect' 19 | , stats: { 20 | origin: 'Betelgeuse-ish' 21 | , occupation: 'traveller' 22 | , species: 'writer' 23 | } 24 | }; 25 | 26 | var earth = { 27 | _id: 'earth' 28 | , name: 'Dent\'s Planet Earth' 29 | }; 30 | 31 | var ship = { 32 | _id: 'gold' 33 | , name: 'Starship Heart of Gold' 34 | }; 35 | 36 | describe('constructor', function () { 37 | var n = 0 38 | , graph = Graph.extend('people', { 39 | initialize: function () { 40 | n++; 41 | } 42 | }) 43 | , g = new graph(); 44 | 45 | it('should call initialize', function () { 46 | n.should.equal(1); 47 | }); 48 | 49 | it('should emit events', function () { 50 | var spy = chai.spy(); 51 | g.on('test', spy); 52 | g.emit('test'); 53 | spy.should.have.been.called.once; 54 | }); 55 | 56 | it('should define itself as a graph', function () { 57 | Graph.toString().should.equal('[object Graph]'); 58 | }); 59 | 60 | it('should know its types', function () { 61 | g.define('person', { _id: String }); 62 | g.types.should.include('person'); 63 | }); 64 | }); 65 | 66 | describe('configuration', function () { 67 | it('should understand types', function () { 68 | var graph = new Graph({ type: 'people' }); 69 | graph.type.should.equal('people'); 70 | var People = Graph.extend('people') 71 | , people = new People(); 72 | people.type.should.equal('people'); 73 | people.type = 'aliens'; 74 | people.type.should.not.equal('aliens'); 75 | people.type.should.equal('people'); 76 | }); 77 | }); 78 | 79 | describe('flags', function () { 80 | var g = new seed.Graph(); 81 | 82 | beforeEach(function () { 83 | g.off(); 84 | }); 85 | 86 | it('should be able to set a single flag', function () { 87 | var spy = chai.spy(function (val) { 88 | val.should.equal('universe'); 89 | }); 90 | g.on([ 'flag', 'hello' ], spy); 91 | g.flag('hello', 'universe'); 92 | g._flags.get('hello').should.equal('universe'); 93 | spy.should.have.been.called.once; 94 | }); 95 | 96 | it('should be able to set a single flag silently', function () { 97 | var spy = chai.spy(function (val) { 98 | val.should.equal('universe'); 99 | }); 100 | g.on([ 'flag', 'hello' ], spy); 101 | g.flag('hello', 'universe', true); 102 | g._flags.get('hello').should.equal('universe'); 103 | spy.should.have.been.not_called; 104 | }); 105 | 106 | it('should be able to set a flag array', function () { 107 | var spy = chai.spy(function (val) { 108 | val.should.equal('universe'); 109 | }); 110 | g.on([ 'flag', '*' ], spy); 111 | g.flag([ 'world', 'universe' ], 'universe'); 112 | g._flags.get('hello').should.equal('universe'); 113 | spy.should.have.been.called.twice; 114 | }); 115 | 116 | it('should be able to set a flag array silently', function () { 117 | var spy = chai.spy(function (val) { 118 | val.should.equal('universe'); 119 | }); 120 | g.on([ 'flag', '*' ], spy); 121 | g.flag([ 'world', 'universe' ], 'universe', true); 122 | g._flags.get('hello').should.equal('universe'); 123 | spy.should.have.been.not_called; 124 | }); 125 | }); 126 | 127 | describe('type definitions', function () { 128 | var g = new Graph(); 129 | 130 | it('should be able to accept a model definition', function () { 131 | g.define('person', Person); 132 | g.types.should.include('person'); 133 | }); 134 | 135 | it('should be able to accept a schema instance', function () { 136 | var s = new seed.Schema({ 137 | name: String 138 | }); 139 | 140 | g.define('person2', s); 141 | g.types.should.include('person2'); 142 | }); 143 | 144 | it('should be able to accept a schema definition', function () { 145 | var s = { 146 | name: String 147 | }; 148 | 149 | g.define('person3', s); 150 | g.types.should.include('person3'); 151 | }); 152 | 153 | it('should have all types included', function () { 154 | g.types.length.should.equal(3); 155 | }); 156 | }); 157 | 158 | describe('adding basic data', function () { 159 | var g = new Graph() 160 | , spy = chai.spy(function (person) { 161 | should.exist(person); 162 | person.flag('type').should.equal('person'); 163 | }); 164 | 165 | g.define('person', Person); 166 | 167 | it('should emit `add` events', function () { 168 | g.on('add:person:*', spy); 169 | }); 170 | 171 | it('should allow data to be set by address', function () { 172 | var a = g.set('person', arthur._id, arthur) 173 | , f = g.set('person', ford._id, ford); 174 | g.length.should.equal(2); 175 | a.id.should.equal(arthur._id); 176 | f.id.should.equal(ford._id); 177 | a._attributes.should.deep.equal(arthur); 178 | f._attributes.should.deep.equal(ford); 179 | }); 180 | 181 | it('should have called all callbacks', function () { 182 | spy.should.have.been.called.twice; 183 | }); 184 | }); 185 | 186 | describe('filter/find', function () { 187 | var g = new Graph(); 188 | 189 | g.define('person', Person); 190 | g.define('location', Location); 191 | 192 | g.set('person', arthur._id, arthur); 193 | g.set('person', ford._id, ford); 194 | g.set('location', earth._id, earth); 195 | g.set('location', ship._id, ship); 196 | 197 | it('should provide a hash when find by attr', function () { 198 | var res = g.find({ 'name' : { $eq: 'Arthur Dent' } }); 199 | res.should.have.length(1); 200 | res.should.be.instanceof(seed.Hash); 201 | }); 202 | 203 | it('should allow for filter by type', function () { 204 | var res = g.filter('person'); 205 | res.should.have.length(2); 206 | res.should.be.instanceof(seed.Hash); 207 | }); 208 | 209 | it('should allow for filter by iterator', function () { 210 | var res = g.filter(function (m) { 211 | return m.get('stats.occupation') == 'traveller'; 212 | }); 213 | res.should.have.length(2); 214 | res.should.be.instanceof(seed.Hash); 215 | }); 216 | 217 | it('should allow for filter by type + iterator', function () { 218 | var res = g.filter('person', function (m) { 219 | return m.id == 'arthur'; 220 | }); 221 | res.should.have.length(1); 222 | res.should.be.instanceof(seed.Hash); 223 | }); 224 | 225 | it('should returned undefined for bad formed filter', function () { 226 | var res = g.filter(); 227 | should.not.exist(res); 228 | }); 229 | 230 | it('should allow for filter then find', function () { 231 | var res = g.filter('person').find({ 'name': { $eq: 'Arthur Dent' }}); 232 | res.should.have.length(1); 233 | res.should.be.instanceof(seed.Hash); 234 | }); 235 | 236 | it('should allow for filters by nested attribute', function () { 237 | var res = g.find({ 'stats.species' : { $eq: 'human' } }); 238 | res.should.have.length(1); 239 | res.should.be.instanceof(seed.Hash); 240 | }); 241 | 242 | }); 243 | 244 | describe('iteration', function () { 245 | var g = new Graph(); 246 | 247 | g.define('person', Person); 248 | g.define('location', Location); 249 | 250 | g.set('person', arthur._id, arthur); 251 | g.set('person', ford._id, ford); 252 | g.set('location', earth._id, earth); 253 | g.set('location', ship._id, ship); 254 | 255 | it('should allow for iteration through all objects', function () { 256 | var i = 0; 257 | g.each(function (m) { 258 | i++; 259 | }); 260 | i.should.equal(4); 261 | }); 262 | 263 | it('should allow for iteration through specific types', function () { 264 | var i = 0; 265 | g.each('person', function (m) { 266 | i++; 267 | }); 268 | i.should.equal(2); 269 | }); 270 | }); 271 | 272 | describe('flush', function () { 273 | 274 | var g = new Graph(); 275 | 276 | g.define('person', Person); 277 | g.define('location', Location); 278 | 279 | beforeEach(function () { 280 | g.set('person', arthur._id, arthur); 281 | g.set('person', ford._id, ford); 282 | g.set('location', earth._id, earth); 283 | g.set('location', ship._id, ship); 284 | }); 285 | 286 | it('should allow flushing', function () { 287 | var spy = chai.spy(); 288 | g.should.have.length(4); 289 | g.on([ 'flush', 'all' ], spy); 290 | g.flush(); 291 | g.should.have.length(0); 292 | spy.should.have.been.called.once; 293 | }); 294 | 295 | it('should allow flushing by type', function () { 296 | var spy = chai.spy(); 297 | g.should.have.length(4); 298 | g.on([ 'flush', 'person'], spy); 299 | g.flush('person'); 300 | g.should.have.length(2); 301 | spy.should.have.been.called.once; 302 | }); 303 | }); 304 | 305 | describe('using schema', function () { 306 | var PersonSchema = new seed.Schema({ 307 | _id: seed.Schema.Type.ObjectId 308 | , name: String 309 | , stats: Object 310 | }); 311 | 312 | var Spaceman = Person.extend('spaceman', { schema: PersonSchema }) 313 | , graph = new seed.Graph('spacemen') 314 | , spaceman; 315 | 316 | before(function () { 317 | graph.define(Spaceman); 318 | }); 319 | 320 | it('can set attributes of a new model', function () { 321 | var spaceman = graph.set('spaceman', arthur); 322 | spaceman.should.have.property('_attributes') 323 | .deep.equal(arthur); 324 | }); 325 | 326 | }); 327 | 328 | }); 329 | -------------------------------------------------------------------------------- /test/hash.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , path = require('path'); 3 | 4 | describe('Hash', function () { 5 | var Hash = seed.Hash; 6 | 7 | var data_raw = fs.readFileSync(path.join(__dirname, 'fixtures', 'countries.json'), 'utf-8') 8 | , data = JSON.parse(data_raw) 9 | , expected_length = 238; 10 | 11 | describe('getters', function () { 12 | it('should have length', function () { 13 | var hash = new Hash(data); 14 | hash.length.should.equal(expected_length); 15 | }); 16 | 17 | it('should have an array list of keys', function () { 18 | var hash = new Hash(data) 19 | , keys = hash.keys; 20 | 21 | keys.should.be.instanceof(Array); 22 | keys.length.should.equal(expected_length); 23 | }); 24 | 25 | it('should have an array of values', function () { 26 | var hash = new Hash(data) 27 | , values = hash.values; 28 | 29 | values.should.be.instanceof(Array); 30 | values.length.should.equal(expected_length); 31 | }); 32 | }); 33 | 34 | 35 | describe('modifiers', function () { 36 | it('GET should work for all values', function () { 37 | var hash = new Hash(data) 38 | , n = 0; 39 | 40 | for (var key in data) { 41 | hash.get(key).should.equal(data[key]); 42 | n++; 43 | } 44 | 45 | hash.length.should.equal(expected_length); 46 | n.should.equal(hash.length); 47 | }); 48 | 49 | describe('SET', function () { 50 | it('should work for all values', function () { 51 | var hash = new Hash(); 52 | 53 | for (var key in data) { 54 | hash.set(key, data[key]); 55 | } 56 | 57 | hash.length.should.equal(expected_length); 58 | }); 59 | 60 | }); 61 | 62 | describe('HAS', function () { 63 | it('should work for all values', function () { 64 | var hash = new Hash(data) 65 | for (var key in data) { 66 | hash.has(key).should.be.true; 67 | } 68 | }); 69 | 70 | it('should return false for non existent keys', function () { 71 | var hash = new Hash(data); 72 | hash.has('hello').should.be.false; 73 | hash.has('hasOwnProperty').should.be.false; 74 | hash.has('__proto__').should.be.false; 75 | hash.has('constructor').should.be.false; 76 | }); 77 | }); 78 | 79 | describe('DEL', function() { 80 | it('should work for all values', function () { 81 | var hash = new Hash(data) 82 | , count = expected_length; 83 | for (var key in data) { 84 | hash.length.should.equal(count); 85 | hash.del(key); 86 | count--; 87 | } 88 | hash.length.should.equal(0); 89 | }); 90 | 91 | }); 92 | }); 93 | 94 | describe('utilities', function () { 95 | it('should allow for hash cloning', function () { 96 | var hash = new Hash(data) 97 | , hash2 = hash.clone(); 98 | 99 | hash.set('Nation of Seed.js', 10000000); 100 | 101 | hash.keys.should.include('Nation of Seed.js'); 102 | hash2.keys.should.not.include('Nation of Seed.js'); 103 | }); 104 | 105 | it('should produce an Array of key/value objects', function () { 106 | var hash = new Hash(data) 107 | , index = 0; 108 | 109 | var arr = hash.toArray(); 110 | 111 | arr.should.be.instanceof(Array); 112 | arr.length.should.equal(expected_length); 113 | arr[0].key.should.equal('Afghanistan'); 114 | arr[0].value.should.equal(29835392); 115 | }); 116 | }); 117 | 118 | describe('position #at', function () { 119 | it('should return the correct item for an index', function () { 120 | var hash = new Hash(data) 121 | , n = 0; 122 | 123 | for (var key in data) { 124 | hash.at(n).should.equal(data[key]); 125 | n++; 126 | } 127 | 128 | n.should.equal(expected_length); 129 | }); 130 | }); 131 | 132 | describe('position #index', function () { 133 | it('should return the correct location for an item', function () { 134 | var hash = new Hash(data) 135 | , n = 0; 136 | 137 | for (var key in data) { 138 | hash.index(key).should.equal(n); 139 | n++; 140 | } 141 | 142 | n.should.equal(expected_length); 143 | }); 144 | }); 145 | 146 | describe('iteration', function () { 147 | describe('#each', function () { 148 | it('should iterate over all objects in hash', function () { 149 | var hash = new Hash(data) 150 | , n = 0; 151 | 152 | hash.each(function (d, k, i) { 153 | i.should.equal(n); 154 | d.should.equal(data[k]); 155 | n++; 156 | }); 157 | 158 | n.should.equal(expected_length); 159 | }); 160 | }); 161 | 162 | describe('#map', function () { 163 | var hash = new Hash(data) 164 | , n = 0 165 | , s = 0; 166 | 167 | var hash2 = hash.map(function (d, k, i) { 168 | d.should.equal(data[k]); 169 | i.should.equal(n); 170 | n++; 171 | s += d; 172 | return d + 1; 173 | }); 174 | 175 | it('should iterate over all object in hash', function () { 176 | n.should.equal(expected_length); 177 | }); 178 | 179 | }); 180 | 181 | describe('#reduce', function () { 182 | var hash = new Hash(data) 183 | , n = 0; 184 | 185 | it('should be able to reduce', function () { 186 | var res =hash.reduce(function (value, key) { 187 | return 1; 188 | }); 189 | 190 | res.should.equal(hash.length); 191 | }); 192 | 193 | it('should be able to map reduce', function () { 194 | 195 | var mapFn = function (key, value, emit) { 196 | var firstLetter = key[0].toUpperCase(); 197 | emit(firstLetter, value); 198 | } 199 | 200 | var reduceFn = function (key, values) { 201 | var res = 0; 202 | values.forEach(function (v) { 203 | res += v; 204 | }); 205 | return res; 206 | } 207 | 208 | var result = hash.mapReduce(mapFn, reduceFn); 209 | chai.expect(result._data).eql({ 210 | A: 164634460, 211 | B: 499541913, 212 | C: 1612921712, 213 | D: 16332279, 214 | E: 201923164, 215 | F: 71801966, 216 | G: 153667627, 217 | H: 34962898, 218 | I: 1616633286, 219 | J: 135946476, 220 | K: 139914902, 221 | L: 29208715, 222 | M: 265693642, 223 | N: 235034558, 224 | O: 3027959, 225 | P: 387744743, 226 | Q: 848016, 227 | R: 172014868, 228 | S: 286525125, 229 | T: 243917984, 230 | U: 492263162, 231 | V: 118519363, 232 | W: 3091113, 233 | Y: 24133492, 234 | Z: 25965640 235 | }); 236 | }); 237 | }); 238 | 239 | describe('#find', function () { 240 | var hash = new Hash(data); 241 | 242 | it('should allow for basic finding', function () { 243 | var hash2 = hash.find({ $gt: 100000000 }); 244 | hash2.should.have.length(12); 245 | }); 246 | 247 | it('should allow for multiple findings', function () { 248 | var hash2 = hash.find({ $gt: 100000000, $lt: 300000000 }); 249 | hash2.should.have.length(9); 250 | }); 251 | 252 | it('should allow for nested findings', function () { 253 | var hash2 = hash.find({ $or: [ { $gt: 300000000 }, { $lt: 10000 } ]}); 254 | hash2.should.have.length(17); 255 | }); 256 | }); 257 | 258 | describe('#select', function () { 259 | var hash = new Hash(data) 260 | , n = 0; 261 | 262 | var hash2 = hash.filter(function (d, k) { 263 | d.should.equal(data[k]); 264 | n++; 265 | return (n <= 10) ? true : false; 266 | }); 267 | 268 | it('should iterate over all object in hash', function () { 269 | n.should.equal(expected_length); 270 | }); 271 | 272 | it('should return a subselection of the original hash', function () { 273 | hash2.length.should.equal(10); 274 | }); 275 | }); 276 | 277 | describe('#sort', function () { 278 | var hash = new Hash(data); 279 | 280 | it('should return all objects sorted ASC', function () { 281 | hash.sort('asc'); 282 | hash.length.should.equal(expected_length); 283 | hash.index('Pitcairn Islands').should.equal(0); 284 | hash.at(0).should.equal(hash.get('Pitcairn Islands')); 285 | }); 286 | 287 | it('should return all objects sorted DESC', function () { 288 | hash.sort('desc'); 289 | hash.length.should.equal(expected_length); 290 | hash.index('China').should.equal(0); 291 | hash.at(0).should.equal(hash.get('China')); 292 | }); 293 | 294 | it('should return all objects sorted KASC', function () { 295 | hash.sort('kasc'); 296 | hash.length.should.equal(expected_length); 297 | hash.index('China').should.equal(43); 298 | hash.at(43).should.equal(hash.get('China')); 299 | }); 300 | 301 | it('should return all objects sorted KASC (default)', function () { 302 | hash.sort(); 303 | hash.length.should.equal(expected_length); 304 | hash.index('China').should.equal(43); 305 | hash.at(43).should.equal(hash.get('China')); 306 | }); 307 | 308 | it('should return all objects sorted KDESC', function () { 309 | hash.sort('kdesc'); 310 | hash.length.should.equal(expected_length); 311 | hash.index('China').should.equal(194); 312 | hash.at(194).should.equal(hash.get('China')); 313 | }); 314 | 315 | it('should return all objects sorted by a custom function', function () { 316 | // alphabetical 317 | hash.sort(function (a, b) { 318 | var A = a.key.toLowerCase() 319 | , B = b.key.toLowerCase(); 320 | if (A < B) return -1; 321 | else if (A > B) return 1; 322 | else return 0; 323 | }); 324 | 325 | hash.length.should.equal(expected_length); 326 | hash.index('China').should.equal(43); 327 | hash.at(43).should.equal(hash.get('China')); 328 | }); 329 | 330 | }); 331 | 332 | }); 333 | }); 334 | -------------------------------------------------------------------------------- /test/graph.traversal.js: -------------------------------------------------------------------------------- 1 | describe('Graph Traversal', function () { 2 | var Edge = __seed.graph.Edge 3 | , Hash = seed.Hash 4 | , Graph = seed.Graph 5 | , Model = seed.Model 6 | , Traversal = __seed.graph.Traversal; 7 | 8 | describe('Construction', function () { 9 | var g = new Graph({ type: 'test1' }) 10 | , traverse = g.traverse(); 11 | 12 | it('should constructor properly', function () { 13 | g.should.respondTo('traverse'); 14 | 15 | traverse.should.be.instanceof(Traversal); 16 | traverse.should 17 | .respondTo('select') 18 | .respondTo('end') 19 | .respondTo('flag'); 20 | 21 | traverse.flag('parent').should.eql(g); 22 | traverse.flag('live').should.be.false; 23 | }); 24 | 25 | it('should have chainable commands', function () { 26 | traverse.should.have.property('out') 27 | .and.be.a('function'); 28 | traverse.should.have.property('outE') 29 | .and.be.a('function'); 30 | traverse.should.have.property('in') 31 | .and.be.a('function'); 32 | traverse.should.have.property('inE') 33 | .and.be.a('function'); 34 | }); 35 | }); 36 | 37 | describe('Static Traversal', function () { 38 | var Person = Model.extend('person') 39 | , g = new Graph({ type: 'static' }) 40 | , doctor = new Person({ name: 'The Doctor' }) 41 | , song = new Person({ name: 'River Song' }) 42 | , pond = new Person({ name: 'Amy Pond' }) 43 | , williams = new Person({ name: 'Rory Williams' }); 44 | 45 | before(function () { 46 | g.define(Person); 47 | g.set(doctor); 48 | }); 49 | 50 | it('can traverse when empty', function (done) { 51 | var traverse = g.traverse(); 52 | traverse 53 | .select(doctor) 54 | .out 55 | .end(function (err, hash) { 56 | should.not.exist(err); 57 | hash.should.be.instanceof(Hash); 58 | hash.should.have.length(0); 59 | done(); 60 | }); 61 | }); 62 | 63 | describe('with data', function () { 64 | 65 | before(function () { 66 | g.set(song); 67 | g.set(pond); 68 | g.set(williams); 69 | 70 | g.relate(doctor, song, 'married'); 71 | g.relate(song, doctor, 'married'); 72 | g.relate(pond, williams, 'married'); 73 | g.relate(williams, pond, 'married'); 74 | g.relate(pond, doctor, 'companion'); 75 | g.relate(williams, pond, 'companion'); 76 | }); 77 | 78 | it('should have the proper edges', function () { 79 | g.should.have.length(4); 80 | g._edges.should.have.length(6); 81 | }); 82 | 83 | it('should allow for `select`', function (done) { 84 | var traverse = g.traverse(); 85 | traverse 86 | .select(doctor) 87 | .end(function (err, hash) { 88 | should.not.exist(err); 89 | hash.should.be.instanceof(Hash); 90 | hash.should.have.length(1); 91 | hash.at(0).should.eql(doctor); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('should allow for `out` VERTICES', function (done) { 97 | var traverse = g.traverse(); 98 | traverse 99 | .select(pond) 100 | .out 101 | .end(function (err, hash) { 102 | should.not.exist(err); 103 | hash.should.be.instanceof(Hash); 104 | hash.should.have.length(2); 105 | hash.keys.should.include('/person/' + williams.id, '/person/' + doctor.id); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('should allow for `out` relation filtered VERTICES', function (done) { 111 | var traverse = g.traverse(); 112 | traverse 113 | .select(pond) 114 | .out('married') 115 | .end(function (err, hash) { 116 | should.not.exist(err); 117 | hash.should.be.instanceof(Hash); 118 | hash.should.have.length(1); 119 | hash.at(0).should.eql(williams); 120 | done(); 121 | }); 122 | }); 123 | 124 | it('should allow for `out` EDGES', function (done) { 125 | var traverse = g.traverse(); 126 | traverse 127 | .select(pond) 128 | .outE 129 | .end(function (err, hash) { 130 | should.not.exist(err); 131 | hash.should.be.instanceof(Hash); 132 | hash.should.have.length(2); 133 | hash.each(function (e) { 134 | e.should.be.instanceof(Edge); 135 | e.get('x').should.eql(pond); 136 | }); 137 | done(); 138 | }); 139 | }); 140 | 141 | it('should allow for `out` relation filtered EDGES', function (done) { 142 | var traverse = g.traverse(); 143 | traverse 144 | .select(pond) 145 | .outE('married') 146 | .end(function (err, hash) { 147 | should.not.exist(err); 148 | hash.should.be.instanceof(Hash); 149 | hash.should.have.length(1); 150 | hash.at(0).should.be.instanceof(Edge); 151 | hash.at(0).get('x').should.eql(pond); 152 | hash.at(0).get('y').should.eql(williams); 153 | hash.at(0).get('rel').should.eql('married'); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('should allow for `in` VERTICES', function (done) { 159 | var traverse = g.traverse(); 160 | traverse 161 | .select(doctor) 162 | .in 163 | .end(function (err, hash) { 164 | should.not.exist(err); 165 | hash.should.be.instanceof(Hash); 166 | hash.should.have.length(2); 167 | hash.keys.should.include('/person/' + song.id, '/person/' + pond.id); 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should allow for `in` relation filtered VERTICES', function (done) { 173 | var traverse = g.traverse(); 174 | traverse 175 | .select(doctor) 176 | .in('married') 177 | .end(function (err, hash) { 178 | should.not.exist(err); 179 | hash.should.be.instanceof(Hash); 180 | hash.should.have.length(1); 181 | hash.at(0).should.eql(song); 182 | done(); 183 | }); 184 | }); 185 | 186 | it('should allow for `in` EDGES', function (done) { 187 | var traverse = g.traverse(); 188 | traverse 189 | .select(doctor) 190 | .inE 191 | .end(function (err, hash) { 192 | should.not.exist(err); 193 | hash.should.be.instanceof(Hash); 194 | hash.should.have.length(2); 195 | hash.each(function (e) { 196 | e.should.be.instanceof(Edge); 197 | e.get('y').should.eql(doctor); 198 | }); 199 | done(); 200 | }); 201 | }); 202 | 203 | it('should allow for `in` relation filtered EDGES', function (done) { 204 | var traverse = g.traverse(); 205 | traverse 206 | .select(doctor) 207 | .inE('companion') 208 | .end(function (err, hash) { 209 | should.not.exist(err); 210 | hash.should.be.instanceof(Hash); 211 | hash.should.have.length(1); 212 | hash.at(0).should.be.instanceof(Edge); 213 | hash.at(0).get('x').should.eql(pond); 214 | hash.at(0).get('y').should.eql(doctor); 215 | hash.at(0).get('rel').should.eql('companion'); 216 | done(); 217 | }); 218 | }); 219 | 220 | }); 221 | }); 222 | 223 | describe('Live Traversal', function () { 224 | var Person = Model.extend('person') 225 | , store = new seed.MemoryStore() 226 | , g = new Graph({ type: 'static', store: store }) 227 | , doctor = new Person({ name: 'The Doctor' }) 228 | , song = new Person({ name: 'River Song' }) 229 | , pond = new Person({ name: 'Amy Pond' }) 230 | , williams = new Person({ name: 'Rory Williams' }); 231 | 232 | before(function (done) { 233 | g.define(Person); 234 | 235 | g.set(doctor); 236 | doctor.save(function (err) { 237 | if (err) throw err; 238 | done(); 239 | }); 240 | }); 241 | 242 | it('can traverse on an empty db', function (done) { 243 | g.flush(); 244 | var traverse = g.traverse({ live: true }); 245 | traverse 246 | .select(doctor) 247 | .out 248 | .end(function (err, hash) { 249 | should.not.exist(err); 250 | hash.should.be.instanceof(Hash); 251 | hash.should.have.length(0); 252 | done(); 253 | }); 254 | }); 255 | 256 | describe('with records', function () { 257 | 258 | before(function (done) { 259 | g.set(song); 260 | g.set(pond); 261 | g.set(williams); 262 | 263 | g.relate(doctor, song, 'married'); 264 | g.relate(song, doctor, 'married'); 265 | g.relate(pond, williams, 'married'); 266 | g.relate(williams, pond, 'married'); 267 | g.relate(pond, doctor, 'companion'); 268 | g.relate(williams, pond, 'companion'); 269 | 270 | g.push(function (err) { 271 | if (err) throw err; 272 | done(); 273 | }); 274 | }); 275 | 276 | beforeEach(function () { 277 | g.flush(); 278 | }); 279 | 280 | it('should allow for a `out` EDGES', function (done) { 281 | g.should.have.length(0); 282 | g._edges.should.have.length(0); 283 | 284 | g.set(pond); 285 | 286 | var traverse = g.traverse({ live: true }); 287 | traverse 288 | .select(pond) 289 | .outE 290 | .end(function (err, hash) { 291 | should.not.exist(err); 292 | hash.should.be.instanceof(Hash); 293 | hash.should.have.length(2); 294 | hash.each(function (e) { 295 | e.should.be.instanceof(Edge); 296 | e.get('x').should.eql(pond); 297 | }); 298 | done(); 299 | }); 300 | }); 301 | 302 | it('should allow for a `out` relation filtered EDGES', function (done) { 303 | g.should.have.length(0); 304 | g._edges.should.have.length(0); 305 | 306 | g.set(pond); 307 | 308 | var traverse = g.traverse({ live: true }); 309 | traverse 310 | .select(pond) 311 | .outE('married') 312 | .end(function (err, hash) { 313 | should.not.exist(err); 314 | hash.should.be.instanceof(Hash); 315 | hash.should.have.length(1); 316 | 317 | var edge = hash.at(0); 318 | edge.should.be.instanceof(Edge); 319 | edge.get('x').should.eql(pond); 320 | edge.get('y.$id').should.equal(williams.id); 321 | done(); 322 | }); 323 | }); 324 | 325 | it('should allow for `in` EDGES', function (done) { 326 | g.should.have.length(0); 327 | g._edges.should.have.length(0); 328 | 329 | g.set(pond); 330 | 331 | var traverse = g.traverse({ live: true }); 332 | traverse 333 | .select(pond) 334 | .inE 335 | .end(function (err, hash) { 336 | should.not.exist(err); 337 | hash.should.be.instanceof(Hash); 338 | hash.should.have.length(2); 339 | hash.each(function (e) { 340 | e.should.be.instanceof(Edge); 341 | e.get('y').should.eql(pond); 342 | }); 343 | done(); 344 | }); 345 | }); 346 | 347 | it('should allow for a `in` relation filtered EDGES', function (done) { 348 | g.should.have.length(0); 349 | g._edges.should.have.length(0); 350 | 351 | g.set(pond); 352 | 353 | var traverse = g.traverse({ live: true }); 354 | traverse 355 | .select(pond) 356 | .inE('married') 357 | .end(function (err, hash) { 358 | should.not.exist(err); 359 | hash.should.be.instanceof(Hash); 360 | hash.should.have.length(1); 361 | 362 | var edge = hash.at(0); 363 | edge.should.be.instanceof(Edge); 364 | edge.get('y').should.eql(pond); 365 | edge.get('x.$id').should.equal(williams.id); 366 | done(); 367 | }); 368 | }); 369 | 370 | it('should allow for `out` VERTICES', function (done) { 371 | g.should.have.length(0); 372 | g._edges.should.have.length(0); 373 | 374 | g.set(pond); 375 | 376 | var traverse = g.traverse({ live: true }); 377 | traverse 378 | .select(pond) 379 | .out 380 | .end(function (err, hash) { 381 | should.not.exist(err); 382 | hash.should.be.instanceof(Hash); 383 | hash.should.have.length(2); 384 | hash.keys.should.include('/person/' + williams.id, '/person/' + doctor.id); 385 | done(); 386 | }); 387 | }); 388 | 389 | it('should allow for `out` filtered VERTICES', function (done) { 390 | g.should.have.length(0); 391 | g._edges.should.have.length(0); 392 | 393 | g.set(pond); 394 | 395 | var traverse = g.traverse({ live: true }); 396 | traverse 397 | .select(pond) 398 | .out('married') 399 | .end(function (err, hash) { 400 | should.not.exist(err); 401 | hash.should.be.instanceof(Hash); 402 | hash.should.have.length(1); 403 | hash.at(0).id.should.equal(williams.id); 404 | hash.at(0).get('name').should.equal(williams.get('name')); 405 | done(); 406 | }); 407 | }); 408 | 409 | it('should allow for `in` VERTICES', function (done) { 410 | g.should.have.length(0); 411 | g._edges.should.have.length(0); 412 | 413 | g.set(doctor); 414 | 415 | var traverse = g.traverse({ live: true }); 416 | traverse 417 | .select(doctor) 418 | .in 419 | .end(function (err, hash) { 420 | should.not.exist(err); 421 | hash.should.be.instanceof(Hash); 422 | hash.should.have.length(2); 423 | hash.keys.should.include('/person/' + pond.id, '/person/' + song.id); 424 | done(); 425 | }); 426 | }); 427 | 428 | it('should allow for `in` filtered VERTICES', function (done) { 429 | g.should.have.length(0); 430 | g._edges.should.have.length(0); 431 | 432 | g.set(doctor); 433 | 434 | var traverse = g.traverse({ live: true }); 435 | traverse 436 | .select(doctor) 437 | .in('married') 438 | .end(function (err, hash) { 439 | should.not.exist(err); 440 | hash.should.be.instanceof(Hash); 441 | hash.should.have.length(1); 442 | hash.at(0).id.should.equal(song.id); 443 | hash.at(0).get('name').should.equal(song.get('name')); 444 | done(); 445 | }); 446 | }); 447 | }); 448 | }); 449 | }); 450 | -------------------------------------------------------------------------------- /lib/seed/model.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Model 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var inherits = require('super'); 12 | 13 | /*! 14 | * Seed component dependancies 15 | */ 16 | 17 | var _ = require('./utils') 18 | , async = require('./base/async') 19 | , errors = require('./errors/model') 20 | , EventEmitter = require('./base/events') 21 | , Hash = require('./base/hash') 22 | , Query = require('./base/query') 23 | , Schema = require('./schema') 24 | , Store = require('./store') 25 | , uid = new _.Flake(); 26 | 27 | /*! 28 | * Module global constants 29 | */ 30 | 31 | var noop = function () {}; 32 | 33 | /*! 34 | * main export 35 | */ 36 | 37 | module.exports = Model; 38 | 39 | /** 40 | * # Model 41 | * 42 | * Create a model instance or extend a model definition. 43 | * 44 | * @param {Object} attributes to be set on model creation 45 | * @param {Object} options to influence model behavior 46 | * @api public 47 | * @name constructor 48 | */ 49 | 50 | function Model (attributes, opts) { 51 | // setup 52 | EventEmitter.call(this); 53 | opts = opts || {}; 54 | 55 | // config 56 | if (opts.schema) this.schema = opts.schema; 57 | if (opts.store) this.store = opts.store; 58 | if (opts.parent) this.parent = opts.parent; 59 | if (opts.type) this.flag('type', opts.type, true); 60 | 61 | // flags 62 | this.flag('new', true, true); 63 | this.flag('dirty', true, true); 64 | 65 | // default data 66 | this.uid = uid.gen(); 67 | this._attributes = {}; 68 | this.merge(attributes, { 69 | silent: true 70 | , force: true 71 | }); 72 | 73 | // init 74 | var args = Array.prototype.slice.call(arguments, 1); 75 | this.initialize.apply(this, args); 76 | } 77 | 78 | /*! 79 | * # #toString() 80 | * 81 | * Provides a sane toString default. 82 | */ 83 | 84 | Model.toString = function () { 85 | return '[object Model]'; 86 | }; 87 | 88 | /** 89 | * # Model.extend(type, proto, class) 90 | * 91 | * Helper to provide a simple way to extend the prototype 92 | * of a model. 93 | * 94 | * @param {String} type name 95 | * @param {Object} prototype definition 96 | * @returns {Model} model constructor modified 97 | * @api public 98 | */ 99 | 100 | Model.extend = function (type, proto) { 101 | if (!(type && 'string' === typeof type)) { 102 | klass = proto; 103 | proto = type; 104 | type = 'model'; 105 | } 106 | 107 | var opts = {}; 108 | klass = klass || {}; 109 | proto = proto || {}; 110 | 111 | if (proto.store) { 112 | opts.store = proto.store; 113 | delete proto.store; 114 | } 115 | 116 | if (proto.schema) { 117 | opts.schema = proto.schema; 118 | delete proto.schema; 119 | } 120 | 121 | // create our child 122 | var self = this 123 | , child = function () { self.apply(this, arguments); }; 124 | 125 | inherits.merge([ child, this ]); 126 | inherits.inherits(child, this); 127 | if (proto) inherits.merge([ child.prototype, proto ]); 128 | child.extend = this.extend; 129 | child.prototype.__type = type; 130 | 131 | // custom initiliaze to pass on flags 132 | child.prototype.initialize = function () { 133 | var options = arguments[0] || {}; 134 | if (!options.type) this.flag('type', this.__type, true); 135 | if (!options.store && opts.store) this.store = opts.store 136 | if (!options.schema && opts.schema) this.schema = opts.schema; 137 | delete this.__type; 138 | if (proto.initialize) proto.initialize.apply(this, arguments); 139 | }; 140 | 141 | return child; 142 | }; 143 | 144 | /*! 145 | * Inherit from drip 146 | */ 147 | 148 | inherits(Model, EventEmitter); 149 | 150 | /*! 151 | * # initialize 152 | * 153 | * noop - Function to be called upon construction. 154 | */ 155 | 156 | Model.prototype.initialize = function () {}; 157 | 158 | /** 159 | * # attributes (getter) 160 | */ 161 | 162 | Object.defineProperty(Model.prototype, 'attributes', 163 | { get: function () { 164 | var attrs = this._attributes; 165 | return attrs; 166 | } 167 | , configurable: true 168 | }); 169 | 170 | /** 171 | * # id (getter/setter) 172 | * 173 | * Sets or gets the current model's `_id`. 174 | * 175 | * @param {Integer|String} id 176 | * @api public 177 | */ 178 | 179 | Object.defineProperty(Model.prototype, 'id', 180 | { get: function () { 181 | var id = this.get('_id'); 182 | if (!id) id = this.uid; 183 | return id; 184 | } 185 | , set: function (id) { 186 | this.set('_id', id); 187 | } 188 | }); 189 | 190 | /** 191 | * # type (getter) 192 | * 193 | * Gets the flag for the current type. 194 | * 195 | * @param {String} type 196 | * @api public 197 | */ 198 | 199 | Object.defineProperty(Model.prototype, 'type', 200 | { get: function () { 201 | var type = this.flag('type'); 202 | if ('undefined' === typeof type) { 203 | type = 'model'; 204 | this.flag('type', type, true); 205 | } 206 | return type; 207 | } 208 | }); 209 | 210 | /** 211 | * # schema (getter/setter) 212 | * 213 | * Sets or gets the flag for the current schema. 214 | * Also makes sure that a schema has `_id` as an 215 | * index upon set. 216 | * 217 | * @param {Seed.Schema} schema 218 | * @api public 219 | */ 220 | 221 | Object.defineProperty(Model.prototype, 'schema', 222 | { get: function () { 223 | var schema = this.flag('schema'); 224 | return schema; 225 | } 226 | , set: function (schema) { 227 | if (schema instanceof Schema) { 228 | if (!schema.paths.has('_id')) { 229 | schema.paths.set('_id', { 230 | type: Schema.Type.ObjectId 231 | , index: true 232 | }); 233 | } else { 234 | var def = schema.paths.get('_id'); 235 | def.index = true; 236 | schema.paths.set('_id', def); 237 | } 238 | this.flag('schema', schema, true); 239 | } 240 | } 241 | }); 242 | 243 | /** 244 | * # DBRef 245 | * 246 | * Helper for standardized DBRef. Based on MongoDB. 247 | * 248 | * @api public 249 | */ 250 | 251 | Object.defineProperty(Model.prototype, 'DBRef', 252 | { get: function () { 253 | return { 254 | $ref: this.type 255 | , $id: this.id 256 | } 257 | } 258 | }); 259 | 260 | /** 261 | * # parent (getter/setter) 262 | * 263 | * Sets or gets the flag for the current parent. 264 | * 265 | * @param {Object} parent 266 | * @api public 267 | */ 268 | 269 | Object.defineProperty(Model.prototype, 'parent', 270 | { get: function () { 271 | var parent = this.flag('parent'); 272 | return parent; 273 | } 274 | , set: function (parent) { 275 | this.flag('parent', parent, true); 276 | } 277 | }); 278 | 279 | /** 280 | * # store (getter/setter) 281 | * 282 | * Sets or gets the flag for the current store. 283 | * 284 | * @param {Seed.Store} store 285 | * @api public 286 | */ 287 | 288 | Object.defineProperty(Model.prototype, 'store', 289 | { get: function () { 290 | var store = this.flag('store'); 291 | if (store) return store; 292 | var parent = this.flag('parent'); 293 | if (parent) return parent.store; 294 | return null; 295 | } 296 | , set: function (store) { 297 | if (store instanceof Store) 298 | this.flag('store', store, true); 299 | } 300 | }); 301 | 302 | /** 303 | * # toString 304 | * 305 | * returns a javascript string describing the 306 | * current model object 307 | * 308 | * @api public 309 | */ 310 | 311 | Model.prototype.toString = function () { 312 | var type = this.type.charAt(0).toUpperCase() + this.type.slice(1); 313 | return '[object ' + type + ']'; 314 | }; 315 | 316 | /** 317 | * # flag 318 | * 319 | * Flags are key:values that represent the models 320 | * state but are not syncronized with the server. 321 | * 322 | * ### Internal Flags 323 | * 324 | * - {Boolean} `new` 325 | * - {Boolaan} `dirty` 326 | * - {String} `type` 327 | * - {Seed.Schema} `schema` 328 | * - {Seed.Store} `store` 329 | * - {Object} `parent` 330 | * 331 | * 332 | * ### Example 333 | * 334 | * model.flag('key'); // to get a value 335 | * model.flag('key', true); // to set a value 336 | * model.flag([ 'key1', 'key2' ], 'stringed'); 337 | * 338 | * @param {String} key 339 | * @param {Mixed} value 340 | * @param {Boolean} silent 341 | * @api public 342 | * @name .flag() 343 | */ 344 | 345 | Model.prototype.flag = function (key, value, silent) { 346 | silent = ('boolean' === typeof silent) ? silent : false; 347 | var self = this 348 | , flags = this._flags || (this._flags = new Hash()); 349 | 350 | if (arguments.length === 1) { 351 | return flags.get(key); 352 | } else if (arguments.length > 1 && Array.isArray(key)) { 353 | key.forEach(function (elem) { 354 | flags.set(elem, value); 355 | if (!silent) self.emit([ 'flag', elem ], value); 356 | }); 357 | } else if (arguments.length > 1) { 358 | flags.set(key, value); 359 | if (!silent) this.emit([ 'flag', key ], value); 360 | } 361 | }; 362 | 363 | /** 364 | * # serialize 365 | * 366 | * Get a string of json representing the model attributes. 367 | * 368 | * @api public 369 | */ 370 | 371 | Model.prototype.serialize = function () { 372 | return JSON.stringify(this._attributes); 373 | }; 374 | 375 | /** 376 | * # .get() 377 | * 378 | * Return the value of an attribute. 379 | * 380 | * @param {String} property 381 | * @api public 382 | */ 383 | 384 | Model.prototype.get = function (prop) { 385 | return Query.getPathValue(prop, this._attributes); 386 | }; 387 | 388 | /** 389 | * # .has() 390 | * 391 | * Returns whether an attribute is defined. 392 | * 393 | * @param {String} property path 394 | * @api public 395 | */ 396 | 397 | Model.prototype.has = function (prop) { 398 | return 'undefined' !== typeof Query.getPathValue(prop, this._attributes); 399 | }; 400 | 401 | /** 402 | * # .set() 403 | * 404 | * Set the value of a set of attributes. Attributes 405 | * will be merged with existing attributes. 406 | * 407 | * #### Options 408 | * 409 | * * _force_ {Boolean} force the operation 410 | * 411 | * @param {String} path 412 | * @param {Mixed} value 413 | * @param {Object} options 414 | * @returns {Boolean} 415 | * @api public 416 | */ 417 | 418 | Model.prototype.set = function(path, value, opts) { 419 | opts = opts || {}; 420 | 421 | if (!opts.force && this.schema) { 422 | var valid = this.schema.validatePath(path, value); 423 | if (!valid) return false 424 | } 425 | 426 | Query.setPathValue(path, value, this._attributes); 427 | this.flag('dirty', true); 428 | return true; 429 | }; 430 | 431 | /** 432 | * # .merge() 433 | * 434 | * Merge given attributes. 435 | * 436 | * ### Example 437 | * 438 | * model.merge({ hello: 'world' }); 439 | * model.merge({ open: { source: 'always' }}); 440 | * 441 | * #### Options 442 | * 443 | * * _force_ {Boolean} force the operation 444 | * 445 | * @param {Object} attributes 446 | * @param {Object} options 447 | * @returns {Boolean} 448 | * @api public 449 | */ 450 | 451 | Model.prototype.merge = function (attrs, opts) { 452 | var oldProps = _.merge({}, this._attributes) 453 | , newProps = _.merge(oldProps, attrs); 454 | 455 | opts = opts || {}; 456 | 457 | if (!opts.force && this.schema) { 458 | var valid = this.schema.validate(newProps); 459 | if (!valid) return false; 460 | newProps = this.schema.getValue(newProps, { preserve: true }); 461 | } 462 | 463 | this._attributes = newProps; 464 | this.flag('dirty', true); 465 | return true; 466 | }; 467 | 468 | Model.prototype.validate = function () { 469 | if (!this.schema) return undefined; 470 | return this.schema.validate(this._attributes); 471 | }; 472 | 473 | /** 474 | * # .save() 475 | * 476 | * Save the current attributes of the model to the 477 | * storage engine. Can execute a `callback` on completion. 478 | * The `this` context will be the model that was just saved. 479 | * 480 | * 481 | * #### Options 482 | * 483 | * * _silent_ {Boolean} emit change events 484 | * 485 | * @param {Object} options 486 | * @param {Function} callback to be executed on compltion 487 | * @api public 488 | */ 489 | 490 | Model.prototype.save = function (opts, cb) { 491 | if ('function' === typeof opts) cb = opts, opts = {}; 492 | cb = cb || noop; 493 | opts = opts || {}; 494 | 495 | if (!this.store) return cb(errors.create('no store')); 496 | 497 | if (!opts.force 498 | && this.schema 499 | && !this.schema.validate(this._attributes)) 500 | return cb(errors.create('not valid')); 501 | 502 | this.store 503 | .sync('set', this) 504 | .then(storeSuccess.call(this, opts, cb), cb); 505 | }; 506 | 507 | /** 508 | * # .fetch() 509 | * 510 | * Get the current model state from the storage engine. Requires that an id is assigned 511 | * as an attribute to lookup. Can execute `callback` on completions. The `this` context 512 | * will be the model that was just fetched. 513 | * 514 | * #### Options 515 | * 516 | * * _silent_ {Boolean} whether to emit `change` events (such as the assign of an id if new) 517 | * 518 | * @param {Object} options 519 | * @param {Function} callback to be executed on compltion 520 | * @api public 521 | */ 522 | 523 | Model.prototype.fetch = function (opts, cb) { 524 | if (opts && 'function' === typeof opts) cb = opts, opts = {}; 525 | cb = cb || noop; 526 | opts = opts || {}; 527 | 528 | if (!this.store) return cb(errors.create('no store')); 529 | 530 | this.store.sync('get', this) 531 | .then(storeSuccess.call(this, opts, cb), cb) 532 | }; 533 | 534 | function storeSuccess (opts, cb) { 535 | var self = this 536 | , cache = _.merge({}, this._attributes); 537 | 538 | return function success (data) { 539 | if (!data) return cb(errors.create('no data')); 540 | 541 | var valid = self.merge(data, { 542 | silent: (opts.silent || false) 543 | , force: (opts.force || false) 544 | }); 545 | 546 | if (!valid) return cb(errors.create('not valid')); 547 | 548 | if (self.schema) { 549 | self.schema.paths.each(function (def, path) { 550 | if (!def.type) console.log(def); 551 | if (def.type.name !== 'DBRef') return; 552 | var prev = Query.getPathValue(path, cache) 553 | , res = Query.getPathValue(path, self._attributes); 554 | if (prev instanceof Model 555 | && prev.type === res.$ref 556 | && prev.id === res.$id) 557 | Query.setPathValue(path, prev, self._attributes); 558 | }); 559 | } 560 | 561 | self.flag('new', false); 562 | self.flag('dirty', false); 563 | cb(null); 564 | } 565 | } 566 | 567 | /** 568 | * # .destroy() 569 | * 570 | * Delete the model from the storage engine. A `destroy` event will emit and `destroyed` 571 | * flag will be set on successful completion. Can execute callback in which the `this` context 572 | * is the model that was just destroyed. 573 | * 574 | * Will remove itself from any graphs it is a part of on `success`. 575 | * 576 | * #### Options 577 | * 578 | * * **silent** {Boolean} whether to emit `change` events (such as the assign of an id if new) 579 | * 580 | * @param {Object} options 581 | * @param {Function} callback to be executed on compltion 582 | * @api public 583 | */ 584 | 585 | Model.prototype.destroy = function (opts, cb) { 586 | if ('function' === typeof opts) cb = opts, opts = {}; 587 | cb = cb || noop; 588 | opts = opts || {}; 589 | 590 | if (!this.store) return cb(errors.create('no store')); 591 | 592 | var self = this; 593 | function success () { 594 | if (self.parent && self.parent.del) 595 | self.parent.del(self.type, self.id); 596 | self.flag('destroyed', true); 597 | self.flag('new', false); 598 | self.flag('dirty', false); 599 | cb(null); 600 | }; 601 | 602 | this.store.sync('destroy', this) 603 | .then(success, cb); 604 | }; 605 | 606 | Model.prototype.loadRef = function (ref, cb) { 607 | cb = cb || noop; 608 | if (!this.store) return cb(errors.create('no store')); 609 | if (this.get(ref) instanceof Model) return cb(null); 610 | 611 | var self = this 612 | , vert = this.get(ref); 613 | 614 | if (!vert || !vert.$id || !vert.$ref) 615 | return cb(errors.create('no dbref')); 616 | 617 | var id = vert.$id 618 | , type = vert.$ref 619 | , meta; 620 | 621 | if (type == this.type) { 622 | meta = this.constructor; 623 | } else { 624 | var graph = this.flag('parent'); 625 | meta = graph._models.get(type); 626 | } 627 | 628 | if (!meta) return cb(errors.create('no type')); 629 | 630 | var opts = graph ? { parent: graph } : {} 631 | , model = new meta({ _id: id }, opts); 632 | 633 | model.fetch(function (err) { 634 | if (err) return cb(err); 635 | self.set(ref, model); 636 | if (graph) graph.set(model); 637 | cb(null); 638 | }); 639 | }; 640 | -------------------------------------------------------------------------------- /lib/seed/graph.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Seed :: Graph 3 | * Copyright(c) 2011-2012 Jake Luer 4 | * MIT Licensed 5 | */ 6 | 7 | /*! 8 | * External module dependancies 9 | */ 10 | 11 | var inherits = require('super'); 12 | 13 | /*! 14 | * Seed component dependancies 15 | */ 16 | 17 | var _ = require('./utils') 18 | , async = require('./base/async') 19 | , errors = require('./errors/graph') 20 | , EventEmitter = require('./base/events') 21 | , Hash = require('./base/hash') 22 | , Model = require('./model') 23 | , Promise = require('./base/promise') 24 | , Schema = require('./schema') 25 | , Store = require('./store'); 26 | 27 | /*! 28 | * Graph specific dependancies 29 | */ 30 | 31 | var helper = require('./graph/helpers') 32 | , Traversal = require('./graph/traversal'); 33 | 34 | /*! 35 | * Graph "constants" 36 | */ 37 | 38 | var noop = function () {}; 39 | 40 | /*! 41 | * Main export 42 | */ 43 | 44 | module.exports = Graph; 45 | 46 | /** 47 | * # Graph (constructor) 48 | * 49 | * Creates a new graph with a given set of options 50 | * 51 | * #### Options 52 | * - **store** - Seed compatible storage engine to over-ride all 53 | * previously defined storage engines 54 | * - **type** - Override the graph type string. Used for storage. 55 | * 56 | * @param {Object} options 57 | * @api public 58 | * @name constructor 59 | */ 60 | 61 | function Graph (options) { 62 | // setup 63 | EventEmitter.call(this); 64 | options = options || {}; 65 | 66 | // config 67 | this._models = new Hash(); 68 | this._vertices = new Hash(null, { findRoot: '_attributes' }); 69 | this._edges = new Hash(null, { findRoot: '_attributes' }); 70 | 71 | if (options.store) this.store = options.store; 72 | if (options.type) this.flag('type', options.type, true); 73 | 74 | // init 75 | this.initialize.apply(this, arguments); 76 | } 77 | 78 | /*! 79 | * Inherits from Drip Event Emitter 80 | */ 81 | 82 | inherits(Graph, EventEmitter); 83 | 84 | /** 85 | * # Graph.extend(proto, class) 86 | * 87 | * Helper to provide a simple way to extend the prototype 88 | * of a model. 89 | * 90 | * @param {Object} prototype definition 91 | * @param {Object} class definition 92 | * @returns {Model} model constructor modified 93 | * @api public 94 | */ 95 | 96 | Graph.extend = function (type, proto, klass) { 97 | if (!(type && 'string' === typeof type)) { 98 | klass = proto; 99 | proto = type; 100 | type = 'graph'; 101 | } 102 | 103 | var opts = {}; 104 | klass = klass || {}; 105 | proto = proto || {}; 106 | 107 | if (proto.store) { 108 | opts.store = proto.store; 109 | delete proto.store; 110 | } 111 | 112 | // create our child 113 | var self = this 114 | , child = function () { self.apply(this, arguments); }; 115 | 116 | inherits.merge([ child, this ]); 117 | inherits.inherits(child, this); 118 | if (proto) inherits.merge([ child.prototype, proto ]); 119 | child.extend = this.extend; 120 | child.prototype.__type = type; 121 | 122 | // custom initialize to pass on flags 123 | child.prototype.initialize = function () { 124 | var options = arguments[0] || {}; 125 | delete this.__type; 126 | if (!options.type) this.flag('type', type, true); 127 | if (!options.store && opts.store) this.flag('store', opts.store, true); 128 | if (proto.initialize) proto.initialize.apply(this, arguments); 129 | }; 130 | 131 | return child; 132 | } 133 | 134 | /*! 135 | * # #toString() 136 | * 137 | * Provides a sane toString default. 138 | */ 139 | 140 | Graph.toString = function () { 141 | return '[object Graph]'; 142 | }; 143 | 144 | /*! 145 | * # initialize 146 | * 147 | * noop - Function to be called upon construction. 148 | */ 149 | 150 | Graph.prototype.initialize = function () {}; 151 | 152 | /** 153 | * # type (getter) 154 | * 155 | * Gets the flag for the current type. 156 | * 157 | * @param {String} type 158 | * @api public 159 | */ 160 | 161 | Object.defineProperty(Graph.prototype, 'type', 162 | { get: function () { 163 | var type = this.flag('type'); 164 | if ('undefined' === typeof type) { 165 | type = 'graph'; 166 | this.flag('type', type, true); 167 | } 168 | return type; 169 | } 170 | }); 171 | 172 | /** 173 | * # types 174 | * 175 | * Returns array of all defined model types. 176 | * 177 | * @returns {Array} types of models defined 178 | * @api public 179 | * @name types 180 | */ 181 | 182 | Object.defineProperty(Graph.prototype, 'types', 183 | { get: function () { 184 | return this._models.keys; 185 | } 186 | }); 187 | 188 | /** 189 | * # length 190 | * 191 | * Returns number of objects in graph. 192 | * 193 | * @returns {Number} count of objects 194 | * @api public 195 | * @name length 196 | */ 197 | 198 | Object.defineProperty(Graph.prototype, 'length', 199 | { get: function () { 200 | return this._vertices.length; 201 | } 202 | }); 203 | 204 | /** 205 | * # store (getter/setter) 206 | * 207 | * Sets or gets the flag for the current store. 208 | * 209 | * @param {Seed.Store} store 210 | * @api public 211 | */ 212 | 213 | Object.defineProperty(Graph.prototype, 'store', 214 | { get: function () { 215 | var store = this.flag('store'); 216 | return store; 217 | } 218 | , set: function (store) { 219 | if (store instanceof Store) 220 | this.flag('store', store, true); 221 | } 222 | }); 223 | 224 | /** 225 | * # toString 226 | * 227 | * returns a javascript string describing the 228 | * current graph object 229 | * 230 | * @api public 231 | */ 232 | 233 | Graph.prototype.toString = function () { 234 | var type = this.type.charAt(0).toUpperCase() + this.type.slice(1); 235 | return '[object ' + type + ']'; 236 | }; 237 | 238 | /** 239 | * # flag 240 | * 241 | * Flags are key:values that represent the models 242 | * state but are not syncronized with the server. 243 | * 244 | * ### Example 245 | * 246 | * graph.flag('key'); // to get a value 247 | * graph.flag('key', true); // to set a value 248 | * graph.flag([ 'key1', 'key2' ], 'stringed'); 249 | * 250 | * @param {String} key 251 | * @param {Mixed} value 252 | * @param {Boolean} silent 253 | * @api public 254 | * @name .flag() 255 | */ 256 | 257 | Graph.prototype.flag = function (key, value, silent) { 258 | silent = ('boolean' === typeof silent) ? silent : false; 259 | var self = this 260 | , flags = this._flags || (this._flags = new Hash()); 261 | 262 | if (arguments.length === 1) { 263 | return flags.get(key); 264 | } else if (arguments.length > 1 && Array.isArray(key)) { 265 | key.forEach(function (elem) { 266 | flags.set(elem, value); 267 | if (!silent) self.emit([ 'flag', elem ], value); 268 | }); 269 | } else if (arguments.length > 1) { 270 | flags.set(key, value); 271 | if (!silent) this.emit([ 'flag', key ], value); 272 | } 273 | }; 274 | 275 | /** 276 | * # .define([type], definition) 277 | * 278 | * Define a `type` of model that can be added to current instance of 279 | * Graph. Accepts Models, Schemas, or Schema Definitions. 280 | * 281 | * @param {String} type optional if providing a model 282 | * @param {Model|Schema|Object} definition 283 | * @api public 284 | * @name .define() 285 | */ 286 | 287 | Graph.prototype.define = function (type, object) { 288 | var model; 289 | 290 | if (!object) { 291 | object = type; 292 | type = object.type || null; 293 | } 294 | 295 | // First we check to see if the object passed 296 | // is a model defintion, as from Model.extend. 297 | if (object.toString() == '[object Model]') { 298 | if (!type && object.prototype.__type != 'model') { 299 | type = object.prototype.__type; 300 | } else if (!type && object.type == 'model') { 301 | throw new Error('Graph#define - invalid type from model'); 302 | } 303 | 304 | model = object; 305 | 306 | // Next we check to see if the object passed 307 | // is an instance of Schema, as from new Schema(). 308 | } else if (object instanceof Schema) { 309 | model = Model.extend(type, { 310 | schema: object 311 | }); 312 | 313 | // Next we check to see if the object passed 314 | // is a plain js object, expected to be a Schema definition 315 | } else if (object && 'function' !== typeof object && Object(object) == object) { 316 | model = Model.extend(type, { 317 | schema: new Schema(object) 318 | }); 319 | 320 | // Finally, we don't have a match so this whole 321 | // define process was just bust. Error! 322 | } else { 323 | throw new Error('Seed#Graph - Unable to define type [' + type + '] from object: ' + object.toString()); 324 | } 325 | 326 | this._models.set(type, model); 327 | }; 328 | 329 | /** 330 | * # .set(address, attributes, [options]) 331 | * 332 | * Helper function to set values for a given model. 333 | * 334 | * #### Options 335 | * * **silent** - emit change events, defaults true 336 | * 337 | * @param {String} address `/type/_id` 338 | * @param {Object} attributes to change 339 | * @param {Object} options 340 | * @api public 341 | * @name .set() 342 | */ 343 | 344 | Graph.prototype.set = function (type, id, attributes, opts) { 345 | if (type instanceof Model) id = type, type = id.type; 346 | 347 | var model = this._models.get(type) 348 | , isNew = true 349 | , options = _.defaults(opts || {}, { dirty: true, silent: false }); 350 | 351 | if (!model) throw errors.create('no type'); 352 | if (!id) throw errors.create('no id'); 353 | 354 | if (id instanceof Model) { 355 | var object = id 356 | , address = '/' + type + '/' + object.id; 357 | object.flag('parent', this); 358 | id = object.id; 359 | } else { 360 | if ('object' == typeof id) { 361 | attributes = id; 362 | id = attributes._id; 363 | } 364 | 365 | attributes = attributes || {}; 366 | var address = '/' + type + '/' + id 367 | , object; 368 | if (id) object = this._vertices.get(address); 369 | if (!object) { 370 | object = new model({}, { parent: this }); 371 | object.merge(attributes, { silent: true }); 372 | if (id) object.id = id; 373 | address = '/' + type + '/' + object.id; 374 | } else { 375 | this._vertices.del(address); 376 | object.merge(attributes); 377 | isNew = false; 378 | } 379 | } 380 | 381 | this._vertices.set(address, object); 382 | object.flag('type', type, options.silent); 383 | object.flag('dirty', options.dirty, options.silent); 384 | 385 | if (!options.silent) { 386 | var ev = ((isNew) ? 'add' : 'change'); 387 | this.emit([ ev, type, object.id ], object); 388 | if (object.flag('dirty')) this.emit([ 'dirty', type, object.id ], object); 389 | } 390 | 391 | return object; 392 | }; 393 | 394 | /** 395 | * # .get(type, id) 396 | * 397 | * Get model of a specific type with `id`. 398 | * 399 | * @param {String} type name 400 | * @param {Mixed} id 401 | * @returns {Model} 402 | * @api public 403 | * @name .get() 404 | */ 405 | 406 | Graph.prototype.get = function (type, id) { 407 | var address = '/' + type + '/' + id; 408 | return this._vertices.get(address); 409 | }; 410 | 411 | /** 412 | * # .has(type, id) 413 | * 414 | * Determine whether an id of type exists in the graph. 415 | * 416 | * @param {String} type name 417 | * @param {Mixed} id 418 | * @returns {Boolean} exists 419 | * @api public 420 | * @name .has() 421 | */ 422 | 423 | Graph.prototype.has = function (type, id) { 424 | if (type instanceof Model) { 425 | var model = type; 426 | type = model.type; 427 | id = model.id; 428 | } 429 | 430 | var address = '/' + type + '/' + id 431 | , exists = this._vertices.get(address); 432 | return ('undefined' !== typeof exists) ? true : false; 433 | }; 434 | 435 | /** 436 | * # .del(address) 437 | * 438 | * Delete a model from the graph at a given address. Does not 439 | * sent delete command to storage engine. 440 | * 441 | * @param {String} address in the form `/type/_id` 442 | * @api public 443 | * @name .del() 444 | */ 445 | 446 | Graph.prototype.del = function (type, id) { 447 | if (type instanceof Model) { 448 | var model = type; 449 | type = model.type; 450 | id = model.id; 451 | } 452 | 453 | var address = '/' + type + '/' + id; 454 | this._vertices.del(address); 455 | }; 456 | 457 | 458 | Graph.prototype.relate = function (vertX, vertY, rel, attrs) { 459 | if (!arguments.length >= 3) throw Error('Missing fields for Graph#relate'); 460 | if (!this.has(vertX)) this.set(vertX); 461 | if (!this.has(vertY)) this.set(vertY); 462 | var edge = helper.buildEdge.apply(this, arguments); 463 | this._edges.set(edge.id, edge); 464 | return edge; 465 | }; 466 | 467 | Graph.prototype.unrelate = function (vertX, vertY, rel) { 468 | if (!arguments.length >= 3) throw Error('Missing fields for Graph#unrelate'); 469 | var edge = helper.getEdge.call(this, vertX, vertY, rel); 470 | this._edges.del(edge.id); 471 | }; 472 | 473 | Graph.prototype.traverse = function (opts) { 474 | var traverse = new Traversal(this, opts || {}); 475 | return traverse; 476 | }; 477 | 478 | /** 479 | * # .each([type, iterator, context) 480 | * 481 | * Apply an iteration function to all instanciated models in the graph. Or, 482 | * optionally, to all instanciated models of a given type. 483 | * 484 | * @param {String} type optional 485 | * @param {Function} iterator 486 | * @param {Object} context to apply as `this` to iteration function 487 | * @api public 488 | * @name .each() 489 | */ 490 | 491 | Graph.prototype.each = function (type, iterator, context) { 492 | // refactor params 493 | if (type && 'function' === typeof type) { 494 | context = iterator; 495 | iterator = type; 496 | type = null; 497 | } 498 | 499 | // ensure context 500 | context = context || this; 501 | 502 | // build hash to iterate 503 | var filterType = function (model) { return model.type === type } 504 | , models = type 505 | ? this._vertices.filter(filterType) 506 | : this._vertices; 507 | 508 | // iterate 509 | models.each(iterator, context); 510 | }; 511 | 512 | /** 513 | * # .find (query) 514 | * 515 | * Find all objects matching a given query. Does not 516 | * descriminate based on type. See Hash#find for more 517 | * information. 518 | * 519 | * @param {Object} query 520 | * @returns {Hash} models that match. 521 | * @see Hash#find 522 | * @api public 523 | */ 524 | 525 | Graph.prototype.find = function (query) { 526 | return this._vertices.find(query); 527 | }; 528 | 529 | /** 530 | * # .filter (type) 531 | * 532 | * Filter provides a way to work with a subset of the 533 | * the graph. Can be used multiple ways: 534 | * 535 | * // type filter 536 | * var models = graph.filter('person'); 537 | * 538 | * // iteration filter 539 | * var models = graph.filter(function (m) { 540 | * return m.get('val') == true; 541 | * }); 542 | * 543 | * // combination 544 | * var models = graph.filter('person', function (m) { 545 | * return m.get('val') == true; 546 | * }); 547 | * 548 | * @param {String} type 549 | * @param {Function} iterator 550 | * @param {Object} context to interpret as this in iterator 551 | * @see Hash#filter 552 | * @api public 553 | */ 554 | 555 | Graph.prototype.filter = function (type, iterator, context) { 556 | // if no type defined 557 | if ('function' == typeof type) { 558 | context = iterator || this; 559 | iterator = type; 560 | return this._vertices.filter(iterator, context); 561 | 562 | // if type defined 563 | } else if ('string' == typeof type) { 564 | context = context || this; 565 | var models = this._vertices.filter(function (model) { 566 | return model.type == type; 567 | }); 568 | 569 | // check for iterator 570 | if ('function' == typeof iterator) 571 | return models.filter(iterator, context); 572 | else return models; 573 | 574 | // bad format 575 | } else { 576 | return undefined; 577 | } 578 | }; 579 | 580 | /** 581 | * # .flush([type]) 582 | * 583 | * Remove all items (of optional type) from the graph. This 584 | * does not delete on the server. 585 | * 586 | * @param {String} type 587 | * @api public 588 | * name .flush() 589 | */ 590 | 591 | Graph.prototype.flush = function (type) { 592 | if (type) { 593 | var filterType = function (model) { return model.type !== type; } 594 | , models = this._vertices.filter(filterType); 595 | this._vertices = models; 596 | this.emit([ 'flush', type ]); 597 | } else { 598 | this._vertices = new Hash(null, { findRoot: '_attributes' }); 599 | this._edges = new Hash(null, { findRoot: '_attributes' }); 600 | this.emit([ 'flush', 'all' ]); 601 | } 602 | }; 603 | 604 | /** 605 | * # .push(options, callback) 606 | * 607 | * Perform a save action on all models in the graph. 608 | * Options are passed to a models `save` command. 609 | * 610 | * @param {Object} options 611 | * @param {Function} callback upon completion. (err) 612 | * @api public 613 | * @name .push() 614 | */ 615 | 616 | Graph.prototype.push = function (opts, cb) { 617 | // parse the parameters 618 | if (opts && 'function' === typeof opts) { 619 | cb = opts; 620 | opts = {}; 621 | } 622 | 623 | // sane defaults 624 | cb = cb || noop; 625 | opts = opts || {}; 626 | 627 | // self defintion & create useful callbacks 628 | var self = this 629 | , queueFn = function (fn, next) { fn(next); } 630 | , queue = async.queue(queueFn, 10) 631 | , resolved = function (next) { return function () { next(); } } 632 | , rejected = function (next) { return function (err) { next(err); } }; 633 | 634 | // convert model#save into promise 635 | function pushModel (model, opts) { 636 | var defer = new Promise(); 637 | model.save(opts, function (err) { 638 | if (err) return defer.reject(err); 639 | defer.resolve(); 640 | }); 641 | return defer.promise; 642 | } 643 | 644 | // Push `dirty` vertices to the server using model's save. 645 | this._vertices.each(function (model) { 646 | if (!model.flag('dirty')) return; 647 | queue.push(function (next) { 648 | pushModel(model, opts) 649 | .then(resolved(next), rejected(next)); 650 | }); 651 | }); 652 | 653 | // refresh lookups for vertices 654 | queue.push(function (next) { 655 | helper.refreshLookups.call(self, self._vertices); 656 | next(); 657 | }); 658 | 659 | // Push `dirty` edges to the server using model's save. 660 | this._edges.each(function (edge) { 661 | if (!edge.flag('dirty')) return; 662 | queue.push(function (next) { 663 | pushModel(edge, opts) 664 | .then(resolved(next), rejected(next)); 665 | }); 666 | }); 667 | 668 | // refresh lookups for edges 669 | queue.push(function (next) { 670 | helper.refreshLookups.call(self, self._edges); 671 | next(); 672 | }); 673 | 674 | // setup completion 675 | queue.drain = cb; 676 | queue.onerror = cb; 677 | queue.process(); 678 | }; 679 | 680 | 681 | /** 682 | * # .pull([options], callback) 683 | * 684 | * Perform a `fetch` on all models in the graph. Will 685 | * not pull in any new models. Options are passed to the model's 686 | * `fetch` command. 687 | * 688 | * @param {Object} options 689 | * @param {Function} callback upon completion. (error) 690 | * @api public 691 | * @name .pull() 692 | */ 693 | 694 | Graph.prototype.pull = function (opts, cb) { 695 | // parse the parameters 696 | if (opts && 'function' === typeof opts) { 697 | callback = opts; 698 | opts = {}; 699 | } 700 | 701 | // sane defaults 702 | cb = cb || noop; 703 | opts = opts || {}; 704 | 705 | // self definition & create useful callbacks 706 | var self = this 707 | , queueFn = function (fn, next) { fn(next); } 708 | , queue = async.queue(queueFn, 10) 709 | , resolved = function (next) { return function () { next(); } } 710 | , rejected = function (next) { return function (err) { next(err); } } 711 | 712 | // convert model#fetch to promise 713 | function pullModel (model, opts) { 714 | var defer = new Promise(); 715 | model.fetch(opts, function (err) { 716 | if (err) return defer.reject(err); 717 | defer.resolve(); 718 | }); 719 | return defer.promise; 720 | } 721 | 722 | // Pull non-`new` vertices to the server using model's fetch. 723 | this._vertices.each(function (model) { 724 | if (model.flag('new') && !opts.force) return; 725 | queue.push(function (next) { 726 | pullModel(model, opts) 727 | .then(resolved(next), rejected(next)); 728 | }); 729 | }); 730 | 731 | // refresh lookups for vertices 732 | queue.push(function (next) { 733 | helper.refreshLookups.call(self, self._vertices); 734 | next(); 735 | }); 736 | 737 | // Pull non-`new` edges to the server using model's fetch. 738 | this._edges.each(function (edge) { 739 | if (edge.flag('new') && !opts.force) return; 740 | queue.push(function (next) { 741 | pullModel(edge, opts) 742 | .then(resolved(next), rejected(next)); 743 | }); 744 | }); 745 | 746 | // refresh lookups for edges 747 | queue.push(function (next) { 748 | helper.refreshLookups.call(self, self._edges); 749 | next(); 750 | }); 751 | 752 | // setup completion 753 | queue.drain = cb; 754 | queue.onerror = cb; 755 | queue.process(); 756 | }; 757 | 758 | 759 | /** 760 | * # .fetch(type, query, callback) 761 | * 762 | * Submit a query to the storage engine for a given type and populate 763 | * the graph with the results. 764 | * 765 | * @param {String} type of model to fetch 766 | * @param {Object} query to pass directly to storage engine 767 | * @param {Function} callback upon completion. (error) 768 | * @api public 769 | * @name .fetch() 770 | */ 771 | 772 | Graph.prototype.fetch = function (type, query, cb) { 773 | // check params 774 | if (query && 'function' === typeof query) { 775 | cb = query; 776 | query = {}; 777 | } 778 | 779 | // sane defaults 780 | cb = cb || noop; 781 | query = query || {}; 782 | 783 | // check needed items 784 | if (!this.store) return cb(errors.create('no store')); 785 | if (!this._models.has(type)) return cb(errors.create('no type')); 786 | 787 | // params 788 | var self = this; 789 | 790 | // on success 791 | function success (data) { 792 | data.forEach(function(attrs) { 793 | self.set(type, attrs._id, attrs, { dirty: false }); 794 | }); 795 | cb(); 796 | }; 797 | 798 | // perform db action 799 | this.store.sync('fetch', { type: type }, query) 800 | .then(success, cb); 801 | }; 802 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.4.4 / 2012-07-25 3 | ================== 4 | 5 | * update package dependancies 6 | 7 | 0.4.3 / 2012-07-03 8 | ================== 9 | 10 | * recommit Hash#sortBy 11 | * add notes on traversal to readme 12 | * add readme link to kinetik 13 | 14 | 0.4.2 / 2012-06-28 15 | ================== 16 | 17 | * update breeze dependancy 18 | 19 | 0.4.1 / 2012-06-26 20 | ================== 21 | 22 | * replace SeedError with dragonfly errors 23 | 24 | 0.4.0 / 2012-06-25 25 | ================== 26 | 27 | * Merge branch 'refactor/basehash' 28 | * update all require references to hash to point to base hash 29 | * move hash to base as its no longer a core constructor 30 | * Merge branch 'feature/loadvert' 31 | * loadref can function without graph present if incoming model is of same type 32 | * add model `loadRef` as model dbref loader 33 | * Tests for #8 34 | * model construction forge merge. Closes #8. 35 | * model set/merge/save/fetch cleanup 36 | * Merge branch 'refactor/hash' 37 | * using sol as basis for hash 38 | * using sol 0.2.x 39 | * added sol dependancy 40 | * Merge branch 'refactor/use-super' 41 | * graph traversal and commands using super 42 | * schema type using super 43 | * base extenders using super 44 | * removed utilts/object/eextend in favor of super supported self extension 45 | * graph using super 46 | * model using super 47 | * store uses super 48 | * schema using super 49 | * hash using super instead of node.utils 50 | * add super dependancy 51 | * Merge branch 'refactor/flow' 52 | * fix bug blocking Graph#fetch 53 | * graph model uses breeze for flow control 54 | * graph edge/vertice traversal can support empty input 55 | * clean up select graph traversal 56 | * graph traverse edges refactored to use base 57 | * graph traversal vertices use proper flow 58 | * graph refactored to use queue from breeze 59 | * rename emptyFn to noop 60 | * using async nextTick in memorystore 61 | * remove old flow utils, include breeze as `async` as base/async, add to main export 62 | * use breeze as base/flow 63 | * add breeze, update filtr 64 | 65 | 0.3.5 / 2012-06-11 66 | ================== 67 | 68 | * queue flow utility calls callback immediately if queue is empty 69 | * refactor memory store to not error on non-existent group 70 | * test for empty traversal 71 | 72 | 0.3.4 / 2012-06-11 73 | ================== 74 | 75 | * update drip to 0.3.x 76 | * test for graph set using schema 77 | 78 | 0.3.3 / 2012-05-30 79 | ================== 80 | 81 | * added ObjectId schema type 82 | * MemoryStore is simulates async using nextTick 83 | * tests for Hash#reduce and Hash#mapReduce 84 | * Hash#mapReduce 85 | * Hash#reduce 86 | * bug - Graph#set wasn't setting object id before returning newly constructed model 87 | 88 | 0.3.2 / 2012-05-27 89 | ================== 90 | 91 | * graph fetched models pass into schema validation 92 | * bug - if model has schema, use schema#getValue to ensure correct format 93 | 94 | 0.3.1 / 2012-05-25 95 | ================== 96 | 97 | * clean npm ignore 98 | * bug - object schema type getValue returned circular reference 99 | 100 | 0.3.0 / 2012-05-25 101 | ================== 102 | 103 | * test coverage support 104 | * case sensitive #2 105 | * case sensitive 106 | * no longer checking versions 107 | * Merge branch 'feature/graph-theory' 108 | * refactor command vertices for proper callback structure 109 | * improved Queue flow helper 110 | * graph edge fetching uses DBRef schema type to ensure clean references 111 | * model schema detection typo 112 | * DBRef schema type supports bson format 113 | * refactor Queue flow utility to use start + callback 114 | * tests for in vertices live 115 | * refactor vertices traversal to use base and smarter concurrency 116 | * parallel flow bug 117 | * tests for live out vertices 118 | * refactored outVertices to support live traversal 119 | * refactored edge model fetch x/y methods 120 | * tests for live in Edges 121 | * allow a queue to be cancelled 122 | * Graph#traverse `inEdges` command supports live mode 123 | * finished tests for Graph#traverse `live` out edges command 124 | * graph helper fetchEdges now properly works with dbrefs 125 | * Store#sync abides to Model schemas 126 | * Graph edge compatible with object type and Schema#getValue 127 | * added Schema#getValue to allow for "prepared" model attributes 128 | * nested deepMerge bug fix for undefined properties 129 | * added schema type `object` tests 130 | * added schema type `object` 131 | * Schema Types all have #getValue helper 132 | * starting tests for live traversals 133 | * Graph traverse command outEdges can do live queries 134 | * relations example using memory store 135 | * Graph#flush also flushes edges if we are flushing everything 136 | * test for deep merging 137 | * deep merge utility 138 | * [bug] Graph#set using existing model needs to set parent 139 | * refactor Graph helper to fetch edges to support x/y/undef type of pulls 140 | * clean up Graph#each / flush 141 | * graph helpers comment header 142 | * [refactor] graph private functions into helper dependancy 143 | * traverse and should return instance of hash 144 | * static traversal tests 145 | * Graph#traverse doesn't automatically select 146 | * [bug] pull only pull non-new .. needed way to overwrite using option.force 147 | * Graph private helper functions :: getEdge and pullEdge 148 | * graph pull edges non-new 149 | * comment cleanup 150 | * [bug] fulfilled misspelled 151 | * DRY Graph `refreshLookups` 152 | * code styling for graph traversal 153 | * Merge branch 'feature/graph-theory' of github.com:qualiancy/seed into feature/graph-theory 154 | * starting graph traversal tests 155 | * [refactor] Graph#push // Graph#pull to use flow concurrency queue and refresh lookups 156 | * graph clean up 157 | * renamed test files for easier group running 158 | * refactored relations example 159 | * Added Traversal#flag. Refactored so parent is a flag. 160 | * util flow Queue tests 161 | * added util flow Queue 162 | * changed flow util `waterfall` to `concurrent` as it inappropriately named 163 | * update relations example 164 | * traverse command inEdges 165 | * traverse command outEdges 166 | * traverse command inVertices 167 | * traverse command loader 168 | * traverse command outVertices 169 | * Graph#traverse command 170 | * starting graph traversal. Traversal object 171 | * added async flow control utils 172 | * edge model loading vertices 173 | * [bug] Graph!#refreshLookups also cleans Graph#_edges lookups 174 | * [feature] Graph#relate / Graph#unrelate + Edge Model 175 | * [feature] Model#validate as public method for self schema validation 176 | * [bug] SchemaType#DBRef validates for returned dbref values 177 | * [refactor] Graph internally stores constructed models in `_vertices` 178 | * [feature] Schema.Type.DBRef include property getter `value` 179 | * [feature] added SchemaType.DBRef 180 | * starting graph traversal tests 181 | * [refactor] Graph#push // Graph#pull to use flow concurrency queue and refresh lookups 182 | * graph clean up 183 | * renamed test files for easier group running 184 | * refactored relations example 185 | * Added Traversal#flag. Refactored so parent is a flag. 186 | * util flow Queue tests 187 | * added util flow Queue 188 | * changed flow util `waterfall` to `concurrent` as it inappropriately named 189 | * update relations example 190 | * traverse command inEdges 191 | * traverse command outEdges 192 | * traverse command inVertices 193 | * traverse command loader 194 | * traverse command outVertices 195 | * Graph#traverse command 196 | * starting graph traversal. Traversal object 197 | * added async flow control utils 198 | * edge model loading vertices 199 | * [bug] Graph!#refreshLookups also cleans Graph#_edges lookups 200 | * [feature] Graph#relate / Graph#unrelate + Edge Model 201 | * [feature] Model#validate as public method for self schema validation 202 | * [bug] SchemaType#DBRef validates for returned dbref values 203 | * [refactor] Graph internally stores constructed models in `_vertices` 204 | * [feature] Schema.Type.DBRef include property getter `value` 205 | * [feature] added SchemaType.DBRef 206 | 207 | 0.2.5-1 / 2012-04-27 208 | ================== 209 | 210 | * Model.store getter checks parent too 211 | 212 | 0.2.5 / 2012-04-27 213 | ================== 214 | 215 | * [bug] Schema#validatePath was throwing a reference error 216 | * Merge branch 'feature/graph-theory' 217 | * [refactor] file restructure 218 | * [refactor] code clean for Graph/Model 219 | * [refactor] Graph/Model # toString 220 | * [feature] Graph now has type (same implementation as model) 221 | * [refactor] clean up relation prototype 222 | * [feature] added Model.prototype.DBRef helper 223 | * [feature] basic relations Graph 224 | * [example] graph relationships 225 | * Merge branch 'feature/relations' 226 | * [refactor] file restructure 227 | * [refactor] code clean for Graph/Model 228 | * [refactor] Graph/Model # toString 229 | * [feature] Graph now has type (same implementation as model) 230 | * [refactor] clean up relation prototype 231 | * [feature] added Model.prototype.DBRef helper 232 | * [feature] basic relations Graph 233 | * [example] graph relationships 234 | * [feature] Model.attributes configurable getter 235 | * [bug] Model/Graph constructor correctly calls initialize with all arguments 236 | * [feature] Graph#del supports `graph.del(model_instance)` 237 | * [feature] Graph#has supports `graph.has(model_instance)` 238 | * [feature] Graph#set supports `graph.set(model_instance)` 239 | * [refactor] Graph#set events 240 | * [bug] Graph#define not setting correct object 241 | * [bug] EmbeddedSchema.validate referencing wrong value 242 | * [feature] support for nested schemas 243 | * [bug] schema required/indexes support nonboolean indicators 244 | 245 | 0.2.4 / 2012-04-11 246 | ================== 247 | 248 | * [clean] code cleanup 249 | * [bug] Graph using storage successfully 250 | * [refactor] Graph#extend to pass along store flag 251 | * [test] Graph#flag 252 | * [refactor] Graph#flags 253 | * [refactor] Schema#castAstype 254 | * [package] only node >= 0.6.x compatible now 255 | * [docs] schema comments 256 | * [bug] type noop setter 257 | * code cleanup 258 | * [refactor] model handles schema ensure index of `_id`. 259 | * [test] model test formatting 260 | * [refactor] model to use flags for storage of store, schema, type, parent 261 | * [test] Model#flag 262 | * [refactor] Model#flag 263 | * [test] added boolean schema type test 264 | * [feature] added boolean schema type 265 | * [bug] schema existence datum checks 266 | * Merge branch 'feature/schema-latlng' 267 | * [tests] geospatial schema type 268 | * [feature] added geospatial schema type 269 | 270 | 0.2.3 / 2012-03-26 271 | ================== 272 | 273 | * tests for Graph#each 274 | * [bug] graph#each crash with type 275 | 276 | 0.2.2 / 2012-03-14 277 | ================== 278 | 279 | * Docs cleanup. 280 | * Added Graph#has 281 | * added Model#has 282 | * schema validate path, model set uses Schema#validatePath 283 | 284 | 0.2.1 / 2012-03-09 285 | ================== 286 | 287 | * temporarily ignore nested schemas 288 | * graph refresh lookups on pull 289 | * refactor graph.set to support models 290 | * test for kid as id 291 | * model id getter shows kid if no _id defined 292 | * tweak schema _id index defaults 293 | * indexes are not required for schema validation 294 | * [bug] Model#save - failure not defined on save fail 295 | 296 | 0.2.0 / 2012-02-26 297 | ================== 298 | 299 | * few more tests and tweaks 300 | * tweak for Model toString() + tests 301 | * testing for custom error object 302 | * code cleanup for schema#validate 303 | * promise export mimics event emitter usage 304 | * utils imported as standard _ in hash 305 | * hash code cleaning 306 | * removed comparator helper export as now inline in hash 307 | * tests for refactored sort 308 | * refactored Hash#sort, added Hash#sortBy 309 | * added Hash#clean & Hash#flush 310 | * graph tests for filter 311 | * inline comments for graph filter 312 | * refactor Graph#filter 313 | * refactor Hash#select as Hash#filter 314 | * schema type tests compatible with id required 315 | * schema forces _id as index 316 | * remove unused schema functions 317 | * graph flag type getter 318 | * graph test flag events 319 | * test-cov / lib-cov in makefile are phony 320 | * tests 0.4.x compatible 321 | * accurate keys/values getters for hash 322 | * clean tests 323 | * hash length & set performance tweak 324 | * ; 325 | * refactor memory store for better remove performance 326 | * refactor memorystore bench 327 | * update matcha 328 | * crystal is cleaner 329 | * more tests 330 | * model tests for construction and get/set 331 | * added chai spies 332 | * model type can't be changed after constructed 333 | * Merge branch 'feature/graph-set-refactor' 334 | * memory store tests compatible with new graph#set format 335 | * graph tests compatible with new set format 336 | * graph set is #set(type, id, attrs) 337 | * Merge branch 'feature/modelschema' 338 | * model cleanup 339 | * model uses schema validation when available 340 | * schema supports indexes 341 | * query helper 342 | * tests for new set 343 | * model set is no path based, old set is called merge 344 | * test coverage support 345 | * comments 346 | * using EventEmitter drip implementation instead of requiring drip for each component 347 | * replacing delete with 'set' undefined 348 | * hash benchmarks 349 | * filter version bump 350 | * comments 351 | * Schema uses event emitter 352 | * drip as Event emitter 353 | * Merge branch 'feature/_id' 354 | * changed `id` to `_id` 355 | * read me [ci-skip] 356 | * memory store tests compatible with `length` 357 | * graph uses `length` instead of count 358 | * memory store has more appropriate object store 359 | * graph tests length fix 360 | * graph flush fix 361 | * graph comments 362 | * model comments 363 | * model comments 364 | * utils comments 365 | * copyright notices 366 | * storage comment updates 367 | * hash#has implemented 368 | * proper storage for hash data 369 | * make bench 370 | * tests fixed for hash 371 | * all hash commenting 372 | * change reporter for tests 373 | * read me updates on upcoming releases 374 | * few simple tests 375 | * benchmark updates 376 | 377 | 0.1.12 / 2012-01-28 378 | ================== 379 | 380 | * removed all references to query, using 'filtr' 381 | * using filtr for querying 382 | * comment typos 383 | * remove filter tests 384 | 385 | 0.1.11 / 2012-01-24 386 | ================== 387 | 388 | * crystal OS license attribute 389 | * object id example 390 | * expose named objected generators 391 | * added base/bits objectid generator "crystal" 392 | * Model supports deep addresses for 'get' 393 | * [bug] SeedError opts misreference 394 | * Model#fetch returns ENOTFOUND if storage engine returns null 395 | * SeedError moves opts.code to this.code as shortcut 396 | * README typos 397 | * few readme updates 398 | 399 | 0.1.10 / 2012-01-11 400 | ================== 401 | 402 | * seed main exports rearranged 403 | * memory store uses oath directly 404 | * Drip as EventEmitter, with extend for easy API usage 405 | * git ignore vim swp 406 | 407 | 0.1.9 / 2012-01-09 408 | ================== 409 | 410 | * Merge branch 'feature/memorystore-graph' 411 | * added name to memory store for compatibility. 412 | * notes for memory store graph delete 413 | * more memory store tests 414 | * tests for memory store with graph 415 | * memory store fetch 416 | * graph#fetch defaults retrieved objects dirty to false 417 | * graph#set accepts options flag, but uses defaults 418 | * renamed ref to utils in graph to _ 419 | * find each hash had backwards iterator 420 | * memory store uses SeedError 421 | * tests for memory store conform to using collections 422 | * memory store uses collections 423 | 424 | 0.1.8 / 2012-01-09 425 | ================== 426 | 427 | * Merge branch 'feature/store-upgrades' 428 | * model destroy success function requires no return value 429 | * store checks version 430 | * error accepts opts, and displays such 431 | * store passes options to initialize 432 | * added server dep 433 | * query is not a primary component [ci skip] 434 | * few more read me tweaks [ci skip] 435 | * read me updates [ci-skip] 436 | * Merge branch 'feature/custom-error' 437 | * example for custom error 438 | * export custom error 439 | * Seed Custom erro 440 | 441 | 0.1.7 / 2012-01-07 442 | ================== 443 | 444 | * update tests for graph#find 445 | * tests for graph.find 446 | * Graph removed select added find 447 | * added hash opts 448 | * implement hash#find using query constructor 449 | * remove old filter constructor 450 | * query code cleanup 451 | * Merge branch 'feature/merge-queryfilter' 452 | * moved filter tests to query, adjusted for test opts 453 | * allowed options for test 454 | * combined filter + query into query, untested 455 | * merged traversing methods for filter into parse and test 456 | * query data in exec 457 | * removed eyes sep from query example 458 | * Merge branch 'feature/query-refactor' 459 | * added query example 460 | * added Query 461 | * added $eq filter 462 | * test filenames conform to new object names (Filter // filters) 463 | * refactor Query as Filter as it is more appropriate.. makes way for actual Querying. 464 | * read me has travis badge 465 | * added travis.yml 466 | * read me updates 467 | * update link in package.json 468 | 469 | 0.1.6 / 2012-01-03 470 | ================== 471 | 472 | * major file rearranging 473 | * Merge branch 'feature/hash-filtering' 474 | * hash find + test 475 | * remove eyes inspector 476 | * detailed testing for Query 477 | * query testing 478 | * basic query functionality 479 | * expected query from main export 480 | * all hash filters for now 481 | * most helpers and tests 482 | * started filters and test 483 | * moved comparator to helpers folder 484 | * tests for Graph.flush 485 | * Graph.flush for all or by type 486 | * tests for graph.select 487 | * graph.select supports string and regex 488 | * empty graph tests for memory store 489 | * graph tests cleanup 490 | * Merge branch 'feature/memorystore-refactor' 491 | * comment cleanup 492 | * tests completed for model crud operations for memory store 493 | * Merge branch 'feature/memorystore-refactor' of github.com:qualiancy/seed into feature/memorystore-refactor 494 | * memorystore constructor test 495 | * MemoryStore require of Store is correct 496 | * memory store using hash 497 | * cleanup memory store nextTick and Promise usage 498 | * Store provide default initialize function 499 | * memory story refactored to extend style 500 | * cleaner memory store requires 501 | * Graph: getters all use defineProperty 502 | * Hash: all getters are defineProperty and cleanup of whitespace 503 | * using node.js inherits 504 | * seed utils a _ in model 505 | * Model getters/setters use Object.defineProperty 506 | * correctly importing drip for Model 507 | * removing model chain api as not compatible with drip 0.2.x 508 | * memorystore constructor test 509 | * MemoryStore require of Store is correct 510 | * memory store using hash 511 | * cleanup memory store nextTick and Promise usage 512 | * Store provide default initialize function 513 | * memory story refactored to extend style 514 | * cleaner memory store requires 515 | * Merge branch 'feature/util-refactor' 516 | * moved utils to utils folder, added loader 517 | * Merge branch 'feature/store-refactor' 518 | * Store tests 519 | * Store is now drip delimited and provides Oath helper 520 | * store main export as Store, not _Store 521 | * empty tests for memory 522 | * switched out should.js for chai should interface 523 | * update deps 524 | 525 | 0.1.5 / 2011-12-12 526 | ================== 527 | 528 | * update dependancies 529 | 530 | 0.1.4 / 2011-12-06 531 | ================== 532 | 533 | * graph supports drip 0.2.0 534 | * hash supports drip 0.2.0 535 | * model supports drip 0.2.0 536 | * tests support drip 0.2.0 537 | * benchmark, not benchmarks (makefile) 538 | * drip update 539 | * Merge branch 'feature/model-tests' 540 | * removed sherlock completely 541 | * benchmarks `matcha` 0.0.2 compatible 542 | * flag code cleanup 543 | * Merge branch 'master' of github.com:logicalparadox/seed into feature/model-tests 544 | * start of benchmarks 545 | * (makefile) benchmarks 546 | * added matcha + first benchmarks 547 | * added model test 548 | 549 | 0.1.3 / 2011-12-04 550 | ================== 551 | 552 | * schema example supports changes to types 553 | * schema tests for required and nested data 554 | * schema improved handling of required and omitted data 555 | * Merge branch 'feature/schema-types' 556 | * schema types defined w/ basic tests 557 | * schema typecasting refactored 558 | 559 | 0.1.2 / 2011-12-04 560 | ================== 561 | 562 | * Merge branch 'feature/mochatests' 563 | * added keywords to package.json 564 | * graph tests rewritten in mocha 565 | * graph flags new objects with type 566 | * hash test descriptor 567 | * data folder for testing not needed 568 | * we prefer dot notation 569 | * hash tests rewritten in mocha 570 | * hash map/select include index in iteration 571 | * toJSON is now `serialize` to match model function 572 | * model chain uses util correctly 573 | * main exports util.flake as object id 574 | * added mocha 575 | * chain removed tea 576 | * Merge branch 'feature/rmtea' 577 | * remove tea from store and memorystore 578 | * schema no tea 579 | * Graph, all tea references removed 580 | * util.inherits…. not util.merge 581 | * Hash#keys & Hash#values are getters, not functions 582 | * model code cleanup based on flag refactor 583 | * model - remove references to tea 584 | * Flake (time based uid generation) in utils 585 | * comparator whitespace 586 | * remove tea from hash 587 | * added utils 588 | * model comments 589 | * model uses model.store or model.parent.store for all db operations 590 | * move model getters/setters to prototype 591 | * package.json description update 592 | 593 | 0.1.1 / 2011-12-04 594 | ================== 595 | 596 | * whitespace cleanup 597 | * schema example comments 598 | * readme cleanup 599 | * README updates 600 | * clean model#set 601 | * schema cleanup 602 | * readme updates 603 | * code cleanup 604 | * Hash#__getters__ moved to prototype 605 | * Model/Graph #flag supports using an array as key 606 | * renamed submodules as index in respective folders 607 | 608 | 0.1.0 / 2011-11-11 609 | ================== 610 | 611 | * remove Collection (obsolete) 612 | * node version package typo 613 | 614 | 0.0.11 / 2011-11-11 615 | ================== 616 | 617 | * Graph#pull 618 | * Graph#push 619 | * Graph#set supports empty attributes 620 | * Graph#fetch is functional 621 | * Graph#all returns clone 622 | * graph defines parent for all models within 623 | * model has more generic `parent` attribute 624 | * query support 625 | * Graph#fetch 626 | * improved sync with query object 627 | 628 | 0.0.10 / 2011-11-02 629 | ================== 630 | 631 | * model set uses attrs, not props 632 | * Graph rebuild step 1 … get, set, del, each 633 | * changing storage semantics 634 | * Seed#ObjectId shortcut 635 | * cleaner tests for hash/graph 636 | * Hash#each uses allows context definition 637 | * Model flags use Hash 638 | * Seed#model - getter/setter for id, better type handling 639 | * collection note to sefl 640 | * Graphs also stores objects in collections 641 | * Schema uses required (not: `require`) to note path is not optional 642 | * Collection#push 643 | * Collection#fetch functional 644 | * Collection#push saves all models currently in memory to store 645 | * Collection#_refreshLookups & Collection#add uses it 646 | * iterator in Collection#each has context = null 647 | * improved opts/callback detecting in Model#save 648 | * Collection#count getter 649 | * improved Collection#add / Collection#fetch 650 | * Collection#each 651 | * Better model path handling 652 | * Collection uses uid (not uuid) for models 653 | * Model UID now model.uid not model.uuid 654 | * Small readme updates 655 | * one last readme typo 656 | * readme tweak 657 | * Big README update :) 658 | 659 | 0.0.9 / 2011-10-25 660 | ================== 661 | 662 | * Merge branch 'feature/schema' 663 | * Merge branch 'master' of github.com:logicalparadox/seed into feature/schema 664 | * Merge branch 'feature/rmFileStore' 665 | * moved filestore to npm `seed-filestore` 666 | * moved UID generator to `tea` 667 | * Schema type validation for custom types. 668 | * basic validation 669 | * Seed#Schema - path detection 670 | * Merge branch 'master' of github.com:logicalparadox/seed 671 | * Hash#max 672 | * Hash#max 673 | * Hash#min 674 | * Hash#--getter AVG 675 | * Hash#--getter SUM 676 | * sherlock 0.1.6 compatible tests (assert.isEmpty) 677 | * cleaner Hash#set 678 | * comparators and Hash#sort + tests 679 | * hash#values cleanup 680 | * Hash#clone 681 | * All Hash function now use this.each instead of for loops 682 | 683 | 0.0.8 / 2011-10-21 684 | ================== 685 | 686 | * Merge branch 'feature/hash' 687 | * all basic tests pass 688 | * Hash#keys uses native Object.keys, doh 689 | * tests Hash each / map / select 690 | * cleaning Hash#select 691 | * fixed hash#index function 692 | * Hash tests and data fixtures 693 | * Hash # select, key, values cleanup 694 | * Hash#index getter 695 | * hash events emit index or key, not full data 696 | * Added Hash 697 | * everything moved around 698 | * no event emitted on graph.remove is object doesn't exist 699 | * Graph#remove + tests 700 | * drip updated 701 | * Gragh#test cleanup 702 | * Graph#get 703 | * Merge branch 'feature/graph-tests' 704 | * graph test written in sherlock 705 | * package - sherlock for testing 706 | * Merge branch 'feature/graph' 707 | * can add models to graphs 708 | * improved model typing, customized extend function 709 | * improved model typing, customized extend function 710 | * Merge branch 'master' into feature/graph 711 | * Merge branch 'feature/model-type' 712 | * model understands type 713 | * basic use model as schema 714 | * graph initialized correctly 715 | * graph initializing 716 | * get/set/destroy of model for filestore 717 | * storing in folder based on model or collection path 718 | * better storage selection upon sync 719 | * added filestore 720 | * models bug - uid now in utils 721 | * tea 0.1.0 compatibility 722 | * capitalization 723 | * model requires definition of storage 724 | * moving uid generation into seed 725 | 726 | 0.0.7 / 2011-10-14 727 | ================== 728 | 729 | * collection uses tea, not utils 730 | * model uses tea, not utils 731 | * store uses tea, not utils 732 | * rewrite model chain api for set/serialize to behave more like jquery chains 733 | * removed utils, added tea to package 734 | * Cleaning chain api and some documentation 735 | * storage naming convention 736 | * MemoryStore documentation 737 | * store documentation 738 | * store defaults to collection store 739 | * comment cleanup 740 | * model doc updates based on storage change 741 | * better storage handling 742 | * comments / docs for models 743 | 744 | 0.0.6 / 2011-10-05 745 | ================== 746 | 747 | * models can chain 748 | * adding oath 749 | * memory storage engine implementing seed standard error codes 750 | * all model storage actions converted to callbacks 751 | * collection tests have different them than models 752 | * save, fetch, destroy converted from oaths to callbacks 753 | * utils isFunction 754 | * using sherlock for extra testing 755 | * collection beginnings: add/remove 756 | * test spies use sherlock 757 | * models using new uuid generator 758 | * better uuid generation - ms time based 759 | * model is actually Model (caps) 760 | * utils no longer part of exports 761 | 762 | 0.0.5 / 2011-10-04 763 | ================== 764 | 765 | * memory#update 766 | * test cleanup 767 | * flag get false bugfix 768 | * model fetch, storage read 769 | * cleanup extra code 770 | * utils now has 'merge' and 'extend' 771 | * model.flag(key) & model.flag(key, value) 772 | * storage plugins are called engines 773 | * storage mod / memory transport support `remove` 774 | * model.destroy functional 775 | * model.save functional and tested 776 | * rewrite storage sync to work with oaths 777 | * convert transport/memory to promise (oath) 778 | * models get/set + events w/ tests 779 | * basics of storage engine 780 | * serialize models 781 | * unique id generator 782 | 783 | 0.0.4 / 2011-10-03 784 | ================== 785 | 786 | * refactored as model/collection factory 787 | * added drip 788 | * moving util extend to utils 789 | * npm ignore 790 | 791 | 0.0.3 / 2011-10-03 792 | ================== 793 | 794 | * constructor using unseeded function and tests 795 | 796 | 0.0.2 / 2011-10-03 797 | ================== 798 | 799 | * test formatting 800 | * define properties from constructor.prototype (allows after extend prototype changes) 801 | * package.json 802 | 803 | 0.0.1 / 2011-10-02 804 | ================== 805 | 806 | * bad constructor tests, tostring tests 807 | * basic object extends functionality and tests 808 | * initialize project 809 | --------------------------------------------------------------------------------