mirror of https://gitee.com/bigwinds/arangodb
Merge pull request #1287 from arangodb/vulpine-grace
Foxx improvements (fixes #1284, #1257)
This commit is contained in:
commit
ecbc6f581f
|
@ -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)
|
||||
-------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue