Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Implemented modelling.

  • Loading branch information...
commit 5e5cb2ea011e8d0348624c15f5aa90e3860ea8ee 1 parent 4ba3cff
@coreh authored
Showing with 414 additions and 28 deletions.
  1. +65 −14 README.md
  2. +16 −1 ferret.js
  3. +3 −3 package.json
  4. +330 −10 test.js
View
79 README.md
@@ -1,10 +1,10 @@
-ferret.js - Adorable bindings for mongodb
-=========================================
+ferret.js - Adorable mongodb library for node.js
+=================================================
What is ferret.js?
------------------
-`ferret.js` is a minimalistic wrapper around the excelent `node-mongodb-native` driver. It's pretty small (around 300 lines of code, without comments) and easy to use. Ferret's design is centered on:
+`ferret.js` is a minimalistic wrapper around the excelent `node-mongodb-native` driver. It's easy to use and pretty small (the core module is around 300 lines of code, without comments). Ferret's design is centered on:
1. Simplicity
2. Proper Error Handling and Recovery
@@ -20,7 +20,7 @@ Ferret is distributed under a MIT license. See the LICENSE file for more informa
Installation
============
-`ferret.js` can be easily installed through NPM.
+`ferret.js` can be easily installed through NPM:
npm install ferret
@@ -34,7 +34,7 @@ Sample Application/Quick Guide
### Hello World
- ferret.find('users', {})
+ ferret.find('users')
.on('each', function(user) {
console.log(user);
})
@@ -49,7 +49,7 @@ The `find` function returns an `util.EventEmitter` instance, to which you can at
If you want to you can bind to `success` instead of `each` to get an array with all the results:
- ferret.find('users', {})
+ ferret.find('users')
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
console.log(users[i]);
@@ -61,7 +61,7 @@ If you want to you can bind to `success` instead of `each` to get an array with
It's probably a good idea to add some error handling to the code. This can be done by attaching a listener to the `error` event (Notice that the `on` calls are chainable):
- ferret.find('users', {})
+ ferret.find('users')
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
console.log(users[i]);
@@ -79,7 +79,7 @@ If nothing is specified, it will default to database `test` on server `127.0.0.1
ferret.connect('test', '127.0.0.1', '27017')
- ferret.find('users', {})
+ ferret.find('users')
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
@@ -104,7 +104,7 @@ If you're going to use multiple databases, you can store the return value of `fe
var someDatabase = ferret.connect('test', '127.0.0.1', '27017')
var otherDatabase = ferret.connect('blah', '127.0.0.1', '27017')
- someDatabase.find('users', {})
+ someDatabase.find('users')
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
@@ -128,7 +128,7 @@ Notice that `find` will no longer need or take a collection name as an argument:
var users = someDatabase.collection('users')
- users.find({})
+ users.find()
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
@@ -155,7 +155,7 @@ It's really not necessary, but if you *really* want to, you can wait until the c
var users = database.collection('users')
- users.find({})
+ users.find()
.on('success', function(users) {
for (var i = 0; i < users.length; i++) {
@@ -171,6 +171,34 @@ It's really not necessary, but if you *really* want to, you can wait until the c
.on('error', function(){
// Could not connect to mongodb
})
+
+### Models
+
+Since version 0.2, ferret supports modelling:
+
+ var User = ferret.model('user', {
+ name: String,
+ age: Number,
+ email: {
+ $set: function(value) {
+ // validate email address
+ }
+ }
+ })
+
+ User.findOne({ name: 'John' })
+ .on('success', function(user) {
+ // Happy birthday!
+ user.age++
+
+ user.save()
+ .on('error', function(err) {
+ // Do something about it too
+ })
+ })
+ .on('error', function(err) {
+ // Do something about it
+ })
Design goals
@@ -186,6 +214,7 @@ Design goals
* a shared connection object
* manually instanced connections
* `FerretCollection` objects
+ * `FerretModel` objects (subset of other APIs)
4. **Sensible defaults** - `ferret.js` comes with sensible defaults built in so that you can get to your application logic up as quickly as possible. If you need a more tailored behavior, you can easily configure things later.
@@ -261,13 +290,14 @@ This section provides a quick overview of the ferret API. For detailed descripto
### Ferret Instance
* **Ferret#state()** - Returns the instance's current state
-* **Ferret#find(collection_name, query[, fields[, options]])** - Find documents
+* **Ferret#find(collection_name[, query[, fields[, options]]])** - Find documents
* **Ferret#findOne(collection_name, query)** - Find the first document
* **Ferret#insert(collection_name, docs)** - Inserts one or more documents
* **Ferret#save(collection_name, doc)** - Inserts if new, updates if existing
* **Ferret#update(collection_name, criteria, replacement[, options])** - Updates existing documents
* **Ferret#remove(collection_name, criteria)** - Removes existing documents
* **Ferret#collection(name)** - Retuns a `FerretCollection` object
+* **Ferret#model(schema)** - Create a new model
`Ferret` inherits `EventEmitter`, so it also provides all functions the latter provides.
@@ -282,13 +312,34 @@ For convenience, all the functions provided by `Ferret` instances are also avail
`FerretCollection` objects can be obtained through the `Ferret#collection` method. They provide many of the methods the ferret instance provides, minus the `collection_name` parameter:
-* **FerretCollection#find(query[, fields[, options]])** - Find documents
+* **FerretCollection#find([query[, fields[, options]]])** - Find documents
* **FerretCollection#findOne(query)** - Find the first document
* **FerretCollection#insert(docs)** - Inserts one or more documents
* **FerretCollection#save(doc)** - Inserts if new, updates if existing
* **FerretCollection#update(criteria, replacement[, options])** - Updates existing documents
* **FerretCollection#remove(criteria)** - Removes existing documents
+### FerretModel
+
+Constructors for `FerretModel` objects can be obtained through the `Ferret#model` method. `FerretModel` provides basic modelling functionality, so you can access data on a more object oriented fashion if you want to.
+
+The API is similar to `Ferret` and `FerretCollection`, but more limited.
+
+
+#### Static methods
+
+* **new FerretModel([data[, options]])** - Create a new model instance
+* **FerretModel#find([query])** - Find documents and wrap them in models
+* **FerretModel#findOne(query)** - Find the first document and wrap it in a model
+* **FerretModel#deserialize(data)** - Create a model from serialized data. Same as `new FerretModel(data, { deserialize: true })`.
+
+#### Instance methods
+
+* **FerretModel#save()** - Persist the model back into the database
+* **FerretModel#remove()** - Delete the object from the database
+* **FerretModel#serialize()** - Convert the model to a format ready for storage
+* **FerretModel#toJSON()** - Same as `FerretModel#serialize`
+
FAQ
---
@@ -298,4 +349,4 @@ Mongoose was already taken ;-)
### Does ferret provide ORM/Modelling functionality?
-Not yet. Stay tuned.
+Yep. With the release of version 0.2.0, ferret now provides models.
View
17 ferret.js
@@ -20,10 +20,11 @@ var Ferret = module.exports = function(database_name, host, port) {
this._server = new mongodb.Server(host, port, { auto_reconnect: false })
this._db = new mongodb.Db(database_name, this._server, {})
- // Initialize state variables, collection cache and ready queue
+ // Initialize state variables, collection cache, ready queue and models
this._ready = this._error = false
this._collections = {}
this._readyQueue = []
+ this._models = {}
// Connect
this._db.open(function(err, db) {
@@ -133,6 +134,7 @@ Ferret.prototype.state = function() {
Ferret.prototype.find = function(collection_name, query, fields, options) {
var ee = new EventEmitter()
+ if (query === undefined) { query = {} }
_collection(this, collection_name, function(err, collection) {
if (err) { ee.emit('error', err) }
else {
@@ -275,6 +277,15 @@ Ferret.prototype.collection = function(name) {
return new FerretCollection(this, name)
}
+// Model Stub
+Ferret.prototype.model = function(name, schema) {
+ // load module on first call
+ require('./model')(Ferret);
+
+ // call the proper function
+ return this.model(name, schema)
+}
+
var sharedFerret = null
// Connect to a server
@@ -334,6 +345,10 @@ Ferret.collection = function(name) {
return new FerretCollection(_ensureSharedFerret(), name)
}
+Ferret.model = function(name, schema) {
+ return _ensureSharedFerret().model(name, schema)
+}
+
/*
* EventEmitter methods replicated on Ferret, for convenience
*/
View
6 package.json
@@ -1,11 +1,11 @@
{
"name": "ferret",
- "description": "Adorable mongodb bindings for node.js",
- "keywords": ["mongodb", "database", "mongo"],
+ "description": "Adorable mongodb library for node.js with modelling support",
+ "keywords": ["mongodb", "database", "mongo", "model"],
"contributors": [
"Marco Aurélio"
],
- "version": "0.1.0",
+ "version": "0.2.0",
"engines": {
"node": ">=0.4.9"
},
View
340 test.js
@@ -1,31 +1,63 @@
-var ferret = require('./ferret')
+var ferret
+
+try {
+ require('colors')
+} catch (e) {
+ Object.defineProperty(String.prototype, "red", { get: function() { return this } })
+ Object.defineProperty(String.prototype, "green", { get: function() { return this } })
+ Object.defineProperty(String.prototype, "yellow", { get: function() { return this } })
+}
var currentTest = -1
-var start, next, tests, numErrors = 0;
-start = next = function(err, shouldStop) {
- if (err) {
- console.error( 'Test #' + currentTest + ' failed: ' )
- console.error( err.stack.toString() )
- numErrors++
+var start, next, tests, numErrors = 0, numSkips = 0, processErrors = 0;
+start = next = function(err, shouldStop, skipped) {
+ if (currentTest >= 0) {
+ if (err) {
+ console.error( 'Test #' + currentTest + ' failed: '.red )
+ console.error( err.stack.toString() )
+ numErrors++
+ } else {
+ if (skipped) {
+ numSkips++;
+ console.log( 'Test #' + currentTest + ' skipped'.yellow)
+ } else {
+ console.log( 'Test #' + currentTest + ' passed'.green)
+ }
+ }
}
if ((++currentTest < tests.length) && !shouldStop) {
process.nextTick(function() {
try {
tests[currentTest]()
} catch (err) {
- console.error("Catched exception.")
- next(err)
+ if (err != null) {
+ console.error("Catched exception.")
+ next(err)
+ }
}
})
} else {
process.nextTick(function() {
- console.log('Ran ' + currentTest + ' tests: ' + (currentTest - numErrors) + ' passed, ' + numErrors + ' failed.')
+ console.log('Ran ' + (currentTest - numSkips) + ' tests: ' + (currentTest - numErrors - numSkips) + ' passed, ' + numErrors + ' failed, ' + numSkips + ' skipped.')
process.exit(numErrors)
})
}
}
+
+var assert = function(exp) {
+ if (!exp) {
+ console.log('assertion: ' + new Error().stack.split('\n')[2].match(/\((.*)\)/)[1] + ' violated'.red)
+ next(new Error('assertion violated'))
+ throw null
+ }
+}
+
tests = [
function() {
+ ferret = require('./ferret')
+ next()
+ },
+ function() {
if (ferret.state() != 'start') {
next(new Error('ferret should be in \'start\' state'))
} else {
@@ -81,6 +113,10 @@ tests = [
})
},
function() {
+ if (process.argv[2] == '--skip-offline') {
+ next(null, false, true);
+ return;
+ }
var triggered = false;
ferret.on('disconnect', function() {
triggered = true;
@@ -98,6 +134,10 @@ tests = [
}, 15000)
},
function() {
+ if (process.argv[2] == '--skip-offline') {
+ next(null, false, true);
+ return;
+ }
ferret.find("users", {})
.on('success', function(users) {
next(new Error("Should have failed, since database is offline."))
@@ -107,6 +147,10 @@ tests = [
})
},
function() {
+ if (process.argv[2] == '--skip-offline') {
+ next(null, false, true);
+ return;
+ }
var triggered = false;
ferret.on('reconnect', function() {
triggered = true;
@@ -135,7 +179,283 @@ tests = [
next()
}
})
+ },
+ function() {
+ var testModel = ferret.model('hello')
+ if (testModel !== undefined) {
+ next(new Error('model should return undefined if not defined'))
+ } else {
+ next()
+ }
+ },
+ function() {
+ var count = 0;
+ var TestModel = ferret.model('hello', {
+ name: String,
+ age: Number,
+ isProgrammer: Boolean,
+ sex: {
+ $set: function(value) {
+ count++
+ if (value != 'M' && value != 'F' && value != '?') {
+ throw new Error('invalid sex')
+ }
+ },
+ $load: function(value) {
+ count++
+ if (value != 'M' && value != 'F') {
+ value = '?'
+ }
+ return value;
+ },
+ $default: '?'
+ },
+ bar: {
+ $get: function(value) {
+ count++
+ return value.replace(/foo/g, 'bar')
+ },
+ $default: 'foo'
+ },
+ dog: {
+ name: String,
+ age: {
+ $set: function(value) {
+ if (typeof value != 'number' && !(value instanceof Number)) {
+ throw new Error('age should be a number')
+ }
+ },
+ $store: function(value) {
+ return value * 7
+ },
+ $load: function(value) {
+ return value / 7
+ },
+ $default: NaN
+ },
+ isProgrammer: {
+ $get: function(value) {
+ // test getter without return
+ count++
+ },
+ $set: function(value) {
+ // test setter with return
+ count++
+ return false;
+ },
+ $default: false
+ }
+ }
+ })
+ if (!TestModel) {
+ next(new Error('did not return a model'))
+ } else if (TestModel !== ferret.model('hello')) {
+ next(new Error('the model created previously and the model returned are not the same'))
+ }
+ var guy = new TestModel()
+ assert(guy._id == undefined)
+ assert(guy.name == '')
+ assert(isNaN(guy.age))
+ assert(guy.isProgrammer === false)
+ assert(guy.sex == '?')
+ assert(guy.bar == 'bar')
+ assert(guy.dog.name == '')
+ assert(isNaN(guy.dog.age))
+ assert(guy.dog.isProgrammer === false)
+ assert(count == 2)
+
+ guy.name = 'John'
+ assert(guy.name == 'John')
+ guy.age = 20;
+ assert(guy.age == 20)
+ guy.sex = 'M'
+ assert(guy.sex == 'M')
+ assert(count == 3)
+ guy.dog.name = 'Sparks'
+ assert(guy.dog.name == 'Sparks')
+ guy.dog.age = 3
+ assert(guy.dog.age == 3)
+ guy.dog.isProgrammer = true;
+ assert(guy.dog.isProgrammer == false)
+
+ var hasFailed = false;
+ try {
+ guy.sex = 'Invalid Value'
+ } catch (e) {
+ hasFailed = true;
+ }
+ assert(hasFailed)
+ assert(guy.sex == 'M')
+ guy.bar = 'foo foo foo';
+ assert(guy.bar == 'bar bar bar')
+
+ // $set test
+ var gal = new TestModel({
+ name: 'Jane',
+ age: 18,
+ sex: 'F',
+ dog: {
+ name: 'Ribs',
+ age: 2,
+ isProgrammer: true // Will be set to false by $set
+ }
+ })
+
+ assert(gal._id == undefined)
+ assert(gal.name == 'Jane')
+ assert(gal.age === 18)
+ assert(gal.sex == 'F')
+ assert(gal.dog.name == 'Ribs')
+ assert(gal.dog.age == 2)
+ assert(gal.dog.isProgrammer == false)
+
+ // $load test (deserialize)
+ gal = new TestModel({
+ name: 'Jane',
+ age: '18',
+ sex: 'F',
+ dog: {
+ name: 'Ribs',
+ age: 14, // in dog years
+ isProgrammer: true // should work since there's no $load
+ }
+ }, { deserialize: true })
+
+ assert(gal._id == undefined)
+ assert(gal.name == 'Jane')
+ assert(gal.age === 18)
+ assert(gal.sex == 'F')
+ assert(gal.dog.name == 'Ribs')
+ assert(gal.dog.age == 2)
+ assert(gal.dog.isProgrammer == true) // behold the amazing programming dog
+
+ var mistery = TestModel.deserialize({
+ sex: 'unknown' // should become '?'
+ })
+
+ assert(mistery.sex == '?')
+
+ guy.save()
+ .on('success', function(savedGuy) {
+ assert(savedGuy === guy)
+ assert(guy._id !== undefined)
+ TestModel.findOne(guy._id)
+ .on('success', function(loadedGuy){
+ assert(loadedGuy !== undefined)
+ assert(loadedGuy !== null)
+ assert(loadedGuy instanceof TestModel)
+ assert(loadedGuy.name == 'John')
+ assert(loadedGuy.age == 20)
+ assert(loadedGuy.sex == 'M')
+ assert(loadedGuy.isProgrammer == false)
+ assert(loadedGuy.dog.name == 'Sparks')
+ assert(loadedGuy.dog.age == 3)
+ assert(loadedGuy.dog.isProgrammer == false)
+ assert(loadedGuy.bar == 'bar bar bar')
+ next()
+ })
+ .on('error', function(err){
+ next(err)
+ })
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ },
+ function() {
+ var TestModel = ferret.model('hello')
+ var count = 0
+ var error = null
+
+ TestModel.find({})
+ .on('each', function(person) {
+ assert(person instanceof TestModel)
+ count++
+ })
+ .on('error', function(err) {
+ error = err
+ })
+
+ setTimeout(function() {
+ assert(count > 0)
+ assert(error == null)
+ next()
+ }, 200)
+ },
+ function() {
+ var TestModel = ferret.model('hello')
+ var count = 0
+
+ var test = new TestModel({
+ name: 'Hello'
+ })
+ test.save()
+ .on('success', function() {
+ TestModel.find()
+ .on('success', function(models) {
+ assert(models instanceof Array)
+ assert(models.length > 0)
+ test.remove()
+ .on('success', function(count) {
+ assert(typeof count == 'number')
+ assert(count == 1)
+ TestModel.find()
+ .on('success', function(moreModels) {
+ assert(moreModels.length == models.length - 1)
+ next()
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ },
+ function() {
+ var TestModel = ferret.model('hello')
+ var total = null
+ var count = 0
+ var lastError = null
+ TestModel.find()
+ .on('success', function(results) {
+ total = results.length;
+ for (var i = 0; i < total; i++) {
+ results[i].remove()
+ .on('success', function() {
+ count++;
+ })
+ .on('error', function(err) {
+ lastError = err
+ })
+ }
+ })
+ .on('error', function(err) {
+ next(err)
+ })
+ setTimeout(function(){
+ assert(count == total)
+ assert(lastError == null)
+ next()
+ }, 200)
+ },
+ function() {
+ assert(processErrors == 0)
+ next()
}
]
+process.on('error', function(err){
+ processErrors++;
+ console.error(err);
+})
+
start()
Please sign in to comment.
Something went wrong with that request. Please try again.