From 0620369a0581b54e62ddcd54b9b3a40ccb8d04f5 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Mar 2015 17:05:07 +0100 Subject: [PATCH 1/6] Saner schema handling for Foxx models. Fixes #1284. --- .../Books/Users/Foxx/Develop/Model.mdpp | 12 +++ js/server/modules/org/arangodb/foxx/model.js | 51 ++++++----- .../modules/org/arangodb/foxx/repository.js | 8 +- js/server/tests/shell-foxx-model.js | 31 +++++++ .../shell-foxx-repository-events-spec.js | 85 +++++++++++++++++++ 5 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 js/server/tests/shell-foxx-repository-events-spec.js diff --git a/Documentation/Books/Users/Foxx/Develop/Model.mdpp b/Documentation/Books/Users/Foxx/Develop/Model.mdpp index 0e0c87e737..55dfc8dbac 100644 --- a/Documentation/Books/Users/Foxx/Develop/Model.mdpp +++ b/Documentation/Books/Users/Foxx/Develop/Model.mdpp @@ -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 diff --git a/js/server/modules/org/arangodb/foxx/model.js b/js/server/modules/org/arangodb/foxx/model.js index df7290c39b..178d8ca6a3 100644 --- a/js/server/modules/org/arangodb/foxx/model.js +++ b/js/server/modules/org/arangodb/foxx/model.js @@ -62,16 +62,13 @@ 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) - ); - } - return _.omit(attributes, extraAttributeNames); + if (!model.schema) return _.clone(attributes); + return _.omit(attributes, _.difference( + _.keys(metadataSchema), + _.keys(model.schema) + )); }; Model = function (attributes) { @@ -111,25 +108,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); } - 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 @@ -191,14 +195,8 @@ _.extend(Model.prototype, { } 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 +207,7 @@ _.extend(Model.prototype, { this.isValid = true; } } + if (result.value === undefined) { delete this.attributes[attributeName]; } else { @@ -270,7 +269,7 @@ _.extend(Model.prototype, { forClient: function () { 'use strict'; - return excludeExtraAttributes(this.attributes, this.constructor); + return excludeExtraAttributes(this.attributes, this); } }); diff --git a/js/server/modules/org/arangodb/foxx/repository.js b/js/server/modules/org/arangodb/foxx/repository.js index fdde55c2fc..3fc3467ebb 100644 --- a/js/server/modules/org/arangodb/foxx/repository.js +++ b/js/server/modules/org/arangodb/foxx/repository.js @@ -106,7 +106,13 @@ Repository = function (collection, opts) { configurable: false, enumerable: true, get: function () { - return this.model.prototype.schema; + var schema = this.model.prototype.schema; + if (schema && schema.isJoi) { + return _.object(_.map(schema._inner.children, function (prop) { + return [prop.key, prop.schema]; + })); + } + return schema; } }); diff --git a/js/server/tests/shell-foxx-model.js b/js/server/tests/shell-foxx-model.js index 626aada14a..f9a3cd54cf 100644 --- a/js/server/tests/shell-foxx-model.js +++ b/js/server/tests/shell-foxx-model.js @@ -190,6 +190,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: { diff --git a/js/server/tests/shell-foxx-repository-events-spec.js b/js/server/tests/shell-foxx-repository-events-spec.js new file mode 100644 index 0000000000..03e4ad32f4 --- /dev/null +++ b/js/server/tests/shell-foxx-repository-events-spec.js @@ -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); + }); +} \ No newline at end of file From 02c9beab0cb61d288e37dd3564cf68a3de9344f2 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Mar 2015 17:06:29 +0100 Subject: [PATCH 2/6] Support chaining in Foxx.Model#set. --- js/server/modules/org/arangodb/foxx/model.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/model.js b/js/server/modules/org/arangodb/foxx/model.js index 178d8ca6a3..6cab5a295e 100644 --- a/js/server/modules/org/arangodb/foxx/model.js +++ b/js/server/modules/org/arangodb/foxx/model.js @@ -191,7 +191,7 @@ _.extend(Model.prototype, { _.each(attributeName, function (value, key) { this.set(key, value); }, this); - return; + return this; } if (this.schema) { @@ -216,6 +216,8 @@ _.extend(Model.prototype, { } else { this.attributes[attributeName] = value; } + + return this; }, //////////////////////////////////////////////////////////////////////////////// From 03ccf67269c0c247e497dd4a961249cecd510508 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Mar 2015 17:27:48 +0100 Subject: [PATCH 3/6] Implemented Repository lifecycle events. See #1257. --- CHANGELOG | 2 + .../Books/Users/Foxx/Develop/Model.mdpp | 2 + .../Books/Users/Foxx/Develop/Repository.mdpp | 41 +++++++++++++++++++ UnitTests/Makefile.unittests | 1 + .../modules/org/arangodb/foxx/repository.js | 16 ++++++++ 5 files changed, 62 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index a95da38ce7..25bc8b73ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -51,6 +51,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) ------------------- diff --git a/Documentation/Books/Users/Foxx/Develop/Model.mdpp b/Documentation/Books/Users/Foxx/Develop/Model.mdpp index 55dfc8dbac..4b90acfde7 100644 --- a/Documentation/Books/Users/Foxx/Develop/Model.mdpp +++ b/Documentation/Books/Users/Foxx/Develop/Model.mdpp @@ -78,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 diff --git a/Documentation/Books/Users/Foxx/Develop/Repository.mdpp b/Documentation/Books/Users/Foxx/Develop/Repository.mdpp index 530030bb97..3005521baa 100644 --- a/Documentation/Books/Users/Foxx/Develop/Repository.mdpp +++ b/Documentation/Books/Users/Foxx/Develop/Repository.mdpp @@ -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 diff --git a/UnitTests/Makefile.unittests b/UnitTests/Makefile.unittests index bf5f53a9e9..fb63ba97fd 100755 --- a/UnitTests/Makefile.unittests +++ b/UnitTests/Makefile.unittests @@ -487,6 +487,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 \ diff --git a/js/server/modules/org/arangodb/foxx/repository.js b/js/server/modules/org/arangodb/foxx/repository.js index 3fc3467ebb..4b6a9e620c 100644 --- a/js/server/modules/org/arangodb/foxx/repository.js +++ b/js/server/modules/org/arangodb/foxx/repository.js @@ -35,6 +35,8 @@ var Repository, ArangoCollection = arangodb.ArangoCollection, errors = arangodb.errors, extend = require('extendible'); + EventEmitter = require('events').EventEmitter, + util = require('util'); //////////////////////////////////////////////////////////////////////////////// /// @startDocuBlock JSF_foxx_repository_initializer @@ -133,8 +135,12 @@ Repository = function (collection, opts) { this.collection.ensureIndex(index); }, this); } + + EventEmitter.call(this); }; +util.inherits(Repository, EventEmitter); + // ----------------------------------------------------------------------------- // --SECTION-- Methods // ----------------------------------------------------------------------------- @@ -161,11 +167,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; }, @@ -322,9 +332,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; }, @@ -463,13 +475,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; }, From eb30d2aad0adba72511aa25d1dc02e37d459e16b Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Mar 2015 18:26:01 +0100 Subject: [PATCH 4/6] Less magic. --- js/server/modules/org/arangodb/foxx/repository.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/js/server/modules/org/arangodb/foxx/repository.js b/js/server/modules/org/arangodb/foxx/repository.js index 4b6a9e620c..8ddab7cba1 100644 --- a/js/server/modules/org/arangodb/foxx/repository.js +++ b/js/server/modules/org/arangodb/foxx/repository.js @@ -108,13 +108,7 @@ Repository = function (collection, opts) { configurable: false, enumerable: true, get: function () { - var schema = this.model.prototype.schema; - if (schema && schema.isJoi) { - return _.object(_.map(schema._inner.children, function (prop) { - return [prop.key, prop.schema]; - })); - } - return schema; + return this.model.prototype.schema; } }); From 8266fb825296cf56a34c6e37ff1b43c1f4031906 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Tue, 24 Mar 2015 20:36:51 +0100 Subject: [PATCH 5/6] Linting. --- js/server/modules/org/arangodb/foxx/model.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/server/modules/org/arangodb/foxx/model.js b/js/server/modules/org/arangodb/foxx/model.js index 6cab5a295e..f0a88b529b 100644 --- a/js/server/modules/org/arangodb/foxx/model.js +++ b/js/server/modules/org/arangodb/foxx/model.js @@ -64,7 +64,9 @@ var Model, excludeExtraAttributes = function (attributes, model) { 'use strict'; - if (!model.schema) return _.clone(attributes); + if (!model.schema) { + return _.clone(attributes); + } return _.omit(attributes, _.difference( _.keys(metadataSchema), _.keys(model.schema) From 85254434ee1ba4ee1297f71ebec6a857608efdd2 Mon Sep 17 00:00:00 2001 From: Alan Plum Date: Wed, 25 Mar 2015 13:01:30 +0100 Subject: [PATCH 6/6] More linting. --- js/server/modules/org/arangodb/foxx/model.js | 2 +- js/server/modules/org/arangodb/foxx/repository.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/server/modules/org/arangodb/foxx/model.js b/js/server/modules/org/arangodb/foxx/model.js index f0a88b529b..3f7b5aaf02 100644 --- a/js/server/modules/org/arangodb/foxx/model.js +++ b/js/server/modules/org/arangodb/foxx/model.js @@ -124,7 +124,7 @@ Model = function (attributes) { this ); } else if (attributes) { - instance.attributes = _.clone(attributes); + this.attributes = _.clone(attributes); } EventEmitter.call(this); }; diff --git a/js/server/modules/org/arangodb/foxx/repository.js b/js/server/modules/org/arangodb/foxx/repository.js index 8ddab7cba1..380ae8bb31 100644 --- a/js/server/modules/org/arangodb/foxx/repository.js +++ b/js/server/modules/org/arangodb/foxx/repository.js @@ -34,7 +34,7 @@ var Repository, ArangoError = arangodb.ArangoError, ArangoCollection = arangodb.ArangoCollection, errors = arangodb.errors, - extend = require('extendible'); + extend = require('extendible'), EventEmitter = require('events').EventEmitter, util = require('util');