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 `extendible` package.
|
||||||
|
|
||||||
|
* added Foxx model lifecycle events to repositories. See #1257.
|
||||||
|
|
||||||
|
|
||||||
v2.5.2 (XXXX-XX-XX)
|
v2.5.2 (XXXX-XX-XX)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
|
@ -37,6 +37,18 @@ var PersonModel = Foxx.Model.extend({
|
||||||
exports.model = PersonModel;
|
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
|
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.
|
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
|
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
|
- beforeRemove
|
||||||
- afterRemove
|
- afterRemove
|
||||||
|
|
||||||
|
Equivalent events will also be emitted by the repository handling the model.
|
||||||
|
|
||||||
Model lifecycle:
|
Model lifecycle:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
|
@ -11,6 +11,47 @@ var TodosRepository = Foxx.Repository.extend({
|
||||||
exports.repository = TodosRepository;
|
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
|
!SUBSECTION Initialize
|
||||||
|
|
||||||
@startDocuBlock JSF_foxx_repository_initializer
|
@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-database-noncluster.js \
|
||||||
@top_srcdir@/js/server/tests/shell-foxx.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-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-query-spec.js \
|
||||||
@top_srcdir@/js/server/tests/shell-foxx-model.js \
|
@top_srcdir@/js/server/tests/shell-foxx-model.js \
|
||||||
@top_srcdir@/js/server/tests/shell-foxx-model-events-spec.js \
|
@top_srcdir@/js/server/tests/shell-foxx-model-events-spec.js \
|
||||||
|
|
|
@ -62,16 +62,15 @@ var Model,
|
||||||
/// @endDocuBlock
|
/// @endDocuBlock
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
excludeExtraAttributes = function (attributes, Model) {
|
excludeExtraAttributes = function (attributes, model) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var extraAttributeNames;
|
if (!model.schema) {
|
||||||
if (Model.prototype.schema) {
|
return _.clone(attributes);
|
||||||
extraAttributeNames = _.difference(
|
|
||||||
_.keys(metadataSchema),
|
|
||||||
_.keys(Model.prototype.schema)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return _.omit(attributes, extraAttributeNames);
|
return _.omit(attributes, _.difference(
|
||||||
|
_.keys(metadataSchema),
|
||||||
|
_.keys(model.schema)
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
Model = function (attributes) {
|
Model = function (attributes) {
|
||||||
|
@ -111,25 +110,32 @@ Model = function (attributes) {
|
||||||
|
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
|
|
||||||
var instance = this;
|
if (this.schema) {
|
||||||
if (instance.schema) {
|
if (this.schema.isJoi) {
|
||||||
|
this.schema = _.object(_.map(this.schema._inner.children, function (prop) {
|
||||||
|
return [prop.key, prop.schema];
|
||||||
|
}));
|
||||||
|
}
|
||||||
_.each(
|
_.each(
|
||||||
_.union(_.keys(instance.schema), _.keys(attributes)),
|
_.union(_.keys(this.schema), _.keys(attributes)),
|
||||||
function (attributeName) {
|
function (key) {
|
||||||
instance.set(attributeName, attributes ? attributes[attributeName] : undefined);
|
this.set(key, attributes && attributes[key]);
|
||||||
}
|
},
|
||||||
|
this
|
||||||
);
|
);
|
||||||
} else if (attributes) {
|
} else if (attributes) {
|
||||||
instance.attributes = _.clone(attributes);
|
this.attributes = _.clone(attributes);
|
||||||
}
|
}
|
||||||
EventEmitter.call(instance);
|
EventEmitter.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
util.inherits(Model, EventEmitter);
|
util.inherits(Model, EventEmitter);
|
||||||
|
|
||||||
Model.fromClient = function (attributes) {
|
Model.fromClient = function (attributes) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return new this(excludeExtraAttributes(attributes, this));
|
var model = new this();
|
||||||
|
model.set(excludeExtraAttributes(attributes, model));
|
||||||
|
return model;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Instance Properties
|
// Instance Properties
|
||||||
|
@ -187,18 +193,12 @@ _.extend(Model.prototype, {
|
||||||
_.each(attributeName, function (value, key) {
|
_.each(attributeName, function (value, key) {
|
||||||
this.set(key, value);
|
this.set(key, value);
|
||||||
}, this);
|
}, this);
|
||||||
return;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.schema) {
|
if (this.schema) {
|
||||||
var schema = (
|
var schema = this.schema[attributeName] || metadataSchema[attributeName] || joi.forbidden();
|
||||||
this.schema[attributeName] ||
|
var result = schema.validate(value);
|
||||||
metadataSchema[attributeName] ||
|
|
||||||
joi.forbidden()
|
|
||||||
),
|
|
||||||
result = (
|
|
||||||
schema.isJoi ? schema : joi.object().keys(schema)
|
|
||||||
).validate(value);
|
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
this.errors[attributeName] = result.error;
|
this.errors[attributeName] = result.error;
|
||||||
|
@ -209,6 +209,7 @@ _.extend(Model.prototype, {
|
||||||
this.isValid = true;
|
this.isValid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.value === undefined) {
|
if (result.value === undefined) {
|
||||||
delete this.attributes[attributeName];
|
delete this.attributes[attributeName];
|
||||||
} else {
|
} else {
|
||||||
|
@ -217,6 +218,8 @@ _.extend(Model.prototype, {
|
||||||
} else {
|
} else {
|
||||||
this.attributes[attributeName] = value;
|
this.attributes[attributeName] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -270,7 +273,7 @@ _.extend(Model.prototype, {
|
||||||
|
|
||||||
forClient: function () {
|
forClient: function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return excludeExtraAttributes(this.attributes, this.constructor);
|
return excludeExtraAttributes(this.attributes, this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,9 @@ var Repository,
|
||||||
ArangoError = arangodb.ArangoError,
|
ArangoError = arangodb.ArangoError,
|
||||||
ArangoCollection = arangodb.ArangoCollection,
|
ArangoCollection = arangodb.ArangoCollection,
|
||||||
errors = arangodb.errors,
|
errors = arangodb.errors,
|
||||||
extend = require('extendible');
|
extend = require('extendible'),
|
||||||
|
EventEmitter = require('events').EventEmitter,
|
||||||
|
util = require('util');
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
/// @startDocuBlock JSF_foxx_repository_initializer
|
/// @startDocuBlock JSF_foxx_repository_initializer
|
||||||
|
@ -127,8 +129,12 @@ Repository = function (collection, opts) {
|
||||||
this.collection.ensureIndex(index);
|
this.collection.ensureIndex(index);
|
||||||
}, this);
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventEmitter.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
util.inherits(Repository, EventEmitter);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// --SECTION-- Methods
|
// --SECTION-- Methods
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -155,11 +161,15 @@ _.extend(Repository.prototype, {
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
save: function (model) {
|
save: function (model) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
this.emit('beforeCreate', model);
|
||||||
model.emit('beforeCreate');
|
model.emit('beforeCreate');
|
||||||
|
this.emit('beforeSave', model);
|
||||||
model.emit('beforeSave');
|
model.emit('beforeSave');
|
||||||
var id_and_rev = this.collection.save(model.forDB());
|
var id_and_rev = this.collection.save(model.forDB());
|
||||||
model.set(id_and_rev);
|
model.set(id_and_rev);
|
||||||
|
this.emit('afterSave', model);
|
||||||
model.emit('afterSave');
|
model.emit('afterSave');
|
||||||
|
this.emit('afterCreate', model);
|
||||||
model.emit('afterCreate');
|
model.emit('afterCreate');
|
||||||
return model;
|
return model;
|
||||||
},
|
},
|
||||||
|
@ -316,9 +326,11 @@ _.extend(Repository.prototype, {
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
remove: function (model) {
|
remove: function (model) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
this.emit('beforeRemove', model);
|
||||||
model.emit('beforeRemove');
|
model.emit('beforeRemove');
|
||||||
var id = model.get('_id'),
|
var id = model.get('_id'),
|
||||||
result = this.collection.remove(id);
|
result = this.collection.remove(id);
|
||||||
|
this.emit('afterRemove', model);
|
||||||
model.emit('afterRemove');
|
model.emit('afterRemove');
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
@ -457,13 +469,17 @@ _.extend(Repository.prototype, {
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
update: function (model, data) {
|
update: function (model, data) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
this.emit('beforeUpdate', model, data);
|
||||||
model.emit('beforeUpdate', data);
|
model.emit('beforeUpdate', data);
|
||||||
|
this.emit('beforeSave', model, data);
|
||||||
model.emit('beforeSave', data);
|
model.emit('beforeSave', data);
|
||||||
var id = model.get("_id") || model.get("_key"),
|
var id = model.get("_id") || model.get("_key"),
|
||||||
id_and_rev = this.collection.update(id, data);
|
id_and_rev = this.collection.update(id, data);
|
||||||
model.set(data);
|
model.set(data);
|
||||||
model.set(id_and_rev);
|
model.set(id_and_rev);
|
||||||
|
this.emit('afterSave', model, data);
|
||||||
model.emit('afterSave', data);
|
model.emit('afterSave', data);
|
||||||
|
this.emit('afterUpdate', model, data);
|
||||||
model.emit('afterUpdate', data);
|
model.emit('afterUpdate', data);
|
||||||
return model;
|
return model;
|
||||||
},
|
},
|
||||||
|
|
|
@ -194,6 +194,37 @@ function ModelSpec () {
|
||||||
assertTrue(instance.isValid);
|
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 () {
|
testCoerceAttributes: function () {
|
||||||
var Model = FoxxModel.extend({
|
var Model = FoxxModel.extend({
|
||||||
schema: {
|
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