Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add event emitters to models and emit some interesting events. #10

Merged
merged 8 commits into from

1 participant

@ceejbot
Owner

Also, finish off the levelup adapter by implementing the last attachments features.

added some commits
@ceejbot Switched to emitting events for before/after um events instead of
testing for functions then calling them.

I'm using LucidJS as an event library just in case I ever try to make
this work in the browser.

Added unit tests for all triggered events.

Wrote a mock db and moved several tests from the couch adapter suite
to the generic persistence layer suite. There's more work to be done
there.

Bumped the version number in anticipation of release.
03ec12e
@ceejbot Documented events in the readme. 9fc0b61
@ceejbot Dead code removal: kill the last remnants of the initial redis attach…
…ment implementation.
b027c28
@ceejbot Formatting tweak. aef0f05
@ceejbot The levelup adapter now cleans up attachments properly.
Added unit tests to prove it. Or so I hope.

The default polyclay require no longer includes all the db adapters, so
you do not need to have them all built in order to use the package.
Right now you have to require the path inside the package directly.
I'll find some better way to do that.
13b7d20
@ceejbot Wrapped requires() for optional adapters in try/catch, so you don't need
to have everything installed.
091fedb
@ceejbot Emit change events when attachments are changed.
Added unit tests for the events.
e5804c4
@ceejbot Remove an unused require(). 70d09a9
@ceejbot ceejbot merged commit 5db4090 into master

1 check passed

Details default The Travis build passed
@ceejbot ceejbot deleted the evented branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 7, 2013
  1. Switched to emitting events for before/after um events instead of

    authored
    testing for functions then calling them.
    
    I'm using LucidJS as an event library just in case I ever try to make
    this work in the browser.
    
    Added unit tests for all triggered events.
    
    Wrote a mock db and moved several tests from the couch adapter suite
    to the generic persistence layer suite. There's more work to be done
    there.
    
    Bumped the version number in anticipation of release.
  2. Formatting tweak.

    authored
  3. The levelup adapter now cleans up attachments properly.

    authored
    Added unit tests to prove it. Or so I hope.
    
    The default polyclay require no longer includes all the db adapters, so
    you do not need to have them all built in order to use the package.
    Right now you have to require the path inside the package directly.
    I'll find some better way to do that.
  4. Emit change events when attachments are changed.

    authored
    Added unit tests for the events.
