1
0
Fork 0

Merge pull request #1287 from arangodb/vulpine-grace

Foxx improvements (fixes #1284, #1257)
This commit is contained in:
Alan Plum 2015-04-08 03:23:55 +02:00
commit ecbc6f581f
8 changed files with 221 additions and 28 deletions

View File

@ -140,6 +140,8 @@ v2.6.0 (XXXX-XX-XX)
* added `extendible` package.
* added Foxx model lifecycle events to repositories. See #1257.
v2.5.2 (XXXX-XX-XX)
-------------------

View File

@ -37,6 +37,18 @@ var PersonModel = Foxx.Model.extend({
exports.model = PersonModel;
```
You can also use `joi.object` schemas directly:
```js
var PersonModel = Foxx.Model.extend({
schema: joi.object().keys({
name: joi.string().required(),
age: joi.number().integer(),
active: joi.boolean().default(true)
})
});
```
This has two effects: On the one hand it provides documentation. If you annotated
your model, you can use it in the **bodyParam** method for documentation.
On the other hand it will influence the behavior of the constructor: If you provide
@ -66,6 +78,8 @@ The following events are emitted by a model:
- beforeRemove
- afterRemove
Equivalent events will also be emitted by the repository handling the model.
Model lifecycle:
```js

View File

@ -11,6 +11,47 @@ var TodosRepository = Foxx.Repository.extend({
exports.repository = TodosRepository;
```
The following events are emitted by a repository:
- beforeCreate
- afterCreate
- beforeSave
- afterSave
- beforeUpdate
- afterUpdate
- beforeRemove
- afterRemove
Model lifecycle:
```js
var person = new PersonModel();
person.on('beforeCreate', function() {
var model = this;
model.fancyMethod(); // Do something fancy with the model
});
var people = new Repository(appContext.collection("people"), { model: PersonModel });
people.save(person);
// beforeCreate(person)
// beforeSave(person)
// The model is created at db
// afterSave(person)
// afterCreate(person)
people.update(person, data);
// beforeUpdate(person, data)
// beforeSave(person, data)
// The model is updated at db
// afterSave(person, data)
// afterUpdate(person, data)
people.remove(person);
// beforeRemove(person)
// The model is deleted at db
// afterRemove(person)
```
!SUBSECTION Initialize
@startDocuBlock JSF_foxx_repository_initializer

View File

@ -488,6 +488,7 @@ SHELL_SERVER_ONLY = \
@top_srcdir@/js/server/tests/shell-database-noncluster.js \
@top_srcdir@/js/server/tests/shell-foxx.js \
@top_srcdir@/js/server/tests/shell-foxx-repository-spec.js \
@top_srcdir@/js/server/tests/shell-foxx-repository-events-spec.js \
@top_srcdir@/js/server/tests/shell-foxx-query-spec.js \
@top_srcdir@/js/server/tests/shell-foxx-model.js \
@top_srcdir@/js/server/tests/shell-foxx-model-events-spec.js \

View File

@ -62,16 +62,15 @@ var Model,
/// @endDocuBlock
////////////////////////////////////////////////////////////////////////////////
excludeExtraAttributes = function (attributes, Model) {
excludeExtraAttributes = function (attributes, model) {
'use strict';
var extraAttributeNames;
if (Model.prototype.schema) {
extraAttributeNames = _.difference(
_.keys(metadataSchema),
_.keys(Model.prototype.schema)
);
if (!model.schema) {
return _.clone(attributes);
}
return _.omit(attributes, extraAttributeNames);
return _.omit(attributes, _.difference(
_.keys(metadataSchema),
_.keys(model.schema)
));
};
Model = function (attributes) {
@ -111,25 +110,32 @@ Model = function (attributes) {
this.errors = {};
var instance = this;
if (instance.schema) {
if (this.schema) {
if (this.schema.isJoi) {
this.schema = _.object(_.map(this.schema._inner.children, function (prop) {
return [prop.key, prop.schema];
}));
}
_.each(
_.union(_.keys(instance.schema), _.keys(attributes)),
function (attributeName) {
instance.set(attributeName, attributes ? attributes[attributeName] : undefined);
}
_.union(_.keys(this.schema), _.keys(attributes)),
function (key) {
this.set(key, attributes && attributes[key]);
},
this
);
} else if (attributes) {
instance.attributes = _.clone(attributes);
this.attributes = _.clone(attributes);
}
EventEmitter.call(instance);
EventEmitter.call(this);
};
util.inherits(Model, EventEmitter);
Model.fromClient = function (attributes) {
'use strict';
return new this(excludeExtraAttributes(attributes, this));
var model = new this();
model.set(excludeExtraAttributes(attributes, model));
return model;
};
// Instance Properties
@ -187,18 +193,12 @@ _.extend(Model.prototype, {
_.each(attributeName, function (value, key) {
this.set(key, value);
}, this);
return;
return this;
}
if (this.schema) {
var schema = (
this.schema[attributeName] ||
metadataSchema[attributeName] ||
joi.forbidden()
),
result = (
schema.isJoi ? schema : joi.object().keys(schema)
).validate(value);
var schema = this.schema[attributeName] || metadataSchema[attributeName] || joi.forbidden();
var result = schema.validate(value);
if (result.error) {
this.errors[attributeName] = result.error;
@ -209,6 +209,7 @@ _.extend(Model.prototype, {
this.isValid = true;
}
}
if (result.value === undefined) {
delete this.attributes[attributeName];
} else {
@ -217,6 +218,8 @@ _.extend(Model.prototype, {
} else {
this.attributes[attributeName] = value;
}
return this;
},
////////////////////////////////////////////////////////////////////////////////
@ -270,7 +273,7 @@ _.extend(Model.prototype, {
forClient: function () {
'use strict';
return excludeExtraAttributes(this.attributes, this.constructor);
return excludeExtraAttributes(this.attributes, this);
}
});

View File

@ -34,7 +34,9 @@ var Repository,
ArangoError = arangodb.ArangoError,
ArangoCollection = arangodb.ArangoCollection,
errors = arangodb.errors,
extend = require('extendible');
extend = require('extendible'),
EventEmitter = require('events').EventEmitter,
util = require('util');
////////////////////////////////////////////////////////////////////////////////
/// @startDocuBlock JSF_foxx_repository_initializer
@ -127,8 +129,12 @@ Repository = function (collection, opts) {
this.collection.ensureIndex(index);
}, this);
}
EventEmitter.call(this);
};
util.inherits(Repository, EventEmitter);
// -----------------------------------------------------------------------------
// --SECTION-- Methods
// -----------------------------------------------------------------------------
@ -155,11 +161,15 @@ _.extend(Repository.prototype, {
////////////////////////////////////////////////////////////////////////////////
save: function (model) {
'use strict';
this.emit('beforeCreate', model);
model.emit('beforeCreate');
this.emit('beforeSave', model);
model.emit('beforeSave');
var id_and_rev = this.collection.save(model.forDB());
model.set(id_and_rev);
this.emit('afterSave', model);
model.emit('afterSave');
this.emit('afterCreate', model);
model.emit('afterCreate');
return model;
},
@ -316,9 +326,11 @@ _.extend(Repository.prototype, {
////////////////////////////////////////////////////////////////////////////////
remove: function (model) {
'use strict';
this.emit('beforeRemove', model);
model.emit('beforeRemove');
var id = model.get('_id'),
result = this.collection.remove(id);
this.emit('afterRemove', model);
model.emit('afterRemove');
return result;
},
@ -457,13 +469,17 @@ _.extend(Repository.prototype, {
////////////////////////////////////////////////////////////////////////////////
update: function (model, data) {
'use strict';
this.emit('beforeUpdate', model, data);
model.emit('beforeUpdate', data);
this.emit('beforeSave', model, data);
model.emit('beforeSave', data);
var id = model.get("_id") || model.get("_key"),
id_and_rev = this.collection.update(id, data);
model.set(data);
model.set(id_and_rev);
this.emit('afterSave', model, data);
model.emit('afterSave', data);
this.emit('afterUpdate', model, data);
model.emit('afterUpdate', data);
return model;
},

View File

@ -194,6 +194,37 @@ function ModelSpec () {
assertTrue(instance.isValid);
},
testJoiObject: function () {
var Model = FoxxModel.extend({
schema: joi.object().keys({
lol: joi.string()
})
});
instance = new Model({lol: 5});
assertEqual(_.keys(instance.attributes).length, 1);
assertEqual(instance.get("lol"), 5);
assertEqual(_.keys(instance.errors).length, 1);
assertFalse(instance.isValid);
},
testAttributeDefaults: function () {
var special = function () {
return 42;
};
var Model = FoxxModel.extend({
schema: {
aString: joi.any().default("potato"),
special: joi.any().default(special, "current date")
}
});
instance = new Model();
assertEqual(instance.get("aString"), "potato");
assertEqual(instance.get("special"), special());
},
testCoerceAttributes: function () {
var Model = FoxxModel.extend({
schema: {

View File

@ -0,0 +1,85 @@
/*global require, describe, expect, it, beforeEach, createSpyObj */
var FoxxRepository = require("org/arangodb/foxx/repository").Repository,
Model = require("org/arangodb/foxx/model").Model;
describe('Model Events', function () {
'use strict';
var collection, instance, repository;
beforeEach(function () {
collection = createSpyObj('collection', [
'update',
'save',
'remove'
]);
instance = new Model();
repository = new FoxxRepository(collection, {model: Model, random: '', beforeCalled: false, afterCalled: false});
});
it('should be possible to subscribe and emit events', function () {
expect(repository.on).toBeDefined();
expect(repository.emit).toBeDefined();
});
it('should emit beforeCreate and afterCreate events when creating the model', function () {
addHooks(repository, instance, 'Create');
expect(repository.save(instance)).toEqual(instance);
expect(repository.beforeCalled).toBe(true);
expect(repository.afterCalled).toBe(true);
});
it('should emit beforeSave and afterSave events when creating the model', function () {
addHooks(repository, instance, 'Save');
expect(repository.save(instance)).toEqual(instance);
expect(repository.beforeCalled).toBe(true);
expect(repository.afterCalled).toBe(true);
});
it('should emit beforeUpdate and afterUpdate events when updating the model', function () {
var newData = { newAttribute: 'test' };
addHooks(repository, instance, 'Update', newData);
expect(repository.update(instance, newData)).toEqual(instance);
expect(repository.beforeCalled).toBe(true);
expect(repository.afterCalled).toBe(true);
});
it('should emit beforeSave and afterSave events when updating the model', function () {
var newData = { newAttribute: 'test' };
addHooks(repository, instance, 'Save', newData);
expect(repository.update(instance, newData)).toEqual(instance);
expect(repository.beforeCalled).toBe(true);
expect(repository.afterCalled).toBe(true);
});
it('should emit beforeRemove and afterRemove events when removing the model', function () {
addHooks(repository, instance, 'Remove');
repository.remove(instance);
expect(repository.beforeCalled).toBe(true);
expect(repository.afterCalled).toBe(true);
});
});
function addHooks(repo, model, ev, dataToReceive) {
'use strict';
var random = String(Math.floor(Math.random() * 1000));
repo.on('before' + ev, function (self, data) {
expect(this).toEqual(repo);
expect(self).toEqual(model);
expect(data).toEqual(dataToReceive);
this.random = random;
this.beforeCalled = true;
});
repo.on('after' + ev, function (self, data) {
expect(this).toEqual(repo);
expect(self).toEqual(model);
expect(data).toEqual(dataToReceive);
this.afterCalled = true;
expect(this.beforeCalled).toBe(true);
expect(this.random).toEqual(random);
});
}