Skip to content

Commit

Permalink
Added flags for hooks, namespacing and mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
masylum committed Jul 14, 2011
1 parent 4b9724b commit a859383
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 234 deletions.
151 changes: 94 additions & 57 deletions README.md
Expand Up @@ -33,11 +33,26 @@ Models don't map data from the db, they just define the logic.
var USER = require('mongolia').model(db, 'users');
```

## mongo commands
## mongo proxied collection commands

Calls to the db are done using the function `mongo`.
Mongolia proxies all the `collection` methods defiend on the driver plus some custom methods.
Calls to the db are done using the method `mongo`.
`mongo` proxies all the `collection` methods defined on the driver plus some custom methods.

This allows mongolia to extend the driver with extra functionalties:

* Namespacing: Allows you to filter the documents going and coming from the db.
* Mapping: Allows you to apply functions to the documents attributes going and coming from the db.
* Hooks: They are triggered before and after a call is done.

The default usage is:

`mongo('method[:namespace]', args)`

If you want to disable any functionality you can by doing:

`mongo({method: method[, namespace: namespace, namespacing: false, mapping: false, hooks: false])`

Example:
``` javascript
var Db = require('mongodb/lib/mongodb/db').Db,
Server = require('mongodb/lib/mongodb/connection').Server,
Expand All @@ -46,60 +61,71 @@ var Db = require('mongodb/lib/mongodb/db').Db,
db.open(function () {
var User = require('./user.js')(db);

User.mongo('findOne', {name: 'foo'}, function (error, user) {
console.log(user);
});
User.mongo('findOne', {name: 'foo'}, console.log);
User.mongo({method: 'insert', hooks: false}, {name: 'foo'}, console.log);
});
```

All the `collection` methods from the driver are supported.

If you need more information visit the [driver](http://github.com/christkv/node-mongodb-native) documentation

## Custom mongo collection commands
### Custom mongo collection commands

Mongolia provides some useful commands that are not available using the driver.

* `findArray`: find that returns an array instead of a cursor.
* `mapReduceArray`: mapReduce that returns an array with the results.
* `mapReduceCursor`: mapReduce that returns a cursor.

## Hooks

Mongolia let you define some hooks on your models that will be triggered after a mongoDB command.
### Namespacing

* `beforeInsert(documents, callback)`: triggered *before* an `insert`.
* `afterInsert(documents, callback)`: triggered *after* an `insert.
Secure your data access defining visibility namespaces.

* `beforeUpdate(query, update, callback)`: triggered *before* an `update` or `findAndModify` command.
* `afterUpdate(query, update, callback)`: triggered *after* an `update` or `findAndModify` command.
You can namespace a call to the database by appending `:namespace` on
your proxied method.

* `beforeRemove(query, callback)`: triggered *before* a `remove` command.
* `afterRemove(query, callback)`: triggered *after* a `remove` command.
If called without a namespace, the method will work ignoring the `namespace` directives.

Example:
You can `extend` other namespaces and `add` or `remove` some data visibility.

``` javascript
var COMMENT = require('mongolia').model(db, 'comments'),
Post = require('./post');
var USER = require('mongolia').model(db, 'users');

COMMENT.beforeInsert = function (documents, callback) {
documents.forEach(function (doc) {
doc.created_at = new Date();
});
callback(null, documents);
USER.namespaces = {
public: ['account.email', 'account.name', '_id'],
private: {
extend: 'public',
add: ['password'],
},
accounting: {
extend: 'private',
add: ['credit_card_number'] // don't do this at home
}
};

COMMENT.atferInsert = function (documents, callback) {
Post(db).mongo('update', {_id: documents[0].post._id}, {'$inc': {num_posts: 1}}, callback);
};
USER.mongo('insert:public', {account: {email: 'foo@bar.com'}, password: 'fleiba', credit_card_number: 123, is_active: true});
// insert => {account: {email: 'foo@bar.com'}}

USER.validateAndUpdate({account: {email: 'foo@bar.com'}}, {'$set': {'account.email': 'super@mail.com', password: '123'}, {namespace: 'public'});
// updates => {'$set': {'account.email': 'super@mail.com'}}

USER.mongo('findArray:public', {account: {email: 'foo@bar.com'}});
// find => {account: {email: 'foo@bar.com', name: 'paco'}}

USER.mongo('findArray:accounting', {account: {email: 'foo@bar.com'}});
// find => {account: {email: 'foo@bar.com', name: 'paco'}, password: 'fleiba', credit_card_number: 123}
```
## Mappings and type casting
Use this feature wisely to filter data coming from forms.
### Mappings and type casting
Mongolia `maps` allows you to cast the data before is stored to the database.
Mongolia will apply the specified function for each attribute on the `maps` object.
By default we provide the map `_id -> ObjectId`, so you don't need to cast it.
``` javascript
var USER = require('mongolia').model(db, 'users');

Expand All @@ -118,47 +144,44 @@ USER.mongo('insert', {email: 'foo@bar.com', password: 123, name: 'john', is_dele
// stored => {password: '123', name: 'JOHN', is_deleted: true}
```
## Namespacing
### Hooks
Secure your data access defining visibility namespaces.
Mongolia let you define some hooks on your models that will be triggered after a mongoDB command.
You can namespace a call to the database by appending `:namespace` on
your proxied method.
* `beforeInsert(documents, callback)`: triggered *before* an `insert`.
* `afterInsert(documents, callback)`: triggered *after* an `insert.

If called without a namespace, the method will work ignoring the `namespace` directives.
* `beforeUpdate(query, update, callback)`: triggered *before* an `update` or `findAndModify` command.
* `afterUpdate(query, update, callback)`: triggered *after* an `update` or `findAndModify` command.

You can `extend` other namespaces and `add` or `remove` some data visibility.
* `beforeRemove(query, callback)`: triggered *before* a `remove` command.
* `afterRemove(query, callback)`: triggered *after* a `remove` command.

Example:

``` javascript
var USER = require('mongolia').model(db, 'users');
var COMMENT = require('mongolia').model(db, 'comments'),
Post = require('./post');
USER.namespaces = {
public: ['account.email', 'account.name', '_id'],
private: {
extend: 'public',
add: ['password'],
},
accounting: {
extend: 'private',
add: ['credit_card_number'] // don't do this at home
}
COMMENT.beforeInsert = function (documents, callback) {
documents.forEach(function (doc) {
doc.created_at = new Date();
});
callback(null, documents);
};
USER.mongo('insert:public', {account: {email: 'foo@bar.com'}, password: 'fleiba', credit_card_number: 123, is_active: true});
// insert => {account: {email: 'foo@bar.com'}}

USER.validateAndUpdate({account: {email: 'foo@bar.com'}}, {'$set': {'account.email': 'super@mail.com', password: '123'}, {namespace: 'public'});
// updates => {'$set': {'account.email': 'super@mail.com'}}

USER.mongo('findArray:public', {account: {email: 'foo@bar.com'}});
// find => {account: {email: 'foo@bar.com', name: 'paco'}}
COMMENT.atferInsert = function (documents, callback) {
documents.forEach(function (doc) {
Post(db).mongo('update', {_id: doc.post._id}, {'$inc': {num_posts: 1}}); // fire and forget
});
callback(null, documents);
};
USER.mongo('findArray:accounting', {account: {email: 'foo@bar.com'}});
// find => {account: {email: 'foo@bar.com', name: 'paco'}, password: 'fleiba', credit_card_number: 123}
USER.mongo('insert', {email: 'foo@bar.com'});
// stored => {email: 'foo@bar.com', created_at: Thu, 14 Jul 2011 12:13:39 GMT}
// Post#num_posts is increased
```

Use this feature wisely to filter data coming from forms.
## Embedded documents

Mongolia helps you to _denormalize_ your mongo database.
Expand Down Expand Up @@ -429,6 +452,20 @@ var User = function (db) {
return USER;
};
```
## Tests
Mongolia is fully tested using [testosterone](http://github.com/masylum/testosterone)
To run the tests use:
```bash
make
```
## Example
Monoglia has a fully working blog example on the `example` folder.
## Contributors
In no specific order.
Expand Down
72 changes: 37 additions & 35 deletions lib/helpers/collection_proxy.js
@@ -1,8 +1,9 @@
var PROXY = {},
var PROXY = {}
, _ = require('underscore');

_apply = function (collection, fn, args) {
return collection[fn].apply(collection, args);
};
function _apply(collection, fn, args) {
return collection[fn].apply(collection, args);
}

Object.defineProperty(PROXY, 'namespacer', {value: require('./namespacer'), writable: true});
Object.defineProperty(PROXY, 'mapper', {value: require('./mapper'), writable: true});
Expand All @@ -11,38 +12,43 @@ Object.defineProperty(PROXY, 'mapper', {value: require('./mapper'), writable: tr
* Proxies calls
*
* @param {Object} model
* @param {String} fn
* @param {String} namespace
* @param {Object} collection
* @param {Object} options
* @param {Array} args
* @param {Function} callback
*/
PROXY.proxy = function (model, fn, namespace, collection, args, callback) {
PROXY.proxy = function (model, options, args, callback) {
var fn = options.method
, arguments_length = arguments.length;

// noop
if (arguments.length < 6) {
callback = function () {};
}
model.getCollection(function (error, collection) {

if (model.namespaces && model.namespaces[namespace]) {
// has side effects, alters args
PROXY.namespacer.filter(model.namespaces, namespace, fn, args);
}
// noop
if (arguments_length < 4) {
callback = function () {};
}

if (model.maps) {
// has side effects, alters args
PROXY.mapper.map(model.maps, fn, args);
}
if (error) return callback(error, null);

// overwritten method
if (PROXY[fn] !== undefined) {
return PROXY[fn](model, collection, args, callback);
if (options.namespacing && model.namespaces && model.namespaces[options.namespace]) {
// has side effects, alters args
PROXY.namespacer.filter(model.namespaces, options.namespace, fn, args);
}

// driver method
} else {
args[args.length - 1] = callback;
_apply(collection, fn, args);
}
if (options.mapping && model.maps) {
// has side effects, alters args
PROXY.mapper.map(model.maps, fn, args);
}

// overwritten method with hooks, or custom method
if (typeof PROXY[fn] !== 'undefined' && (typeof collection[fn] !== 'undefined' ? options.hooks : true)) {
return PROXY[fn](model, collection, args, callback);

// driver method or hooks disabled
} else {
args[args.length - 1] = callback;
_apply(collection, fn, args);
}
});
};

/**
Expand Down Expand Up @@ -71,9 +77,8 @@ PROXY.findArray = function (model, collection, args, callback) {
*/
PROXY.insert = function (model, collection, args, callback) {
args[args.length - 1] = function (error, ret) {
if (error) {
return callback(error, null);
}
if (error) return callback(error, null);

model.afterInsert(args[0], function (error, _) {
callback(error, ret);
});
Expand All @@ -84,12 +89,9 @@ PROXY.insert = function (model, collection, args, callback) {
}

model.beforeInsert(args[0], function (error, documents) {
if (error) {
return callback(error, null);
}
if (error) return callback(error, null);

args[0] = documents;

_apply(collection, 'insert', args);
});
};
Expand Down
12 changes: 6 additions & 6 deletions lib/helpers/mapper.js
@@ -1,7 +1,7 @@
var MAPPER = {}
, _ = require('underscore');

MAPPER.filterUpdate = function (maps, arg) {
MAPPER.mapDocument = function (maps, arg) {

/* 0: dont match
* 1: partial matching, needs more inspection
Expand Down Expand Up @@ -57,14 +57,14 @@ MAPPER.filterUpdate = function (maps, arg) {
el[attr][i] = getObject(maps, attr)(el[attr][i]);
});
} else {
el[attr] = getObject(maps, attr)(el[attr]);
el[attr] = getObject(maps, attr)(el[attr]);
}
} else {
if (is_matching === 1 && typeof el[attr] === 'object') {
filter(maps[attr], el[attr], level + 1);
}
}
}
}
}
}
}
Expand All @@ -82,10 +82,10 @@ MAPPER.filterUpdate = function (maps, arg) {

MAPPER.map = function (maps, fn, args) {
if (fn === 'insert' || fn.match(/^find/)) {
MAPPER.filterUpdate(maps, args[0]);
MAPPER.mapDocument(maps, args[0]);
} else if (fn === 'update') {
MAPPER.filterUpdate(maps, args[0]);
MAPPER.filterUpdate(maps, args[1]);
MAPPER.mapDocument(maps, args[0]);
MAPPER.mapDocument(maps, args[1]);
}
};

Expand Down

0 comments on commit a859383

Please sign in to comment.