This page is out of date. Refresh to see the latest.
View
36 README.md
@@ -2,7 +2,7 @@
Polymer modeling clay for node.js. A model schema definition with type validations, dirty-state tracking, and rollback. Models are optionally persistable to CouchDB using [cradle](https://github.com/cloudhead/cradle), to [Redis](http://redis.io/), or to [LevelUP](https://github.com/rvagg/node-levelup). Polyclay gives you the safety of type-enforcing properties without making you write a lot of boilerplate.
-Current version: __1.2.3__
+Current version: __1.3.0__
[![Build Status](https://secure.travis-ci.org/ceejbot/polyclay.png)](http://travis-ci.org/ceejbot/polyclay)
@@ -294,14 +294,36 @@ obj.save(function(err, resp)
});
```
-### Before & after hooks
+## Events
-If you supply the following methods on your model class, they will be called when their names suggest:
+Polyclay uses [LucidJS](http://robertwhurst.github.io/LucidJS/) to emit events when interesting things happen to your model object.
+
+`change`: when any property has been changed
+`change.<prop>`: after a specific property has been changed
+`update`: after the objects's properties have been updated in `update()`
+`rollback`: after the object has been rolled back to a previous state
+`after-load`: after the object has been loaded from storage & a model instantiated
+`before-save`: before the object is saved to storage in `save()`
+`after-save`: after a save to storage has succeeded, before callback
+`before-destroy`: before deleting the object from storage in `destroy()`
+`after-destroy`: after deleting the object from storage in `destroy()`
+
+Here's an example of a property change listener:
+
+```javascript
+var obj = new Widget();
+obj.on('change.name', function(newval)
+{
+ assert(obj.name === newval);
+ console.log(newval); // logs 'planet x'
+});
+
+widget.name = 'planet x';
+
+```
+
+See the Lucid documentation for other things you can do with triggering and listening to events on your polyclay objects.
-`afterLoad()`: after a document has been loaded from storage & a model instantiated
-`beforeSave()`: before a document is saved to storage in `save()`
-`afterSave()`: after a save to storage has succeeded, before callback
-`beforeDestroy()`: before deleting a model from storage in `destroy()`
## Mixins
View
6 index.js
@@ -2,6 +2,6 @@ exports.Model = require('./lib/polyclay').Model;
exports.persist = require('./lib/persistence').persist;
exports.dataLength = require('./lib/util').dataLength;
exports.mixin = require('./lib/mixins').mixin;
-exports.CouchAdapter = require('./lib/adapters/couch');
-exports.RedisAdapter = require('./lib/adapters/redis');
-exports.LevelupAdapter = require('./lib/adapters/levelup');
+try { exports.CouchAdapter = require('./lib/adapters/couch'); } catch(ex) { }
+try { exports.RedisAdapter = require('./lib/adapters/redis'); } catch(ex) { }
+try { exports.LevelupAdapter = require('./lib/adapters/levelup'); } catch(ex) { }
View
68 lib/adapters/levelup.js
@@ -4,6 +4,7 @@
var
_ = require('lodash'),
assert = require('assert'),
+ async = require('async'),
fs = require('fs'),
levelup = require('levelup'),
path = require('path')
@@ -39,7 +40,7 @@ LevelupAdapter.prototype.namespaceKey = function(key)
if (key.indexOf(this.keyspace) === 0)
return key;
return this.keyspace + key;
-}
+};
LevelupAdapter.prototype.provision = function(callback)
{
@@ -64,7 +65,7 @@ LevelupAdapter.prototype.all = function(callback)
start: this.dbname + ':',
end: this.dbname + ';'
};
- this.db.createKeyStream().on('data', function (data)
+ this.db.createKeyStream(opts).on('data', function (data)
{
keys.push(data);
}).on('end', function()
@@ -95,7 +96,7 @@ LevelupAdapter.prototype.save = function(object, json, callback)
ops.push({ type: 'del', key: k });
else
ops.push({ type: 'put', key: k, value: body });
- };
+ }
this.db.put(basekey, payload.body, function(err, response)
{
@@ -161,24 +162,69 @@ LevelupAdapter.prototype.merge = function(key, attributes, callback)
LevelupAdapter.prototype.remove = function(object, callback)
{
- // TODO remove attachments
+ var self = this;
+ var key;
+ if (typeof object === 'string')
+ key = object;
+ else
+ key = object.key;
- this.db.del(this.namespaceKey(object.key), callback);
+ this.db.del(self.namespaceKey(key), function(err, response)
+ {
+ if (err) return callback(err);
+ self.removeAttachmentsFor(key, callback);
+ });
};
-LevelupAdapter.prototype.destroyMany = function(objects, callback)
+LevelupAdapter.prototype.removeAttachmentsFor = function(key, callback)
{
- // TODO remove attachments
+ var self = this;
+ var actions = [];
+ var opts =
+ {
+ start: this.namespaceKey(key) + ':',
+ end: this.namespaceKey(key) + ';'
+ };
+
+ this.attachdb.createKeyStream(opts).on('data', function (data)
+ {
+ actions.push({ type: 'del', key: data });
+ }).on('end', function()
+ {
+ if (actions.length === 0)
+ return callback(null, 'OK');
+ self.attachdb.batch(actions, function(err)
+ {
+ callback(err, err ? null : 'OK');
+ });
+ }).on('err', function(err)
+ {
+ callback(err);
+ });
+};
+
+LevelupAdapter.prototype.destroyMany = function(objects, callback)
+{
var self = this;
- var ops = _.map(objects, function(obj)
+ var actions = [], ops = [], k;
+ _.each(objects, function(obj)
{
if (typeof obj === 'string')
- return { type: 'del', key: self.namespaceKey(obj) };
- return { type: 'del', key: self.namespaceKey(obj.key) };
+ k = obj;
+ else
+ k = obj.key;
+
+ ops.push({ type: 'del', key: self.namespaceKey(k) });
+ actions.push(function(cb) { self.removeAttachmentsFor(k, cb); });
});
- this.db.batch(ops, callback);
+ actions.push(function(cb) { self.db.batch(ops, cb); });
+ async.parallel(actions, function(err, replies)
+ {
+ if (err) return callback(err);
+ callback(null, objects.length);
+ });
};
LevelupAdapter.prototype.attachment = function(key, name, callback)
View
11 lib/adapters/redis.js
@@ -162,7 +162,6 @@ RedisAdapter.prototype.removeAttachment = function(object, name, callback)
this.redis.hdel(this.attachmentKey(object.key), name, callback);
};
-var attachpat = /^attach:(.*)/;
RedisAdapter.prototype.inflate = function(payload)
{
if (payload === null)
@@ -177,16 +176,6 @@ RedisAdapter.prototype.inflate = function(payload)
{
var field = fields[i];
- if (matches = field.match(attachpat))
- {
- var name = matches[1];
- var struct = JSON.parse(payload[field]);
- if (_.isObject(struct.body))
- struct.body = new Buffer(struct.body);
- json._attachments[name] = struct;
- continue;
- }
-
try
{
json[field] = JSON.parse(payload[field]);
View
25 lib/persistence.js
@@ -38,8 +38,6 @@ function persist(modelfunc, keyfield)
});
}
- modelfunc.name = "change me";
-
// methods on the model class
modelfunc.defineAttachment = function(name, mimetype)
@@ -73,6 +71,7 @@ function persist(modelfunc, keyfield)
this.__attachments[name].stub = false;
this.__attachments[name].__dirty = true;
this.__attachments[name].content_type = this.__types[name];
+ this.trigger('change.' + name);
};
modelfunc.prototype.__defineSetter__(name, modelfunc.prototype['set_' + name]);
@@ -180,9 +179,7 @@ function persist(modelfunc, keyfield)
{
var self = this;
- if (self.beforeSave)
- self.beforeSave();
-
+ self.trigger('before-save');
var serialized = self.serialize();
serialized._attachments = self._serializeAttachments();
@@ -193,10 +190,7 @@ function persist(modelfunc, keyfield)
if (err) return callback(err);
self.clearDirty();
self.__new = false;
-
- if (self.afterSave)
- self.afterSave();
-
+ self.trigger('after-save');
callback(null, response);
});
}
@@ -207,9 +201,7 @@ function persist(modelfunc, keyfield)
{
if (err) return callback(err);
self.clearDirty();
- if (self.afterSave)
- self.afterSave();
-
+ self.trigger('after-save');
callback(null, 'OK');
});
}
@@ -223,13 +215,12 @@ function persist(modelfunc, keyfield)
if (self.destroyed)
return callback(new Error('object already destroyed'));
- if (self.beforeDestroy)
- self.beforeDestroy();
-
+ self.trigger('before-destroy');
modelfunc.adapter.remove(self, function(err, response)
{
if (err) return callback(err, false);
self.destroyed = true;
+ self.trigger('after-destroy');
callback(null, true);
});
};
@@ -246,9 +237,6 @@ function persist(modelfunc, keyfield)
this.__new = false;
this.destroyed = false;
this.clearDirty();
-
- if (this.afterLoad)
- this.afterLoad();
};
modelfunc.prototype.handleAttachments = function(attachments)
@@ -291,6 +279,7 @@ function persist(modelfunc, keyfield)
modelfunc.adapter.removeAttachment(this, name, function(err, response)
{
+ self.trigger('change.' + name);
callback(err, !err);
});
};
View
23 lib/polyclay.js
@@ -1,6 +1,7 @@
var
_ = require('lodash'),
- assert = require('assert')
+ assert = require('assert'),
+ lucid = require('lucidjs')
;
var PolyClay = function(){};
@@ -11,6 +12,7 @@ PolyClay.Model.buildClass = function(options, methods)
{
var sub = function()
{
+ lucid.emitter(this);
this.__attributes = {};
this.__attributesPrev = {};
this.__new = true;
@@ -75,13 +77,13 @@ PolyClay.defaults = function(type)
{
switch(type)
{
- case 'string': return '';
- case 'array': return [];
- case 'number': return 0;
- case 'boolean': return false;
- case 'date': return new Date();
- case 'hash': return {};
- case 'reference': return {};
+ case 'string': return '';
+ case 'array': return [];
+ case 'number': return 0;
+ case 'boolean': return false;
+ case 'date': return new Date();
+ case 'hash': return {};
+ case 'reference': return {};
}
};
@@ -156,6 +158,7 @@ PolyClay.Model.addProperty = function(obj, propname, type)
this.__attributesPrev[propname] = this.__attributes[propname];
this.__attributes[propname] = newval;
this.__dirty = true;
+ this.trigger('change.' + propname, newval);
};
obj.prototype.__defineGetter__(propname, getterFunc);
@@ -232,6 +235,7 @@ PolyClay.Model.addEnumerableProperty = function(obj, propname, enumerable)
this.__attributesPrev[propname] = this.__attributes[propname];
this.__attributes[propname] = newval;
this.__dirty = true;
+ this.trigger('change.' + propname, newval);
};
obj.prototype.__properties.push(propname);
@@ -250,6 +254,7 @@ PolyClay.Model.addOptionalProperty = function(obj, propname)
if (arguments.length === 0)
return this.__attributes[propname];
this.__attributes[propname] = arguments["0"];
+ this.trigger('change.' + propname, arguments["0"]);
};
obj.prototype.__defineGetter__(propname, result);
obj.prototype.__defineSetter__(propname, result);
@@ -293,6 +298,7 @@ PolyClay.Model.prototype.update = function(attr)
//else
// console.log('skipping unknown property ' + k);
}
+ this.trigger('update');
};
PolyClay.Model.prototype.isDirty = function()
@@ -316,6 +322,7 @@ PolyClay.Model.prototype.rollback = function()
this[props[i]] = this.__attributesPrev[props[i]];
this.clearDirty();
+ this.trigger('rollback');
return true;
};
View
5 package.json
@@ -1,6 +1,6 @@
{
"name": "polyclay",
- "version": "1.2.3",
+ "version": "1.3.0",
"description": "a schema-enforcing model class for node with optional key-value store persistence",
"main": "index.js",
"directories":
@@ -27,7 +27,8 @@
"dependencies":
{
"async": "*",
- "lodash": "*"
+ "lodash": "*",
+ "lucidjs": "*"
},
"optionalDependencies":
{
View
108 test/mock-adapter.js
@@ -0,0 +1,108 @@
+var _ = require('lodash');
+
+function MockDBAdapter()
+{
+ this.db = {};
+ this.attachments = {};
+}
+
+MockDBAdapter.prototype.configure = function(options, modelfunc)
+{
+ this.constructor = modelfunc;
+};
+
+MockDBAdapter.prototype.provision = function() { };
+MockDBAdapter.prototype.shutdown = function() { };
+
+MockDBAdapter.prototype.save = function(obj, properties, callback)
+{
+ this.db[obj.key] = properties;
+ callback(null, 'OK');
+};
+MockDBAdapter.prototype.update = MockDBAdapter.prototype.save;
+
+MockDBAdapter.prototype.merge = function(key, properties, callback)
+{
+ var previous = this.db[key];
+ _.assign(previous, properties);
+ this.db[key] = previous;
+ callback(null, 'OK');
+};
+
+MockDBAdapter.prototype.saveAttachment = function(obj, attachment, callback)
+{
+ this.attachments[obj.key + ':' + attachment.name] = attachment;
+ callback(null, 'OK');
+};
+
+MockDBAdapter.prototype.get = function(key, callback)
+{
+ var props = this.db[key];
+ if (!props)
+ return callback(null, null);
+
+ callback(null, this.inflate(props));
+};
+
+MockDBAdapter.prototype.getBatch = function(keylist, callback)
+{
+ var results = [];
+ for (var i = 0; i < keylist.length; i++)
+ {
+ var props = this.db[keylist[i]];
+ if (!props)
+ {
+ results.push(null);
+ continue;
+ }
+ results.push(this.inflate(props));
+ }
+
+ callback(null, results);
+};
+
+MockDBAdapter.prototype.all = function(callback)
+{
+ this.getBatch(Object.keys(this.db), callback);
+};
+
+MockDBAdapter.prototype.attachment = function(key, name, callback)
+{
+ callback(null, this.attachments[key + ':' + name]);
+};
+
+MockDBAdapter.prototype.remove = function(obj, callback)
+{
+ delete this.db[obj.key];
+
+ var prefix = obj.key + ':';
+ var keys = Object.keys(this.attachments);
+ for (var i = 0; i < keys.length; i++)
+ {
+ var k = keys[i];
+ if (k.indexOf(prefix) === 0)
+ delete this.attachments[k];
+ }
+
+ callback(null);
+};
+
+MockDBAdapter.prototype.destroyMany = function(objlist, callback)
+{
+ var keys = _.map(objlist, function(item) { return item.key; });
+};
+
+MockDBAdapter.prototype.removeAttachment = function(obj, name, callback)
+{
+ delete this.attachments[obj.key + ':' + name];
+ callback(null, 'OK');
+};
+
+MockDBAdapter.prototype.inflate = function(hash)
+{
+ var obj = new this.constructor();
+ obj.update(hash);
+ return obj;
+};
+
+module.exports = MockDBAdapter;
View
73 test/test-01-polyclay.js
@@ -268,9 +268,13 @@ describe('polyclay', function()
{
var name;
var instanceProps = Object.getOwnPropertyNames(instance);
+ var lucidprops = ['on', 'set', 'pipe', 'once', 'off', 'trigger', 'listeners'];
+
for (var i = 0; i < instanceProps.length; i++)
{
name = instanceProps[i];
+ if (lucidprops.indexOf(name) > -1)
+ continue;
assert(name.indexOf('__') === 0, 'model property "' + name + '" does not start with underscores');
}
});
@@ -424,4 +428,73 @@ describe('polyclay', function()
obj.is_valid.should.equal(data.is_valid);
});
+ it('emits a "change" event when a property is set', function(done)
+ {
+ var obj = new Model();
+ obj.on('change', function(val)
+ {
+ assert.equal(val, 9000, 'change event did not send new value');
+ done();
+ });
+ obj.count = 9000;
+ });
+
+ it('emits a "change.propname" event when a property is set', function(done)
+ {
+ var obj = new Model();
+ obj.on('change.count', function(val)
+ {
+ assert.equal(val, 9001, 'change.field event did not send new value');
+ done();
+ });
+ obj.count = 9001;
+ });
+
+ it('emits change events for optional properties', function(done)
+ {
+ var obj = new Model();
+ obj.on('change.ephemeral', function(val)
+ {
+ assert.equal(val, 'fleeting', 'change.field event did not send new value');
+ done();
+ });
+ obj.ephemeral = 'fleeting';
+ });
+
+ it('emits an event on rollback', function(done)
+ {
+ var obj = new Model();
+ obj.on('rollback', function()
+ {
+ assert.equal(obj.name, 'blort');
+ assert.equal(obj.count, 9000);
+ assert.ok(!obj.isDirty(), 'object is still dirty after rollback');
+ done();
+ });
+
+ obj.count = 9000;
+ obj.name = 'blort';
+ obj.clearDirty();
+ obj.name = 'foo';
+ obj.count = 2000;
+
+ obj.rollback();
+ });
+
+ it('emits an event on update', function(done)
+ {
+ var data = {
+ is_valid: true,
+ foozles: ['three', 'four'],
+ count: 50,
+ required_prop: 'badges'
+ };
+ var obj = new Model();
+ obj.on('update', function()
+ {
+ done();
+ });
+ obj.update(data);
+ });
+
});
View
184 test/test-02-persistence.js
@@ -4,15 +4,12 @@ var
chai = require('chai'),
assert = chai.assert,
expect = chai.expect,
- should = chai.should()
- ;
-
-var
- cradle = require('cradle'),
+ should = chai.should(),
fs = require('fs'),
path = require('path'),
polyclay = require('../index'),
- util = require('util')
+ util = require('util'),
+ MockDBAdapter = require('./mock-adapter')
;
var testDir = process.cwd();
@@ -47,13 +44,13 @@ describe('dataLength()', function()
});
});
-
describe('persistence layer', function()
{
var modelDefinition =
{
properties:
{
+ key: 'string',
name: 'string',
created: 'date',
foozles: 'array',
@@ -69,13 +66,11 @@ describe('persistence layer', function()
initialize: function()
{
this.ran_init = true;
+ this.on('after-load', this.afterLoad.bind(this));
},
methods:
{
- beforeSave: function() { this.beforeSaveCalled = true; },
- afterSave: function() { this.afterSaveCalled = true; },
- afterLoad: function() { this.afterLoadCalled = true; },
- beforeDestroy: function() { this.beforeDestroyCalled = true; },
+ afterLoad: function() { this.afterLoad = true; }
}
};
@@ -84,28 +79,11 @@ describe('persistence layer', function()
before(function()
{
Model = polyclay.Model.buildClass(modelDefinition);
-
- Model.design =
- {
- views:
- {
- by_name: { map: "function(doc) {\n emit(doc.name, doc);\n}", language: "javascript" }
- }
- };
-
- Model.fetchByName = function(name, callback)
- {
- Model.adapter.db.view('models/by_name', { key: name }, function(err, documents)
- {
- if (err) return callback(err);
- Model.constructMany(documents, callback);
- });
- };
});
it('adds functions to the prototype when persist is called', function()
{
- polyclay.persist(Model);
+ polyclay.persist(Model, 'key');
Model.prototype.save.should.be.a('function');
Model.prototype.destroy.should.be.a('function');
});
@@ -171,4 +149,152 @@ describe('persistence layer', function()
willThrow.should.throw(Error);
});
+ it('destroyMany() does nothing when given empty input', function(done)
+ {
+ Model.destroyMany(null, function(err)
+ {
+ should.not.exist(err);
+ done();
+ });
+ });
+
+ it('destroy responds with an error when passed an object without an id', function(done)
+ {
+ var obj = new Model();
+ obj.destroy(function(err, destroyed)
+ {
+ err.should.be.an('object');
+ err.message.should.equal('cannot destroy object without an id');
+ done();
+ });
+ });
+
+ it('destroy responds with an error when passed an object that has already been destroyed', function(done)
+ {
+ var obj = new Model();
+ obj.key = 'foozle';
+ obj.destroyed = true;
+ obj.destroy(function(err, destroyed)
+ {
+ err.should.be.an('object');
+ err.message.should.equal('object already destroyed');
+ done();
+ });
+ });
+
+ it('sets the db adapter in setStorage()', function()
+ {
+ Model.setStorage({}, MockDBAdapter);
+ Model.should.have.property('adapter');
+ assert.ok(Model.adapter instanceof MockDBAdapter);
+ });
+
+
+ it('emits before-save', function(done)
+ {
+ var obj = new Model();
+ obj.key = '1';
+ obj.on('before-save', function()
+ {
+ done();
+ });
+ obj.save(function(err, resp)
+ {
+ should.not.exist(err);
+ });
+ });
+
+ it('emits after-save', function(done)
+ {
+ var obj = new Model();
+ obj.key = '2';
+ obj.on('after-save', function()
+ {
+ done();
+ });
+ obj.save(function(err, resp)
+ {
+ should.not.exist(err);
+ });
+ });
+
+ it('emits after-load', function(done)
+ {
+ Model.get('1', function(err, obj)
+ {
+ should.not.exist(err);
+ obj.afterLoad.should.be.ok;
+ done();
+ });
+ });
+
+ it('emits before-destroy', function(done)
+ {
+ Model.get('1', function(err, obj)
+ {
+ obj.on('before-destroy', function()
+ {
+ done();
+ });
+
+ obj.destroy(function(err, destroyed)
+ {
+ should.not.exist(err);
+ });
+ });
+ });
+
+ it('emits after-destroy', function(done)
+ {
+ Model.get('2', function(err, obj)
+ {
+ obj.on('after-destroy', function()
+ {
+ obj.destroyed.should.equal(true);
+ done();
+ });
+
+ obj.destroy(function(err, destroyed)
+ {
+ should.not.exist(err);
+ destroyed.should.be.ok;
+ });
+ });
+ });
+
+ it('emits change events for attachments', function(done)
+ {
+ var Ephemeral = polyclay.Model.buildClass({});
+ polyclay.persist(Ephemeral);
+ Ephemeral.defineAttachment('test', 'text/plain');
+
+ var obj = new Ephemeral();
+ obj.on('change.test', function()
+ {
+ done();
+ });
+ obj.test = 'i am an attachment';
+ });
+
+ it('emits change events when attachments are removed', function(done)
+ {
+ var Ephemeral = polyclay.Model.buildClass({});
+ polyclay.persist(Ephemeral);
+ Ephemeral.defineAttachment('test', 'text/plain');
+ Ephemeral.setStorage({}, MockDBAdapter);
+
+ var obj = new Ephemeral();
+ obj.test = 'i am an attachment';
+ obj.save(function(err, resp)
+ {
+ obj.on('change.test', function()
+ {
+ done();
+ });
+ obj.removeAttachment('test', function(err, resp)
+ {
+ should.not.exist(err);
+ });
+ });
+ });
});
View
82 test/test-04-couch.js
@@ -41,6 +41,11 @@ describe('couch adapter', function()
initialize: function()
{
this.ran_init = true;
+ this.on('before-save', this.beforeSave.bind(this));
+ this.on('after-save', this.afterSave.bind(this));
+ this.on('after-load', this.afterLoad.bind(this));
+ this.on('before-destroy', this.beforeDestroy.bind(this));
+ this.on('after-destroy', this.afterDestroy.bind(this));
},
methods:
{
@@ -48,6 +53,7 @@ describe('couch adapter', function()
afterSave: function() { this.afterSaveCalled = true; },
afterLoad: function() { this.afterLoadCalled = true; },
beforeDestroy: function() { this.beforeDestroyCalled = true; },
+ afterDestroy: function() { this.afterDestroyCalled = true; },
}
};
@@ -96,13 +102,13 @@ describe('couch adapter', function()
it('can be configured for database access', function(done)
{
var connection = new cradle.Connection(
- couch_config.host,
- couch_config.port,
- {
- cache: false,
- raw: false,
- auth: couch_config.auth
- }
+ couch_config.host,
+ couch_config.port,
+ {
+ cache: false,
+ raw: false,
+ auth: couch_config.auth
+ }
);
var options =
{
@@ -437,11 +443,12 @@ describe('couch adapter', function()
});
});
- it('calls a beforeSave() hook before saving a model', function(done)
+ it('emits "before-save" before saving a model', function(done)
{
hookTest = new Model();
hookTest.name = 'hook test';
+ hookTest.should.not.have.property('afterSaveCalled');
hookTest.should.not.have.property('beforeSaveCalled');
hookTest.save(function(err, res)
{
@@ -453,36 +460,12 @@ describe('couch adapter', function()
});
});
- it('calls afterSave() after saving a model', function()
+ it('emits "after-save" after saving a model', function()
{
hookTest.should.have.property('afterSaveCalled');
hookTest.afterSaveCalled.should.equal(true);
});
- it('calls afterLoad() after loading a model from the db', function(done)
- {
- hookTest.should.not.have.property('afterLoadCalled');
- Model.get(hookid, function(err, loaded)
- {
- should.not.exist(err);
- loaded.should.have.property('afterLoadCalled');
- loaded.afterLoadCalled.should.equal(true);
- done();
- });
- });
-
- it('calls beforeDestroy() before destroying a model', function(done)
- {
- hookTest.should.not.have.property('beforeDestroyCalled');
- hookTest.destroy(function(err, deleted)
- {
- should.not.exist(err);
- hookTest.should.have.property('beforeDestroyCalled');
- hookTest.beforeDestroyCalled.should.equal(true);
- done();
- });
- });
-
it('can remove a document from the db', function(done)
{
instance.destroy(function(err, deleted)
@@ -515,39 +498,6 @@ describe('couch adapter', function()
});
});
- it('destroyMany() does nothing when given empty input', function(done)
- {
- Model.destroyMany(null, function(err)
- {
- should.not.exist(err);
- done();
- });
- });
-
- it('destroy responds with an error when passed an object without an id', function(done)
- {
- var obj = new Model();
- obj.destroy(function(err, destroyed)
- {
- err.should.be.an('object');
- err.message.should.equal('cannot destroy object without an id');
- done();
- });
- });
-
- it('destroy responds with an error when passed an object that has already been destroyed', function(done)
- {
- var obj = new Model();
- obj._id = 'foozle';
- obj.destroyed = true;
- obj.destroy(function(err, destroyed)
- {
- err.should.be.an('object');
- err.message.should.equal('object already destroyed');
- done();
- });
- });
-
// remaining uncovered cases:
// saveAttachment() -- just a passthrough to cradle, so very low value
// handleAttachments() -- only called by initFromStorage(), not sure it's ever been exercised
View
7 test/test-05-redis.js
@@ -42,13 +42,6 @@ describe('redis adapter', function()
initialize: function()
{
this.ran_init = true;
- },
- methods:
- {
- beforeSave: function() { this.beforeSaveCalled = true; },
- afterSave: function() { this.afterSaveCalled = true; },
- afterLoad: function() { this.afterLoadCalled = true; },
- beforeDestroy: function() { this.beforeDestroyCalled = true; },
}
};
View
36 test/test-06-levelup.js
@@ -392,11 +392,35 @@ describe('levelup adapter', function()
});
});
+ it('removes attachments when it removes a document', function(done)
+ {
+ var obj = new Model();
+ obj.key = 'tmp';
+ obj.avatar = attachmentdata;
+ obj.save(function(err, response)
+ {
+ should.not.exist(err);
+ obj.destroy(function(err, deleted)
+ {
+ should.not.exist(err);
+ deleted.should.be.ok;
+
+ Model.adapter.attachment('tmp', 'avatar', function(err, payload)
+ {
+ should.not.exist(err);
+ assert.ok(payload === null, 'got an attachment payload; was not deleted');
+ done();
+ });
+ });
+ });
+ });
+
it('can remove documents in batches', function(done)
{
var obj2 = new Model();
obj2.key = '4';
obj2.name = 'two';
+ obj2.avatar = attachmentdata;
obj2.save(function(err, response)
{
Model.get('2', function(err, obj)
@@ -408,13 +432,23 @@ describe('levelup adapter', function()
Model.destroyMany(itemlist, function(err, response)
{
should.not.exist(err);
- // TODO examine response more carefully
+ response.should.equal(2);
done();
});
});
});
});
+ it('removes attachments when it removes in batches', function(done)
+ {
+ Model.adapter.attachment('4', 'avatar', function(err, payload)
+ {
+ should.not.exist(err);
+ assert.ok(payload === null, 'got an attachment payload; was not deleted');
+ done();
+ });
+ });
+
it('destroyMany() does nothing when given empty input', function(done)
{
Model.destroyMany(null, function(err)
Something went wrong with that request. Please try again.