From fb3fbf29d477d0d7338c8c80b9c8be9742e8cbe0 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 14 Jul 2014 23:49:09 -0600 Subject: [PATCH 01/26] movign indentation to 2 spaces --- .jshintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jshintrc b/.jshintrc index f41eb2b6..28f79fb6 100644 --- a/.jshintrc +++ b/.jshintrc @@ -6,7 +6,7 @@ "curly": true, "eqeqeq": true, "immed": true, - "indent": 4, + "indent": 2, "latedef": "nofunc", "newcap": true, "noarg": true, From eb936684461fec50bd384575e4f00374c45678b6 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 14 Jul 2014 23:49:39 -0600 Subject: [PATCH 02/26] updating dependencies and making package.json structure the same as the other modules --- package.json | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 74c009c3..c00698cd 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,8 @@ { "name": "feathers-mongoose", + "description": "Feathers Mongoose ORM service", "version": "1.2.0", - "description": "Easily create a Mongoose Service for Featherjs.", - "main": "lib/index.js", - "scripts": { - "test": "grunt test" - }, + "homepage": "https://github.com/feathersjs/feathers-mongoose-service", "repository": { "type": "git", "url": "git://github.com/feathersjs/feathers-mongoose-service.git" @@ -21,27 +18,41 @@ "mongoose", "service" ], - "author": "Glavin Wiechert (https://github.com/Glavin001)", + "author": "Feathers (http://feathersjs.com)", + "contributors": [ + "Glavin Wiechert (https://github.com/Glavin001)", + "Eric Kryski (http://erickryski.com)" + ], "license": "MIT", "bugs": { "url": "https://github.com/feathersjs/feathers-mongoose-service/issues" }, - "homepage": "https://github.com/feathersjs/feathers-mongoose-service", + "main": "lib/mongoose.js", + "scripts": { + "test": "grunt test" + }, + "engines": { + "node": "~0.10.0", + "npm": "~1.4.0" + }, "devDependencies": { - "mocha": "*", - "grunt-cli": "~0.1.7", + "body-parser": "^1.4.3", + "feathers": ">= 1.0.0-pre.1", "grunt": "~0.4.1", - "grunt-release": "~0.7.0", + "grunt-cli": "~0.1.7", "grunt-contrib-jshint": "~0.x", - "grunt-simple-mocha": "~0.4.0", "grunt-contrib-watch": "~0.5.3", "grunt-express-server": "~0.4.11", - "feathers": ">= 0.3.0 < 1", - "mongoose": ">= 3.8.4 < 4" + "grunt-release": "~0.7.0", + "grunt-simple-mocha": "~0.4.0", + "mocha": "*" + }, + "dependencies": { + "lodash": "^2.4.1", + "mongoose": "^3.8.13", + "uberproto": "^1.1.2" }, - "dependencies": {}, "peerDependencies": { - "feathers": ">= 0.3.0 < 1" - , "mongoose": ">= 3.8.4 < 4" + "feathers": ">= 1.0.0-pre.1" } } From c6fae594b02247a71cbcd43e6013dd354db55580 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 14 Jul 2014 23:50:41 -0600 Subject: [PATCH 03/26] updating example and giving it the same folder structure as the other modules --- example/index.js | 33 --------------------------------- examples/basic.js | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 example/index.js create mode 100644 examples/basic.js diff --git a/example/index.js b/example/index.js deleted file mode 100644 index 4c3b5a40..00000000 --- a/example/index.js +++ /dev/null @@ -1,33 +0,0 @@ -// Get Feathers -var feathers = require('feathers'); -// Get Mongoose -var mongoose = require('mongoose'); -// Connect to your MongoDB -mongoose.connect('mongodb://localhost/test'); - -// Get Mongoose-Service -var mongooseService = require('../lib'); // Mongoose-Service - -// Create your Mongoose-Service, for a `User` -var userService = mongooseService('user', { - email: {type : String, required : true, index: {unique: true, dropDups: true}}, - firstName: {type : String, required : true}, - lastName: {type : String, required : true}, - age: {type : Number, required : true}, - password: {type : String, required : true, select: false}, - skills: {type : Array, required : true} - }, mongoose); - -// Setup Feathers -var app = feathers(); - -// Configure Feathers -app.use(feathers.logger('dev')); // For debugging purposes. -// ................ -var port = 8080; -// ................ -app.configure(feathers.socketio()) - .use('/user', userService) // <-- Register your custom Mongoose-Service with Feathers - .listen(port, function() { - console.log('Express server listening on port ' + port); - }); diff --git a/examples/basic.js b/examples/basic.js new file mode 100644 index 00000000..cae886ad --- /dev/null +++ b/examples/basic.js @@ -0,0 +1,21 @@ +var feathers = require('feathers'); +var mongooseService = require('../lib/mongoose'); +var bodyParser = require('body-parser'); + +// Create your Mongoose-Service, for a `User` +var userService = mongooseService('user', { + email: {type : String, required : true, index: {unique: true, dropDups: true}}, + firstName: {type : String, required : true}, + lastName: {type : String, required : true} + }); + +// Setup Feathers +var app = feathers(); + +app.configure(feathers.rest()) + .use(bodyParser.json()) + .use('/users', userService) + .configure(feathers.errors()) + .listen(8080); + +console.log('App listening on 127.0.0.1:8080'); From ff5044cde7ac87b627715542f6deeef1cc89a44e Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 14 Jul 2014 23:52:34 -0600 Subject: [PATCH 04/26] making mongoose compatible with the new feathers interface. Also making it extendable by using uberproto. Closes #12 --- lib/index.js | 104 --------------------------- lib/mongoose.js | 183 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 104 deletions(-) delete mode 100644 lib/index.js create mode 100644 lib/mongoose.js diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index fd2cb295..00000000 --- a/lib/index.js +++ /dev/null @@ -1,104 +0,0 @@ -// Dependencies - -// ----------------- -module.exports = function(modelName, schema, mongoose) { - - // Constructor - function Store(modelName, schema, mongoose) { - // Extract - var Schema = mongoose.Schema; - var connection = mongoose.connection; - - // Set model name - this.name = modelName; - - // Create Schema - this.schema = new Schema(schema); - - // Create Model - this.model = connection.model(this.name, this.schema); - - return this; - } - - // Getter methods for 'model' and 'schema' - Store.prototype.getModel = function() { - return this.model; - }; - - Store.prototype.getSchema = function () { - return this.schema; - }; - - // Helpers - var parseQuery = function(query) { - if (typeof query !== 'object') - { - query = {}; - } - // Parse query arguments - for (var k in query) - { - if (typeof query[k] !== 'object') - { - try - { - query[k] = JSON.parse(query[k]); - } catch (e) - { - console.log('Error:', e); - query[k] = null; - } - } - } - return query; - }; - - // Returns a Model by it's id - Store.prototype.find = function (params, callback) { - if (typeof params === 'function') { - callback = params; - params = {}; - } - var query = parseQuery(params.query); - - this.model.find(query.conditions, query.fields, query.options, function (err, data) { - callback(err, data); - }); - }; - - Store.prototype.get = function (id, params, callback) { - if (typeof params === 'function') { - callback = params; - params = {}; - } - var query = parseQuery(params.query); - - this.model.findById(id, query.fields, query.options, function (err, data) { - callback(err, data); - }); - }; - - Store.prototype.create = function (data, params, callback) { - // Create our actual Model object so that we only get what we really want - var obj = new this.model(data); - obj.save(function (err, data) { - callback(err, data); - }); - }; - - Store.prototype.update = function (id, data, params, callback) { - this.model.findByIdAndUpdate(id, data, { upsert: true }, function(err, data) { - return callback(err, data); - }); - }; - - Store.prototype.remove = function (id, params, callback) { - this.model.findByIdAndRemove(id, function(err, data) { - return callback(err, data); - }); - }; - - return new Store(modelName, schema, mongoose); - -}; diff --git a/lib/mongoose.js b/lib/mongoose.js new file mode 100644 index 00000000..00dfaa6d --- /dev/null +++ b/lib/mongoose.js @@ -0,0 +1,183 @@ +var _ = require('lodash'); +var Proto = require('uberproto'); +var mongoose = require('mongoose'); +var errors = require('feathers').errors.types; +var Schema = mongoose.Schema; +var ObjectId = mongoose.Types.ObjectId; + +// Helpers +var parseQuery = function(query) { + if (typeof query !== 'object') { + return {}; + } + + // Parse query arguments + for (var k in query) { + if (typeof query[k] !== 'object') { + try { + query[k] = JSON.parse(query[k]); + } catch (e) { + console.log('Error:', e); + query[k] = null; + } + } + } + + return query; +}; + +var MongooseService = Proto.extend({ + // TODO (EK): How do we handle indexes? + init: function(modelName, schema, options) { + options = options || {}; + + if(typeof modelName !== 'string') { + throw new errors.GeneralError('Must provide a valid model name'); + } + + this.options = _.extend({}, options); + + this._connect(this.options); + this.name = modelName; + this.type = 'mongoose'; + this.schema = new Schema(schema, this.options); + this.model = mongoose.model(this.name, this.schema); + }, + + // NOTE (EK): We create a new database connection for every MongooseService. + // This may not be good but... in the mean time the rationale for this + // design is because each user of a MongooseService instance could be a separate + // app residing on a totally different server. + + // TODO (EK): We need to handle replica sets. + _connect: function(options) { + var connectionString = options.connectionString; + + if(!connectionString) { + var config = _.extend({ + host: 'localhost', + port: 27017, + db: 'feathers' + }, options); + + connectionString = config.host + ':' + config.port + '/' + config.db; + } + + if(options.username && options.password) { + connectionString += options.username + ':' + options.password + '@'; + } + + if(options.reconnect) { + connectionString += '?auto_reconnect=true'; + } + + // TODO (EK): Support mongoose connection options + // http://mongoosejs.com/docs/connections.html + this.store = mongoose.connect(connectionString); + }, + + find: function(params, cb) { + if(_.isFunction(params)) { + cb = params; + params = {}; + } + + var query = parseQuery(params.query); + + this.model.find(query.conditions, query.fields, query.options, function (error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + cb(null, data); + }); + }, + + get: function(id, params, cb) { + if(_.isFunction(id)) { + cb = id; + return cb(new errors.BadRequest('A string or number id must be provided')); + } + + if(_.isFunction(params)) { + cb = params; + params = {}; + } + + var query = parseQuery(params.query); + + this.model.findById(new ObjectId(id), query.fields, query.options, function (error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + if (!data) { + return cb(new errors.NotFound('No record found for id ' + id)); + } + + cb(null, data); + }); + }, + + // TODO (EK): Batch support for create, update, delete. + create: function(data, params, cb) { + if(_.isFunction(params)) { + cb = params; + params = {}; + } + + // Create our actual Model object + var model = new this.model(data); + model.save(function (error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + cb(null, data.length === 1 ? data[0] : data); + }); + }, + + patch: this.update, + + update: function(id, data, params, cb) { + if(_.isFunction(params)) { + cb = params; + params = {}; + } + + this.model.findByIdAndUpdate(id, data, { upsert: true }, function(error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + if (!data) { + return cb(new errors.NotFound('No record found for id ' + id)); + } + + cb(null, data); + }); + }, + + remove: function(id, params, cb) { + if(_.isFunction(params)) { + cb = params; + params = {}; + } + + this.model.findByIdAndRemove(id, function(error, data) { + if (error) { + return cb(error); + } + + if (!data) { + return cb(new errors.NotFound('No record found for id ' + id)); + } + + cb(null, data); + }); + } +}); + +module.exports = function(modelName, schema, options) { + return Proto.create.call(MongooseService, modelName, schema, options); +}; \ No newline at end of file From 0fd481e18becf1d1ba3b98c9b4fc588ef036cb32 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 14 Jul 2014 23:58:25 -0600 Subject: [PATCH 05/26] make sure all id strings are cast to ObjectIds --- lib/mongoose.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index 00dfaa6d..eeabcbb0 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -145,7 +145,7 @@ var MongooseService = Proto.extend({ params = {}; } - this.model.findByIdAndUpdate(id, data, { upsert: true }, function(error, data) { + this.model.findByIdAndUpdate(new ObjectId(id), data, { upsert: true }, function(error, data) { if (error) { return cb(new errors.BadRequest(error)); } @@ -164,7 +164,7 @@ var MongooseService = Proto.extend({ params = {}; } - this.model.findByIdAndRemove(id, function(error, data) { + this.model.findByIdAndRemove(new ObjectId(id), function(error, data) { if (error) { return cb(error); } From a1e884abcc89cc6e9c487e942fe5eca42bd97350 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 15 Jul 2014 00:11:02 -0600 Subject: [PATCH 06/26] fixing patch support --- lib/mongoose.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index eeabcbb0..68299b53 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -137,7 +137,9 @@ var MongooseService = Proto.extend({ }); }, - patch: this.update, + patch: function(id, data, params, cb) { + this.update.apply(this, arguments); + }, update: function(id, data, params, cb) { if(_.isFunction(params)) { From b715c8d94b6ccceaf4826ca7aa0ac5d9d3e76b03 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 15 Jul 2014 01:54:26 -0600 Subject: [PATCH 07/26] fixing feathers dependency version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c00698cd..27618cca 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "body-parser": "^1.4.3", - "feathers": ">= 1.0.0-pre.1", + "feathers": ">= 1.0.x", "grunt": "~0.4.1", "grunt-cli": "~0.1.7", "grunt-contrib-jshint": "~0.x", @@ -53,6 +53,6 @@ "uberproto": "^1.1.2" }, "peerDependencies": { - "feathers": ">= 1.0.0-pre.1" + "feathers": ">= 1.0.x" } } From d60f6c36ff183b803f4dd200de6449c785cf6d40 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 15 Jul 2014 01:55:33 -0600 Subject: [PATCH 08/26] adding support for virtuals, methods, statics & indexes. Also exporting the service itself --- lib/mongoose.js | 68 +++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index 68299b53..24c55371 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -5,30 +5,9 @@ var errors = require('feathers').errors.types; var Schema = mongoose.Schema; var ObjectId = mongoose.Types.ObjectId; -// Helpers -var parseQuery = function(query) { - if (typeof query !== 'object') { - return {}; - } - - // Parse query arguments - for (var k in query) { - if (typeof query[k] !== 'object') { - try { - query[k] = JSON.parse(query[k]); - } catch (e) { - console.log('Error:', e); - query[k] = null; - } - } - } - - return query; -}; - var MongooseService = Proto.extend({ // TODO (EK): How do we handle indexes? - init: function(modelName, schema, options) { + init: function(modelName, entity, options) { options = options || {}; if(typeof modelName !== 'string') { @@ -40,7 +19,34 @@ var MongooseService = Proto.extend({ this._connect(this.options); this.name = modelName; this.type = 'mongoose'; - this.schema = new Schema(schema, this.options); + this.schema = new Schema(entity.schema, this.options); + + if (entity.methods) { + _.each(entity.methods, function(val, key){ + this.schema.methods[key] = val; + }, this); + } + + if (entity.statics) { + _.each(entity.statics, function(val, key){ + this.schema.statics[key] = val; + }, this); + } + + if (entity.virtuals) { + _.each(entity.virtuals, function(val, key){ + _.each(val, function(fn, method){ + this.schema.virtual(key)[method](fn); + }, this); + }, this); + } + + if (entity.indexes) { + _.each(entity.indexes, function(val){ + this.schema.index(val); + }, this); + } + this.model = mongoose.model(this.name, this.schema); }, @@ -81,10 +87,9 @@ var MongooseService = Proto.extend({ cb = params; params = {}; } - - var query = parseQuery(params.query); - - this.model.find(query.conditions, query.fields, query.options, function (error, data) { + + // TODO (EK): handle params.fields & params.options + this.model.find(params.query, {}, {}, function (error, data) { if (error) { return cb(new errors.BadRequest(error)); } @@ -104,9 +109,8 @@ var MongooseService = Proto.extend({ params = {}; } - var query = parseQuery(params.query); - - this.model.findById(new ObjectId(id), query.fields, query.options, function (error, data) { + // TODO (EK): handle params.fields & params.options + this.model.findById(new ObjectId(id), {}, {}, function (error, data) { if (error) { return cb(new errors.BadRequest(error)); } @@ -182,4 +186,6 @@ var MongooseService = Proto.extend({ module.exports = function(modelName, schema, options) { return Proto.create.call(MongooseService, modelName, schema, options); -}; \ No newline at end of file +}; + +module.exports.Service = MongooseService; \ No newline at end of file From 6d6ecd0491e1a440eb68f784c85e8ed4ece407bc Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Thu, 17 Jul 2014 11:58:31 -0600 Subject: [PATCH 09/26] creating a connection per adapter use --- lib/mongoose.js | 15 +++++++++------ test/index.test.js | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index 24c55371..eecb5e41 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -15,8 +15,6 @@ var MongooseService = Proto.extend({ } this.options = _.extend({}, options); - - this._connect(this.options); this.name = modelName; this.type = 'mongoose'; this.schema = new Schema(entity.schema, this.options); @@ -47,13 +45,16 @@ var MongooseService = Proto.extend({ }, this); } - this.model = mongoose.model(this.name, this.schema); + mongoose.model(this.name, this.schema); + this._connect(this.options); + this.model = this.store.model(this.name); }, // NOTE (EK): We create a new database connection for every MongooseService. // This may not be good but... in the mean time the rationale for this // design is because each user of a MongooseService instance could be a separate - // app residing on a totally different server. + // app residing on a totally different server, or each service could talk to + // totally different databases. // TODO (EK): We need to handle replica sets. _connect: function(options) { @@ -79,7 +80,7 @@ var MongooseService = Proto.extend({ // TODO (EK): Support mongoose connection options // http://mongoosejs.com/docs/connections.html - this.store = mongoose.connect(connectionString); + this.store = mongoose.createConnection(connectionString); }, find: function(params, cb) { @@ -188,4 +189,6 @@ module.exports = function(modelName, schema, options) { return Proto.create.call(MongooseService, modelName, schema, options); }; -module.exports.Service = MongooseService; \ No newline at end of file +module.exports.Service = MongooseService; + +module.exports.mongoose = mongoose; \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index 8c62c9b7..870a8cfe 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,5 +1,5 @@ var assert = require('assert'); -var mongooseService = require('../lib'); +var mongooseService = require('../lib/mongoose'); var mongoose = require('mongoose'); var connection = mongoose.connect('mongodb://localhost/test'); From 2da99f24dd32a26b0327aa4748c7fd53195be83b Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Sat, 19 Jul 2014 00:41:27 -0600 Subject: [PATCH 10/26] bumping version to 2.0.0-pre.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27618cca..b25817c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "feathers-mongoose", "description": "Feathers Mongoose ORM service", - "version": "1.2.0", + "version": "2.0.0-pre.1", "homepage": "https://github.com/feathersjs/feathers-mongoose-service", "repository": { "type": "git", From 49f49a63f264a9f4db2364c67997ee41a1c43e24 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 1 Sep 2014 14:43:29 -0600 Subject: [PATCH 11/26] adding the ability to pass an existing mongoose connection --- lib/mongoose.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/mongoose.js b/lib/mongoose.js index eecb5e41..d4e8bea7 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -60,6 +60,11 @@ var MongooseService = Proto.extend({ _connect: function(options) { var connectionString = options.connectionString; + if (options.connection) { + this.store = options.connection; + return; + } + if(!connectionString) { var config = _.extend({ host: 'localhost', From d8b49ec6b49ad35e0f23238835cdd1286ab2b1d3 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Thu, 29 Jan 2015 15:36:32 -0700 Subject: [PATCH 12/26] adding support for objects that are already mongoose schemas --- lib/mongoose.js | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index d4e8bea7..a366931b 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -17,32 +17,38 @@ var MongooseService = Proto.extend({ this.options = _.extend({}, options); this.name = modelName; this.type = 'mongoose'; - this.schema = new Schema(entity.schema, this.options); - if (entity.methods) { - _.each(entity.methods, function(val, key){ - this.schema.methods[key] = val; - }, this); + if (entity instanceof Schema) { + this.schema = entity; } + else { + this.schema = new Schema(entity.schema, this.options); - if (entity.statics) { - _.each(entity.statics, function(val, key){ - this.schema.statics[key] = val; - }, this); - } + if (entity.methods) { + _.each(entity.methods, function(val, key){ + this.schema.methods[key] = val; + }, this); + } - if (entity.virtuals) { - _.each(entity.virtuals, function(val, key){ - _.each(val, function(fn, method){ - this.schema.virtual(key)[method](fn); + if (entity.statics) { + _.each(entity.statics, function(val, key){ + this.schema.statics[key] = val; }, this); - }, this); - } + } - if (entity.indexes) { - _.each(entity.indexes, function(val){ - this.schema.index(val); - }, this); + if (entity.virtuals) { + _.each(entity.virtuals, function(val, key){ + _.each(val, function(fn, method){ + this.schema.virtual(key)[method](fn); + }, this); + }, this); + } + + if (entity.indexes) { + _.each(entity.indexes, function(val){ + this.schema.index(val); + }, this); + } } mongoose.model(this.name, this.schema); From b69a88a42860dfdb34b5e46fc5251bb9474e30a1 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Fri, 30 Jan 2015 00:59:14 -0700 Subject: [PATCH 13/26] adding ability to create multiple docs at the same time --- lib/mongoose.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index a366931b..b19282bd 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -143,12 +143,16 @@ var MongooseService = Proto.extend({ } // Create our actual Model object - var model = new this.model(data); - model.save(function (error, data) { + // TODO(EK): Look at batch insert support + // https://github.com/LearnBoost/mongoose/issues/723 + this.model.create(data, function (error, data) { if (error) { return cb(new errors.BadRequest(error)); } + var args = Array.prototype.slice.call(arguments); + data = args.slice(1, args.length); + cb(null, data.length === 1 ? data[0] : data); }); }, From a8e9ad737e543ce6e6d63e6d5d14910ae0ad6575 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Fri, 30 Jan 2015 12:14:27 -0700 Subject: [PATCH 14/26] adding support for passing in a mongoose model --- lib/mongoose.js | 73 +++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index b19282bd..66015a68 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -15,47 +15,62 @@ var MongooseService = Proto.extend({ } this.options = _.extend({}, options); - this.name = modelName; this.type = 'mongoose'; - if (entity instanceof Schema) { - this.schema = entity; + // It is a mongoose model already + if (entity.modelName) { + this.name = entity.modelName; } + // It's not a model so we need to create one else { - this.schema = new Schema(entity.schema, this.options); + this.name = modelName; - if (entity.methods) { - _.each(entity.methods, function(val, key){ - this.schema.methods[key] = val; - }, this); + if (entity instanceof Schema) { + this.schema = entity; } - - if (entity.statics) { - _.each(entity.statics, function(val, key){ - this.schema.statics[key] = val; - }, this); - } - - if (entity.virtuals) { - _.each(entity.virtuals, function(val, key){ - _.each(val, function(fn, method){ - this.schema.virtual(key)[method](fn); - }, this); - }, this); - } - - if (entity.indexes) { - _.each(entity.indexes, function(val){ - this.schema.index(val); - }, this); + else { + this.schema = this._createSchema(entity); } + + mongoose.model(this.name, this.schema); } - mongoose.model(this.name, this.schema); this._connect(this.options); this.model = this.store.model(this.name); }, + _createSchema: function(entity){ + var schema = new Schema(entity.schema, this.options); + + if (entity.methods) { + _.each(entity.methods, function(val, key){ + schema.methods[key] = val; + }); + } + + if (entity.statics) { + _.each(entity.statics, function(val, key){ + schema.statics[key] = val; + }); + } + + if (entity.virtuals) { + _.each(entity.virtuals, function(val, key){ + _.each(val, function(fn, method){ + schema.virtual(key)[method](fn); + }); + }); + } + + if (entity.indexes) { + _.each(entity.indexes, function(val){ + schema.index(val); + }); + } + + return schema; + }, + // NOTE (EK): We create a new database connection for every MongooseService. // This may not be good but... in the mean time the rationale for this // design is because each user of a MongooseService instance could be a separate @@ -143,8 +158,6 @@ var MongooseService = Proto.extend({ } // Create our actual Model object - // TODO(EK): Look at batch insert support - // https://github.com/LearnBoost/mongoose/issues/723 this.model.create(data, function (error, data) { if (error) { return cb(new errors.BadRequest(error)); From dda51a3eeee60a8f46b7cd80bea24969cccff43a Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Thu, 5 Feb 2015 01:06:04 -0700 Subject: [PATCH 15/26] adding population to get method --- lib/mongoose.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index 66015a68..27bc812a 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -136,18 +136,23 @@ var MongooseService = Proto.extend({ params = {}; } - // TODO (EK): handle params.fields & params.options - this.model.findById(new ObjectId(id), {}, {}, function (error, data) { - if (error) { - return cb(new errors.BadRequest(error)); - } - - if (!data) { - return cb(new errors.NotFound('No record found for id ' + id)); - } + var populate = params.query && params.query.populate ? params.query.populate : ''; - cb(null, data); - }); + // TODO (EK): handle params.fields & params.options + this.model + .findById(new ObjectId(id.toString())) + .populate(populate) + .exec(function (error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + if (!data) { + return cb(new errors.NotFound('No record found for id ' + id)); + } + + cb(null, data); + }); }, // TODO (EK): Batch support for create, update, delete. From d6a623fdabcccfb44cec94943650da0b1e292702 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Fri, 6 Feb 2015 14:40:11 -0700 Subject: [PATCH 16/26] adding filter, sort, limit, skip, populate to find method --- lib/mongoose.js | 55 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/lib/mongoose.js b/lib/mongoose.js index 27bc812a..8dc9cc1b 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -110,19 +110,58 @@ var MongooseService = Proto.extend({ }, find: function(params, cb) { - if(_.isFunction(params)) { + if (_.isFunction(params)) { cb = params; params = {}; } + + var sort = {}; + var populate = ''; + var limit = 100; + var skip = 0; + var select = {}; + + params.query = params.query || {}; + + if (params.query.sort) { + sort = params.query.sort; + delete params.query.sort; + } + + if (params.query.populate) { + populate = params.query.populate; + delete params.query.populate; + } + + if (params.query.limit) { + limit = params.query.limit; + delete params.query.limit; + } + + if (params.query.skip) { + skip = params.query.skip; + delete params.query.skip; + } + + if (params.query.select) { + select = params.query.select; + delete params.query.select; + } - // TODO (EK): handle params.fields & params.options - this.model.find(params.query, {}, {}, function (error, data) { - if (error) { - return cb(new errors.BadRequest(error)); - } + this.model + .find(params.query) + .sort(sort) + .limit(limit) + .skip(skip) + .select(select) + .populate(populate) + .exec(function (error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } - cb(null, data); - }); + cb(null, data); + }); }, get: function(id, params, cb) { From bce0d22acc21715e8cebb23917f4db5e2d7157fa Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 13:32:27 -0600 Subject: [PATCH 17/26] making consistent with other adapters. Adding a proper example --- .editorconfig | 13 -- .jshintrc | 57 +++--- .travis.yml | 9 +- Gruntfile.js | 62 ------ LICENSE | 2 +- README.md | 237 ++++++++++++++++++++-- docs/API.md | 74 ------- docs/Getting-Started.md | 116 ----------- docs/README.md | 5 - examples/basic.js | 41 ++-- examples/models/todo.js | 20 ++ lib/{mongoose.js => feathers-mongoose.js} | 2 +- package.json | 50 ++--- test/{index.test.js => test.js} | 2 +- 14 files changed, 329 insertions(+), 361 deletions(-) delete mode 100644 .editorconfig delete mode 100644 Gruntfile.js delete mode 100644 docs/API.md delete mode 100644 docs/Getting-Started.md delete mode 100644 docs/README.md create mode 100644 examples/models/todo.js rename lib/{mongoose.js => feathers-mongoose.js} (99%) rename test/{index.test.js => test.js} (95%) diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index fe66af08..00000000 --- a/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 4 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false \ No newline at end of file diff --git a/.jshintrc b/.jshintrc index 28f79fb6..3a886033 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,29 +1,32 @@ { - "node": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": "nofunc", - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": true, - "strict": false, - "trailing": true, - "smarttabs": true, - "white": false, - "loopfunc": true, - "globals": { - "it": true, - "describe": true, - "before": true, - "after": true, - "exports": true - } + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": false, + "trailing": true, + "smarttabs": true, + "white": false, + "loopfunc": true, + "expr": true, + "globals": { + "it": true, + "describe": true, + "before": true, + "beforeEach": true, + "after": true, + "afterEach": true, + "exports": true + } } \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0420fd05..72b58790 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ language: node_js +services: mongodb node_js: - - "0.11" - "0.10" - - "0.8" -before_install: - - "npm install -g mocha" -services: - - mongodb + - "node" + - "iojs" \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index f9a0bbe3..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -module.exports = function (grunt) { - - // Project configuration. - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - release: {}, - jshint: { - options: { - jshintrc: '.jshintrc' - }, - lib: ['lib/**/*.js', 'Gruntfile.js'], - test: 'test/**/*.js', - example: 'example/*.js' - }, - simplemocha: { - options: { - - }, - all: { - src: ['test/**/*.js'] - } - }, - watch: { - scripts: { - files: '**/*.js', - tasks: ['jshint', 'express:dev'], - options: { - spawn: false - } - }, - tests: { - files: '**/*.js', - tasks: ['simplemocha'], - options: { - spawn: true - } - } - }, - express: { - options: { - // Override defaults here - }, - dev: { - options: { - script: 'example/index.js' - } - } - } - }); - - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-release'); - grunt.loadNpmTasks('grunt-simple-mocha'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-express-server'); - - grunt.registerTask('test', ['jshint', 'simplemocha']); - grunt.registerTask('default', ['jshint', 'simplemocha', 'express:dev', 'watch']); - -}; \ No newline at end of file diff --git a/LICENSE b/LICENSE index 5497e899..c6c231be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Glavin Wiechert +Copyright (c) 2015 FeathersJS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index e0c07c2c..efea7458 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -[feathers-mongoose](https://github.com/feathersjs/feathers-mongoose) [![NPM version](https://badge.fury.io/js/feathers-mongoose.png)](http://badge.fury.io/js/feathers-mongoose) -========================= +feathers-mongoose +================ + +[![NPM](https://nodei.co/npm/feathers-mongoose.png?downloads=true&stars=true)](https://nodei.co/npm/feathers-mongoose/) [![Build Status](https://travis-ci.org/feathersjs/feathers-mongoose.svg?branch=master)](https://travis-ci.org/feathersjs/feathers-mongoose) + [![Code Climate](https://codeclimate.com/github/feathersjs/feathers-mongoose.png)](https://codeclimate.com/github/feathersjs/feathers-mongoose) -[![Dependency Status](https://david-dm.org/feathersjs/feathers-mongoose.svg)](https://david-dm.org/feathersjs/feathers-mongoose) -[![devDependency Status](https://david-dm.org/feathersjs/feathers-mongoose/dev-status.svg)](https://david-dm.org/feathersjs/feathers-mongoose#info=devDependencies) -[![Total views](https://sourcegraph.com/api/repos/github.com/feathersjs/feathers-mongoose/counters/views.png)](https://sourcegraph.com/github.com/feathersjs/feathers-mongoose) -[![Views in the last 24 hours](https://sourcegraph.com/api/repos/github.com/feathersjs/feathers-mongoose/counters/views-24h.png)](https://sourcegraph.com/github.com/feathersjs/feathers-mongoose) -[![NPM](https://nodei.co/npm/feathers-mongoose.png?downloads=true&stars=true)](https://nodei.co/npm/feathers-mongoose/) -> Easily create a [Mongoose](http://mongoosejs.com/) Service for [Featherjs](https://github.com/feathersjs). +> Create a [Mongoose](http://mongoosejs.com/) ORM wrapped service for [Featherjs](https://github.com/feathersjs). ## Installation @@ -19,16 +17,231 @@ npm install feathers-mongoose --save ``` -See [Getting Started, in the docs/ directory](https://github.com/feathersjs/feathers-mongoose/tree/master/docs/Getting-Started.md). +## Getting Started + +Creating an Mongoose service is this simple: + +```js +var mongooseService = require('feathers-mongoose'); +app.use('todos', mongooseService('todo', todoSchema, options)); +``` + +See [Mongoose Schema Guide](http://mongoosejs.com/docs/guide.html) for more information on defining your schema. + + +### Complete Example + +Here's a complete example of a Feathers server with a `todos` mongoose-service. + +```js +// models/todo.js +var Todo = { + schema: { + title: {type: String, required: true}, + description: {type: String}, + dueDate: {type: Date, 'default': Date.now}, + complete: {type: Boolean, 'default': false} + }, + methods: { + }, + statics: { + }, + virtuals: { + }, + indexes: [ + ] +}; + +// server.js +var feathers = require('feathers'), + bodyParser = require('body-parser'), + mongooseService = require('feathers-mongoose'); + +// Create a feathers instance. +var app = feathers() + // Setup the public folder. + .use(feathers.static(__dirname + '/public')) + // Enable Socket.io + .configure(feathers.socketio()) + // Enable REST services + .configure(feathers.rest()) + // Turn on JSON parser for REST services + .use(bodyParser.json()) + // Turn on URL-encoded parser for REST services + .use(bodyParser.urlencoded({extended: true})) + +// Connect to the db, create and register a Feathers service. +app.use('todos', new mongooseService('todo', Todo)); + +// Start the server. +var port = 8080; +app.listen(port, function() { + console.log('Feathers server listening on port ' + port); +}); +``` + +You can run this example by using `node examples/basic` and going to [localhost:8080/todos](http://localhost:8080/todos). You should see an empty array. That's because you don't have any Todos yet but you now have full CRUD for your new todos service, including mongoose validations! + +### Mongoose Schemas + +The recommended way of defining and passing a model to a `feathers-mongoose` service is shown above. Using object literal syntax makes things easily testable without having to mock out existing mongoose functionality. + +With that said, you have two other options: + +#### Typical Mongoose Schema + +```js +// models/todo.js +var mongoose = require('mongoose'), + Schema = mongoose.Schema; + +var TodoSchema = Schema({ + title: {type: String, required: true}, + description: {type: String}, + dueDate: {type: Date, 'default': Date.now}, + complete: {type: Boolean, 'default': false} +}); + +TodoSchema.methods.isComplete = function() { + return this.complete; +} + +module.exports = TodoSchema; +``` + +Then you can simply pass that to a mongoose service like so: + +```js +//server.js +app.use('todos', mongooseService('todo', TodoSchema)); +``` + +#### A Mongoose Model +Usually before you are able to actually use a mongoose schema you need to turn it into a model. `feathers-mongoose` does that for you but you can also pass a mongoose model explicitly. + +This style is not a whole lot different than above. Note the `mongoose.model()` call. + +```js +// models/todo.js +var mongoose = require('mongoose'), + Schema = mongoose.Schema; + +var TodoSchema = Schema({ + title: {type: String, required: true}, + description: {type: String}, + dueDate: {type: Date, 'default': Date.now}, + complete: {type: Boolean, 'default': false} +}); + +TodoSchema.methods.isComplete = function() { + return this.complete; +} + +module.exports = mongoose.model('Todo', TodoSchema); +``` + +Then you can simply pass that to a mongoose service like so: + +```js +//server.js +app.use('todos', mongooseService('todo', TodoModel)); +``` + + +### Special Query Params +The `find` API allows the use of `$limit`, `$skip`, `$sort`, and `$select` in the query. These special parameters can be passed directly inside the query object: + +```js +// Find all recipes that include salt, limit to 10, only include name field. +{"ingredients":"salt", "$limit":10, "$select":"name:1"} // JSON +GET /?ingredients=salt&%24limit=10&%24select=name%3A1 // HTTP +``` + +As a result of allowing these to be put directly into the query string, you won't want to use `$limit`, `$skip`, `$sort`, or `$select` as the name of fields in your document schema. + +### `$limit` + +`$limit` will return only the number of results you specify: + +``` +// Retrieves the first two records found where age is 37. +query: { + age: 37, + $limit: 2 +} +``` + + +### `$skip` + +`$skip` will skip the specified number of results: + +``` +// Retrieves all except the first two records found where age is 37. +query: { + age: 37, + $skip: 2 +} +``` + + +### `$sort` + +`$sort` will sort based on the object you provide: + +``` +// Retrieves all where age is 37, sorted ascending alphabetically by name. +query: { + age: 37, + $sort: {'name': 1} +} + +// Retrieves all where age is 37, sorted descending alphabetically by name. +query: { + age: 37, + $sort: {'name': -1} +} +``` + + +### `$select` +`$select` support in a query allows you to pick which fields to include or exclude in the results. Note: you can use the include syntax or the exclude syntax, not both together. See the section on [`Select`](http://mongoosejs.com/docs/api.html#query_Query-select) in the Mongoose docs. +``` +// Only retrieve name. +query: { + name: 'Alice', + $select: {'name': 1} +} + +// Retrieve everything except age. +query: { + name: 'Alice', + $select: {'age': 0} +} +``` + + +## API + +`feathers-nedb` services comply with the standard [FeathersJS API](http://feathersjs.com/api/#). + +## Changelog +### 2.0.0 +* Consistency with other service adapters +* Compatibility with Feathers 1.0+ +* Adequate tests -## Documentation +### 0.1.1 +* Fist working release -See the [docs/ directory](https://github.com/feathersjs/feathers-mongoose/tree/master/docs). +### 0.1.0 +* Initial release. ## License [MIT](LICENSE) -## Author +## Authors +[Eric Kryski](http://erickryski.com) [Glavin Wiechert](https://github.com/Glavin001) diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index f65211f6..00000000 --- a/docs/API.md +++ /dev/null @@ -1,74 +0,0 @@ -# API - -All requests below are assumed that *your* service's `path` is `"/path"`. - -For instance, if you added your service with the following `path`: - -```javascript -app.use('/path', yourService) // <-- Register your custom Mongoose-Service with Feathers -``` - -Then the corresponding `GET` request to `find all` of the [documents](http://docs.mongodb.org/manual/core/document/) would be: - -``` -GET /path -``` - -**Important**: Make sure all requests have the `Content-Type` set to `application/json`. See [Feathersjs Issue](https://github.com/feathersjs/feathers/issues/40) for more information. - ------ - -## Finding [Documents](http://docs.mongodb.org/manual/core/document/) - -This query will return all documents at that `path`. - -``` -GET /path -``` - -### Advanced Querying - -You have access to the powerful query language behind Mongoose and MongoDB. - -See [Mongoose's find method](http://mongoosejs.com/docs/api.html#model_Model.find) for details of how it works behind the scenes. - -Example request with all of the optional query parameters: - -``` -GET /path?conditions={}&fields="field1 field 2"&options={"sort":{"field":-1}} -``` - -#### Conditions - -See [Mongoose find](http://mongoosejs.com/docs/api.html#model_Model.find) for details. - -#### Fields - -See [Mongoose field selection](http://mongoosejs.com/docs/api.html#query_Query-select) for details. - -#### Options - -See [Mongoose options](http://mongoosejs.com/docs/api.html#query_Query-setOptions) for details. - -## Finding a Specific [Document](http://docs.mongodb.org/manual/core/document/) by Id - -``` -GET /path/:id -``` - -### Advanced Querying - -See [Advanced Querying, above](#Advanced-Querying). - - -## Updating a Specific [Document](http://docs.mongodb.org/manual/core/document/) by Id - -``` -PUT /path/:id -``` - -## Deleting a Specific [Document](http://docs.mongodb.org/manual/core/document/) by Id - -``` -DELETE /path/:id -``` diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md deleted file mode 100644 index e72f2b3e..00000000 --- a/docs/Getting-Started.md +++ /dev/null @@ -1,116 +0,0 @@ -# Getting Started - -## Usage - -> **Important**: Read over the [Feathers documentation](http://feathersjs.com/#documentation), specifically the section on [Services](http://feathersjs.com/#toc12). All of the Feathers REST and Socket API requests are supported. - -### Run example: - -Run the following: - -``` -node example/index.js -``` - -Open your browser to [http://localhost:8080/user](http://localhost:8080/user). -There will likely be an empty array in response. This is because you have no `User`s, yet. - -I recommend the [Postman extension for Chrome](https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm) if you would like an easy to use REST Client. - -### How To Create a Mongoose Service - -```javascript -var customService = new mongooseService(modelName, schema, mongoose); -``` - -See [Mongoose Schema Guide](http://mongoosejs.com/docs/guide.html) for more information on defining your schema. - -> Skip to [Step 3](#3-create-your-custom-mongoose-service) for the important part. - -#### 1) Get your dependencies. - -```javascript -// Get Feathers -var feathers = require('feathers'); -// Get Mongoose -var mongoose = require('mongoose'); -// Get Mongoose-Service -var mongooseService = require('feathers-mongoose-service'); -``` - -#### 2) Create your Mongoose connection to Mongodb. - -```javascript -// Get Mongoose -var mongoose = require('mongoose'); -// Connect to your MongoDB -mongoose.connect('mongodb://localhost/test'); -``` - -#### 3) **Create your custom Mongoose Service** - -```javascript -// Create your Mongoose-Service, for a `User` -var userService = mongooseService('user', { - email: {type : String, required : true, index: {unique: true, dropDups: true}}, - firstName: {type : String, required : true}, - lastName: {type : String, required : true}, - age: {type : Number, required : true}, - password: {type : String, required : true, select: false}, - skills: {type : Array, required : true} - }, mongoose); -``` - -#### 4) Use your service with Feathers. - -```javascript -// Setup Feathers -var app = feathers(); -// Configure Feathers -app.use(feathers.logger('dev')); // For debugging purposes. -// ................ -var port = 8080; -// ................ -app.configure(feathers.socketio()) - .use('/user', userService) // <-- Register your custom Mongoose-Service with Feathers - .listen(port, function() { - console.log('Express server listening on port ' + port); - }); -``` - -### Complete Working Example code: - -```javascript -// Get Feathers -var feathers = require('feathers'); -// Get Mongoose -var mongoose = require('mongoose'); -// Get Mongoose-Service -var mongooseService = require('feathers-mongoose-service'); -// Connect to your MongoDB -mongoose.connect('mongodb://localhost/test'); - -// Create your Mongoose-Service, for a `User` -var userService = mongooseService('user', { - email: {type : String, required : true, index: {unique: true, dropDups: true}}, - firstName: {type : String, required : true}, - lastName: {type : String, required : true}, - age: {type : Number, required : true}, - password: {type : String, required : true, select: false}, - skills: {type : Array, required : true} - }, mongoose); - -// Setup Feathers -var app = feathers(); - -// Configure Feathers -app.use(feathers.logger('dev')); // For debugging purposes. -// ................ -var port = 8080; -// ................ -app.configure(feathers.socketio()) - .use('/user', userService) // <-- Register your custom Mongoose-Service with Feathers - .listen(port, function() { - console.log('Express server listening on port ' + port); - }); -``` diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 693703dd..00000000 --- a/docs/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Documentation - -See [Getting-Started.md](Getting-Started.md) for instructions on using `Mongoose-Service` in your Node.js server. - -See [API.md](API.md) for details about the API and the many forms of queries that you can utilize. diff --git a/examples/basic.js b/examples/basic.js index cae886ad..77590e73 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -1,21 +1,26 @@ -var feathers = require('feathers'); -var mongooseService = require('../lib/mongoose'); -var bodyParser = require('body-parser'); +var feathers = require('feathers'), + bodyParser = require('body-parser'), + mongooseService = require('../lib/feathers-mongoose'), + Todo = require('./models/todo'); -// Create your Mongoose-Service, for a `User` -var userService = mongooseService('user', { - email: {type : String, required : true, index: {unique: true, dropDups: true}}, - firstName: {type : String, required : true}, - lastName: {type : String, required : true} - }); +// Create a feathers instance. +var app = feathers() + // Setup the public folder. + .use(feathers.static(__dirname + '/public')) + // Enable Socket.io + .configure(feathers.socketio()) + // Enable REST services + .configure(feathers.rest()) + // Turn on JSON parser for REST services + .use(bodyParser.json()) + // Turn on URL-encoded parser for REST services + .use(bodyParser.urlencoded({extended: true})) -// Setup Feathers -var app = feathers(); +// Connect to the db, create and register a Feathers service. +app.use('todos', new mongooseService('todo', Todo)); -app.configure(feathers.rest()) - .use(bodyParser.json()) - .use('/users', userService) - .configure(feathers.errors()) - .listen(8080); - -console.log('App listening on 127.0.0.1:8080'); +// Start the server. +var port = 8080; +app.listen(port, function() { + console.log('Feathers server listening on port ' + port); +}); \ No newline at end of file diff --git a/examples/models/todo.js b/examples/models/todo.js new file mode 100644 index 00000000..c76823d3 --- /dev/null +++ b/examples/models/todo.js @@ -0,0 +1,20 @@ +var mongoose = require('mongoose'); + +var Todo = { + schema: { + title: {type: String, required: true}, + description: {type: String}, + dueDate: {type: Date, 'default': Date.now}, + complete: {type: Boolean, 'default': false} + }, + methods: { + }, + statics: { + }, + virtuals: { + }, + indexes: [ + ] +}; + +module.exports = Todo; \ No newline at end of file diff --git a/lib/mongoose.js b/lib/feathers-mongoose.js similarity index 99% rename from lib/mongoose.js rename to lib/feathers-mongoose.js index 8dc9cc1b..5b4cccaf 100644 --- a/lib/mongoose.js +++ b/lib/feathers-mongoose.js @@ -6,7 +6,7 @@ var Schema = mongoose.Schema; var ObjectId = mongoose.Types.ObjectId; var MongooseService = Proto.extend({ - // TODO (EK): How do we handle indexes? + init: function(modelName, entity, options) { options = options || {}; diff --git a/package.json b/package.json index b25817c5..6b57fc5c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,16 @@ { "name": "feathers-mongoose", "description": "Feathers Mongoose ORM service", - "version": "2.0.0-pre.1", - "homepage": "https://github.com/feathersjs/feathers-mongoose-service", + "version": "2.0.0", + "homepage": "https://github.com/feathersjs/feathers-mongoose", "repository": { "type": "git", - "url": "git://github.com/feathersjs/feathers-mongoose-service.git" + "url": "git://github.com/feathersjs/feathers-mongoose.git" }, + "bugs": { + "url": "https://github.com/feathersjs/feathers-mongoose/issues" + }, + "license": "MIT", "keywords": [ "feathers", "feathers-plugin", @@ -20,39 +24,35 @@ ], "author": "Feathers (http://feathersjs.com)", "contributors": [ - "Glavin Wiechert (https://github.com/Glavin001)", - "Eric Kryski (http://erickryski.com)" + "Eric Kryski (http://erickryski.com)", + "Glavin Wiechert (https://github.com/Glavin001)" ], - "license": "MIT", - "bugs": { - "url": "https://github.com/feathersjs/feathers-mongoose-service/issues" - }, - "main": "lib/mongoose.js", + "main": "lib/feathers-mongoose.js", "scripts": { - "test": "grunt test" + "publish": "git push origin --tags", + "release:patch": "npm version patch && npm publish", + "release:minor": "npm version minor && npm publish", + "release:major": "npm version major && npm publish", + "jshint": "jshint lib/. test/. --config", + "mocha": "mocha test/ --recursive --timeout 5000", + "test": "npm run jshint && npm run mocha" }, "engines": { "node": "~0.10.0", "npm": "~1.4.0" }, - "devDependencies": { - "body-parser": "^1.4.3", - "feathers": ">= 1.0.x", - "grunt": "~0.4.1", - "grunt-cli": "~0.1.7", - "grunt-contrib-jshint": "~0.x", - "grunt-contrib-watch": "~0.5.3", - "grunt-express-server": "~0.4.11", - "grunt-release": "~0.7.0", - "grunt-simple-mocha": "~0.4.0", - "mocha": "*" - }, "dependencies": { + "feathers-errors": "^0.2.5", "lodash": "^2.4.1", "mongoose": "^3.8.13", "uberproto": "^1.1.2" }, - "peerDependencies": { - "feathers": ">= 1.0.x" + "devDependencies": { + "async": "^1.3.0", + "body-parser": "^1.13.2", + "chai": "^3.0.0", + "feathers": "^1.1.0-pre.0", + "jshint": "^2.8.0", + "mocha": "^2.2.5" } } diff --git a/test/index.test.js b/test/test.js similarity index 95% rename from test/index.test.js rename to test/test.js index 870a8cfe..49fc5c45 100644 --- a/test/index.test.js +++ b/test/test.js @@ -1,5 +1,5 @@ var assert = require('assert'); -var mongooseService = require('../lib/mongoose'); +var mongooseService = require('../lib/feathers-mongoose'); var mongoose = require('mongoose'); var connection = mongoose.connect('mongodb://localhost/test'); From 684917bf41e4589dab3fc529c0ca88079d83a7ae Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 13:37:55 -0600 Subject: [PATCH 18/26] adding example of methods and indexes for object literal style --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index efea7458..bada2be4 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ var mongooseService = require('feathers-mongoose'); app.use('todos', mongooseService('todo', todoSchema, options)); ``` -See [Mongoose Schema Guide](http://mongoosejs.com/docs/guide.html) for more information on defining your schema. +See the [Mongoose Schema Guide](http://mongoosejs.com/docs/guide.html) for more information on defining your schema. ### Complete Example @@ -43,12 +43,16 @@ var Todo = { complete: {type: Boolean, 'default': false} }, methods: { + isComplete: function(){ + return this.complete; + } }, statics: { }, virtuals: { }, indexes: [ + {'complete': true, background: true} ] }; @@ -106,6 +110,8 @@ TodoSchema.methods.isComplete = function() { return this.complete; } +TodoSchema.index({'complete': true, background: true}); + module.exports = TodoSchema; ``` @@ -137,6 +143,8 @@ TodoSchema.methods.isComplete = function() { return this.complete; } +TodoSchema.index({'complete': true, background: true}); + module.exports = mongoose.model('Todo', TodoSchema); ``` @@ -148,6 +156,10 @@ app.use('todos', mongooseService('todo', TodoModel)); ``` +### Custom Validation + +TODO (EK): Add example with custom validations using `node-validator` or something. + ### Special Query Params The `find` API allows the use of `$limit`, `$skip`, `$sort`, and `$select` in the query. These special parameters can be passed directly inside the query object: From b1f92e9ec5b0e9879683b5bf1aa9ecfcb0716c0c Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 15:08:30 -0600 Subject: [PATCH 19/26] adding codeclimate.yml --- .codeclimate.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..719c2998 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,4 @@ +languages: + JavaScript: true +# exclude_paths: +# - "foo/bar.rb" \ No newline at end of file From f691848bbbf85683ca4638e286627c1f41833d64 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 18:46:25 -0600 Subject: [PATCH 20/26] updating docs with the special filters --- README.md | 54 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index bada2be4..060925af 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,10 @@ feathers-mongoose [![NPM](https://nodei.co/npm/feathers-mongoose.png?downloads=true&stars=true)](https://nodei.co/npm/feathers-mongoose/) [![Build Status](https://travis-ci.org/feathersjs/feathers-mongoose.svg?branch=master)](https://travis-ci.org/feathersjs/feathers-mongoose) - [![Code Climate](https://codeclimate.com/github/feathersjs/feathers-mongoose.png)](https://codeclimate.com/github/feathersjs/feathers-mongoose) -> Create a [Mongoose](http://mongoosejs.com/) ORM wrapped service for [Featherjs](https://github.com/feathersjs). +> Create a [Mongoose](http://mongoosejs.com/) ORM wrapped service for [FeathersJS](https://github.com/feathersjs). ## Installation @@ -28,7 +27,6 @@ app.use('todos', mongooseService('todo', todoSchema, options)); See the [Mongoose Schema Guide](http://mongoosejs.com/docs/guide.html) for more information on defining your schema. - ### Complete Example Here's a complete example of a Feathers server with a `todos` mongoose-service. @@ -86,13 +84,15 @@ app.listen(port, function() { You can run this example by using `node examples/basic` and going to [localhost:8080/todos](http://localhost:8080/todos). You should see an empty array. That's because you don't have any Todos yet but you now have full CRUD for your new todos service, including mongoose validations! -### Mongoose Schemas +## Options + +## Mongoose Schemas The recommended way of defining and passing a model to a `feathers-mongoose` service is shown above. Using object literal syntax makes things easily testable without having to mock out existing mongoose functionality. With that said, you have two other options: -#### Typical Mongoose Schema +### Passing a Mongoose Schema ```js // models/todo.js @@ -122,7 +122,7 @@ Then you can simply pass that to a mongoose service like so: app.use('todos', mongooseService('todo', TodoSchema)); ``` -#### A Mongoose Model +### Passing a Mongoose Model Usually before you are able to actually use a mongoose schema you need to turn it into a model. `feathers-mongoose` does that for you but you can also pass a mongoose model explicitly. This style is not a whole lot different than above. Note the `mongoose.model()` call. @@ -156,12 +156,12 @@ app.use('todos', mongooseService('todo', TodoModel)); ``` -### Custom Validation +## Custom Validation TODO (EK): Add example with custom validations using `node-validator` or something. -### Special Query Params -The `find` API allows the use of `$limit`, `$skip`, `$sort`, and `$select` in the query. These special parameters can be passed directly inside the query object: +## Special Query Params +The `find` API allows the use of `$limit`, `$skip`, `$sort`, `$select`, `$populate` in the query. These special parameters can be passed directly inside the query object: ```js // Find all recipes that include salt, limit to 10, only include name field. @@ -169,7 +169,7 @@ The `find` API allows the use of `$limit`, `$skip`, `$sort`, and `$select` in th GET /?ingredients=salt&%24limit=10&%24select=name%3A1 // HTTP ``` -As a result of allowing these to be put directly into the query string, you won't want to use `$limit`, `$skip`, `$sort`, or `$select` as the name of fields in your document schema. +As a result of allowing these to be put directly into the query string, you won't want to use `$limit`, `$skip`, `$sort`, `$select`, or `$populate` as the name of fields in your document schema. ### `$limit` @@ -217,7 +217,7 @@ query: { ### `$select` -`$select` support in a query allows you to pick which fields to include or exclude in the results. Note: you can use the include syntax or the exclude syntax, not both together. See the section on [`Select`](http://mongoosejs.com/docs/api.html#query_Query-select) in the Mongoose docs. +`$select` support in a query allows you to pick which fields to include or exclude in the results. **Note:** you can use the include syntax or the exclude syntax, not both together. See the section on [`Select`](http://mongoosejs.com/docs/api.html#query_Query-select) in the Mongoose docs. ``` // Only retrieve name. query: { @@ -232,22 +232,38 @@ query: { } ``` +### `$populate` +`$populate` support in a query allows you to populate an embedded document and return it in the results. See the section on [`Population`](http://mongoosejs.com/docs/populate.html) in the Mongoose docs. +``` +// Return people named "Alice" and her children. +query: { + name: 'Alice', + $populate: ['children'] +} +``` + ## API -`feathers-nedb` services comply with the standard [FeathersJS API](http://feathersjs.com/api/#). +`feathers-mongoose` services comply with the standard [FeathersJS API](http://feathersjs.com/docs). ## Changelog ### 2.0.0 -* Consistency with other service adapters -* Compatibility with Feathers 1.0+ -* Adequate tests +- Consistency with other service adapters +- Compatibility with Feathers 1.0+ +- Adequate tests +- Add special query params: + - $sort + - $skip + - $limit + - $select + - $populate ### 0.1.1 -* Fist working release +- First working release ### 0.1.0 -* Initial release. +- Initial release. ## License @@ -255,5 +271,5 @@ query: { ## Authors -[Eric Kryski](http://erickryski.com) -[Glavin Wiechert](https://github.com/Glavin001) +- [Eric Kryski](http://erickryski.com) +- [Glavin Wiechert](https://github.com/Glavin001) From 70a09fcc766457406435f4ca431f2ed37d6cda52 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 18:46:34 -0600 Subject: [PATCH 21/26] updating deps --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 6b57fc5c..44d70143 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,9 @@ "uberproto": "^1.1.2" }, "devDependencies": { - "async": "^1.3.0", "body-parser": "^1.13.2", "chai": "^3.0.0", - "feathers": "^1.1.0-pre.0", + "feathers": "^1.1.0", "jshint": "^2.8.0", "mocha": "^2.2.5" } From 80b05fd81def66c472d05e0d1d85c14bb158f874 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Mon, 27 Jul 2015 18:56:23 -0600 Subject: [PATCH 22/26] documenting connection options --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 060925af..4e72aaf0 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,23 @@ You can run this example by using `node examples/basic` and going to [localhost: ## Options +The following options can be passed when creating a new Mongoose service: + +**General options:** + +- `connectionString` - A MongoDB connection string +- `username` - MongoDB username +- `password` - MongoDB password +- `autoreconnect` - Whether MongoDB you reconnect automatically (default: `true`) + +**Connection options**: (when `connectionString` is not set) + +- `db` - The name of the database (default: `"feathers"`) +- `host` - The MongoDB host (default: `"localhost"`) +- `port` - The MongoDB port (default: `27017`) + +Additionally you can pass an existing mongoose connection via the `connection` property. + ## Mongoose Schemas The recommended way of defining and passing a model to a `feathers-mongoose` service is shown above. Using object literal syntax makes things easily testable without having to mock out existing mongoose functionality. From e521a269f5c923b8a5d61bb4707f3ea37fad590e Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 4 Aug 2015 23:22:30 -0600 Subject: [PATCH 23/26] updating mongoose latest stable --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 44d70143..53ccc4ca 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,9 @@ }, "dependencies": { "feathers-errors": "^0.2.5", + "feathers-query-filters": "^1.1.1", "lodash": "^2.4.1", - "mongoose": "^3.8.13", + "mongoose": "^4.1.0", "uberproto": "^1.1.2" }, "devDependencies": { From b48856115dfb6091b4d2d1007049e64951e82f55 Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 4 Aug 2015 23:22:53 -0600 Subject: [PATCH 24/26] updating examples with proper indexes --- README.md | 21 +++++++++++---------- examples/basic.js | 4 +++- examples/models/todo.js | 4 +++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4e72aaf0..1ecf692e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ var Todo = { title: {type: String, required: true}, description: {type: String}, dueDate: {type: Date, 'default': Date.now}, - complete: {type: Boolean, 'default': false} + complete: {type: Boolean, 'default': false, index: true} }, methods: { isComplete: function(){ @@ -50,7 +50,7 @@ var Todo = { virtuals: { }, indexes: [ - {'complete': true, background: true} + {'dueDate': -1, background: true} ] }; @@ -91,17 +91,17 @@ The following options can be passed when creating a new Mongoose service: **General options:** - `connectionString` - A MongoDB connection string -- `username` - MongoDB username -- `password` - MongoDB password -- `autoreconnect` - Whether MongoDB you reconnect automatically (default: `true`) **Connection options**: (when `connectionString` is not set) +- `username` - MongoDB username +- `password` - MongoDB password - `db` - The name of the database (default: `"feathers"`) - `host` - The MongoDB host (default: `"localhost"`) - `port` - The MongoDB port (default: `27017`) +- `reconnect` - Whether the connection should automatically reconnect (default: `true`) -Additionally you can pass an existing mongoose connection via the `connection` property. +**Note:** By default, each service creates its own database connection. If you don't want this you can pass an existing mongoose connection via the `connection` property. ## Mongoose Schemas @@ -120,14 +120,14 @@ var TodoSchema = Schema({ title: {type: String, required: true}, description: {type: String}, dueDate: {type: Date, 'default': Date.now}, - complete: {type: Boolean, 'default': false} + complete: {type: Boolean, 'default': false, index: true} }); TodoSchema.methods.isComplete = function() { return this.complete; } -TodoSchema.index({'complete': true, background: true}); +TodoSchema.index({'dueDate': -1, background: true}); module.exports = TodoSchema; ``` @@ -153,14 +153,14 @@ var TodoSchema = Schema({ title: {type: String, required: true}, description: {type: String}, dueDate: {type: Date, 'default': Date.now}, - complete: {type: Boolean, 'default': false} + complete: {type: Boolean, 'default': false, index: true} }); TodoSchema.methods.isComplete = function() { return this.complete; } -TodoSchema.index({'complete': true, background: true}); +TodoSchema.index({'dueDate': -1, background: true}); module.exports = mongoose.model('Todo', TodoSchema); ``` @@ -269,6 +269,7 @@ query: { - Consistency with other service adapters - Compatibility with Feathers 1.0+ - Adequate tests +- Autoreconnect by default when not passing a connection string - Add special query params: - $sort - $skip diff --git a/examples/basic.js b/examples/basic.js index 77590e73..1fd8a0f4 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -17,7 +17,9 @@ var app = feathers() .use(bodyParser.urlencoded({extended: true})) // Connect to the db, create and register a Feathers service. -app.use('todos', new mongooseService('todo', Todo)); +app.use('todos', mongooseService('todo', Todo)); + +app.configure(feathers.errors()); // Start the server. var port = 8080; diff --git a/examples/models/todo.js b/examples/models/todo.js index c76823d3..600944b4 100644 --- a/examples/models/todo.js +++ b/examples/models/todo.js @@ -3,9 +3,10 @@ var mongoose = require('mongoose'); var Todo = { schema: { title: {type: String, required: true}, + author: {type: String, required: true}, description: {type: String}, dueDate: {type: Date, 'default': Date.now}, - complete: {type: Boolean, 'default': false} + complete: {type: Boolean, 'default': false, index: true} }, methods: { }, @@ -14,6 +15,7 @@ var Todo = { virtuals: { }, indexes: [ + {'dueDate': -1, background: true} ] }; From 8c4f9915f69935f4966000f5dd107db8379455ad Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 4 Aug 2015 23:23:32 -0600 Subject: [PATCH 25/26] Getting special query filters working and fixing a few bugs --- lib/feathers-mongoose.js | 99 ++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/lib/feathers-mongoose.js b/lib/feathers-mongoose.js index 5b4cccaf..297d9bc7 100644 --- a/lib/feathers-mongoose.js +++ b/lib/feathers-mongoose.js @@ -1,7 +1,8 @@ var _ = require('lodash'); var Proto = require('uberproto'); var mongoose = require('mongoose'); -var errors = require('feathers').errors.types; +var errors = require('feathers-errors').types; +var filter = require('feathers-query-filters'); var Schema = mongoose.Schema; var ObjectId = mongoose.Types.ObjectId; @@ -10,7 +11,7 @@ var MongooseService = Proto.extend({ init: function(modelName, entity, options) { options = options || {}; - if(typeof modelName !== 'string') { + if (!_.isString(modelName)) { throw new errors.GeneralError('Must provide a valid model name'); } @@ -86,22 +87,25 @@ var MongooseService = Proto.extend({ return; } - if(!connectionString) { + if (!connectionString) { var config = _.extend({ host: 'localhost', port: 27017, - db: 'feathers' + db: 'feathers', + reconnect: true }, options); - connectionString = config.host + ':' + config.port + '/' + config.db; - } + connectionString = 'mongodb://'; - if(options.username && options.password) { - connectionString += options.username + ':' + options.password + '@'; - } + if (config.username && config.password) { + connectionString += config.username + ':' + config.password + '@'; + } + + connectionString += config.host + ':' + config.port + '/' + config.db; - if(options.reconnect) { - connectionString += '?auto_reconnect=true'; + if (config.reconnect) { + connectionString += '?auto_reconnect=true'; + } } // TODO (EK): Support mongoose connection options @@ -115,53 +119,44 @@ var MongooseService = Proto.extend({ params = {}; } - var sort = {}; - var populate = ''; - var limit = 100; - var skip = 0; - var select = {}; - params.query = params.query || {}; + var filters = filter(params.query); - if (params.query.sort) { - sort = params.query.sort; - delete params.query.sort; + var query = this.model.find(params.query); + + if (filters.$select && filters.$select.length) { + var fields = {}; + + _.each(filters.$select, function(key){ + fields[key] = 1; + }); + + query.select(fields); } - if (params.query.populate) { - populate = params.query.populate; - delete params.query.populate; + if (filters.$sort){ + query.sort(filters.$sort); } - if (params.query.limit) { - limit = params.query.limit; - delete params.query.limit; + if (filters.$limit){ + query.limit(filters.$limit); } - if (params.query.skip) { - skip = params.query.skip; - delete params.query.skip; + if (filters.$skip){ + query.skip(filters.$skip); } - if (params.query.select) { - select = params.query.select; - delete params.query.select; + if (filters.$populate){ + query.populate(filters.$populate); } - - this.model - .find(params.query) - .sort(sort) - .limit(limit) - .skip(skip) - .select(select) - .populate(populate) - .exec(function (error, data) { - if (error) { - return cb(new errors.BadRequest(error)); - } - cb(null, data); - }); + query.exec(function(error, data) { + if (error) { + return cb(new errors.BadRequest(error)); + } + + cb(null, data); + }); }, get: function(id, params, cb) { @@ -215,16 +210,22 @@ var MongooseService = Proto.extend({ }, patch: function(id, data, params, cb) { - this.update.apply(this, arguments); + this.update.call(this, id, data, params, cb); }, update: function(id, data, params, cb) { - if(_.isFunction(params)) { + if (_.isFunction(data)) { + cb = data; + return cb(new errors.BadRequest('You need to provide data to be updated')); + } + + if (_.isFunction(params)) { cb = params; params = {}; } - this.model.findByIdAndUpdate(new ObjectId(id), data, { upsert: true }, function(error, data) { + // TODO (EK): Support updating multiple docs. Maybe we just use feathers-batch + this.model.findByIdAndUpdate(new ObjectId(id), data, {new: true}, function(error, data) { if (error) { return cb(new errors.BadRequest(error)); } From ac56d920c4b6221d361e3a6284e7f7313e6cccad Mon Sep 17 00:00:00 2001 From: Eric Kryski Date: Tue, 4 Aug 2015 23:23:39 -0600 Subject: [PATCH 26/26] Adding tests and fixtures --- test/fixtures.js | 27 +++ test/test.js | 533 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 528 insertions(+), 32 deletions(-) create mode 100644 test/fixtures.js diff --git a/test/fixtures.js b/test/fixtures.js new file mode 100644 index 00000000..6265f342 --- /dev/null +++ b/test/fixtures.js @@ -0,0 +1,27 @@ +module.exports = { + singlePost: { + title: 'First Post', + author: 'Alice' + }, + multiplePosts: [ + { + title: 'Second Post', + author: 'Bob' + }, + { + title: 'Third and Final Post', + author: 'Doug' + } + ], + invalidPost: [ + { + title: 'Invalid Post' + } + ], + comments: [ + { + content: 'First comment', + commenter: 'Anonymous' + } + ] +}; \ No newline at end of file diff --git a/test/test.js b/test/test.js index 49fc5c45..776b48d7 100644 --- a/test/test.js +++ b/test/test.js @@ -1,47 +1,516 @@ -var assert = require('assert'); +var chai = require('chai'); +var expect = chai.expect; +var Fixtures = require('./fixtures'); +var errors = require('feathers-errors').types; var mongooseService = require('../lib/feathers-mongoose'); var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var _ids = {}; +var Comment = { + schema: { + content: {type: String, required: true}, + commenter: {type: String, required: true} + } +}; -var connection = mongoose.connect('mongodb://localhost/test'); +var Post = { + schema: { + title: {type: String, required: true}, + author: {type: String, required: true}, + published: {type: Boolean, 'default': false}, + comments: [{type: Schema.ObjectId, ref: 'comment'}] + }, + methods: { + instanceMethod: function(){} + }, + statics: { + staticMethod: function(){} + }, + virtuals: { + 'virtualAttribute': { + get: function(){ return 'virtual'; } + } + }, + indexes: [ + {'published': 1, background: true} + ] +}; -describe('Usage', function() { - - /* - // Old usage - describe('#new(connection)', function() { - it('should return a new Mongoose Service', function() { +describe('Feathers Mongoose Service', function() { - var service = new mongooseService('test', { - field: {type: String} - }, connection); + describe('#init', function() { + beforeEach(function(){ + mongoose.models = {}; + mongoose.modelSchemas = {}; + mongoose.connection.models = {}; + mongoose.connection.collections = {}; + }); + + describe('without model name', function() { + it('throws an error', function() { + expect(mongooseService).to.throw('Must provide a valid model name'); + }); + }); + + describe('with model name', function() { + it('sets the type', function(){ + var service = mongooseService('post', Post, {db: 'test'}); + expect(service.type).to.equal('mongoose'); + }); + + it('sets custom options', function(){ + var service = mongooseService('post', Post, {db: 'test'}); + expect(service.options.db).equal('test'); + }); + + it('changes a default mongoose option', function(){ + var service = mongooseService('post', Post, {versionKey: '__version'}); + expect(service.options.versionKey).equal('__version'); + }); + + describe('when entity is mongoose model', function() { + it('sets the name', function(){ + var PostSchema = mongoose.Schema(Post.schema); + var PostModel = mongoose.model('post', PostSchema); + var service = mongooseService('post', PostModel, {db: 'test'}); + expect(service.name).to.equal('post'); + }); + }); + + describe('when entity is a mongoose schema', function() { + it('sets the name', function(){ + var PostSchema = mongoose.Schema(Post.schema); + var service = mongooseService('post', PostSchema, {db: 'test'}); + expect(service.name).to.equal('post'); + }); + }); + + describe('when entity is an object literal', function() { + it('sets the name', function(){ + var service = mongooseService('post', Post, {db: 'test'}); + expect(service.name).to.equal('post'); + }); + + it('sets any instance methods', function() { + var service = mongooseService('post', Post, {db: 'test'}); + var post = new service.model({title: 'A post', author: 'Ernest'}); + expect(post.instanceMethod).to.not.be.undefined; + }); + + it('sets any static methods', function() { + var service = mongooseService('post', Post, {db: 'test'}); + expect(service.model.staticMethod).to.not.be.undefined; + }); - if (!!service) { - // passed - } - assert.equal(-1, [1,2,3].indexOf(5)); - assert.equal(-1, [1,2,3].indexOf(0)); + it('sets any virtual methods', function() { + var service = mongooseService('post', Post, {db: 'test'}); + var post = new service.model({title: 'A post', author: 'Ernest'}); + expect(post.virtualAttribute).to.equal('virtual'); + }); + + it('sets any indexes', function() { + mongooseService('post', Post, {db: 'test'}); + var indexes = mongoose.modelSchemas.post._indexes; + expect(indexes.length).to.not.equal(0); + }); + }); + }); + + describe('with existing connection', function() { + it('uses the existing connection', function(done) { + var connection = mongoose.createConnection('mongodb://localhost:27017/test'); + var service = mongooseService('post', Post, {connection: connection}); + + service.find({}, function(error, data){ + expect(error).to.be.null; + expect(data).to.be.ok; + done(); + }); + }); + }); + + describe('with connection string', function() { + it('sets up a connection', function(done) { + var service = mongooseService('post', Post, {db: 'test'}); + service.find({}, function(error, data){ + expect(error).to.be.null; + expect(data).to.be.ok; + done(); }); + }); }); - */ - describe('Mongoose - Check Connection', function() { - it('should have a valid connection', function() { - assert.equal(true, !!connection); + describe('without connection string', function() { + it('sets up a default connection', function(done) { + var service = mongooseService('post', Post); + + service.find({}, function(error, data){ + expect(error).to.be.null; + expect(data).to.be.ok; + done(); }); + }); + + it('sets up a custom connection', function(done) { + var options = { + host: 'localhost', + port: 27017, + db: 'test', + reconnect: false + }; + + var service = mongooseService('post', Post, options); + + service.find({}, function(error, data){ + expect(error).to.be.null; + expect(data).to.be.ok; + done(); + }); + }); }); + }); + + describe('CRUD methods', function(){ + before(function(){ + mongoose.models = {}; + mongoose.modelSchemas = {}; + mongoose.connection.models = {}; + mongoose.connection.collections = {}; + this.service = mongooseService('post', Post); + this.commentService = mongooseService('comment', Comment); + }); + + beforeEach(function(done) { + this.service.create(Fixtures.singlePost, function(error, data) { + if (error) { + console.error(error); + } + + _ids.FirstPost = data._id; + done(); + }); + }); + + afterEach(function(done) { + this.service.remove(_ids.FirstPost, function() { + done(); + }); + }); + + describe('#find', function() { + beforeEach(function(done) { + var secondPost = Fixtures.multiplePosts[0]; + var thirdPost = Fixtures.multiplePosts[1]; + + this.commentService.create(Fixtures.comments[0], function(err, comment){ + secondPost.comments = [comment._id]; + + this.service.create(secondPost, function(err, post) { + _ids.SecondPost = post._id; + + this.service.create(thirdPost, function(err, post) { + _ids.ThirdPost = post._id; + done(); + }); + }.bind(this)); + }.bind(this)); + }); + + afterEach(function(done) { + this.service.remove(_ids.SecondPost, function() { + this.service.remove(_ids.ThirdPost, function() { + done(); + }); + }.bind(this)); + }); + + it('returns all items', function(done) { + this.service.find({}, function(error, data) { + expect(error).to.be.null; + expect(data).to.be.instanceof(Array); + expect(data.length).to.equal(3); + done(); + }); + }); + + it('filters results by query parameters', function(done) { + this.service.find({ query: { title: 'First Post' } }, function(error, data) { + expect(error).to.be.null; + expect(data).to.be.instanceof(Array); + expect(data.length).to.equal(1); + expect(data[0].title).to.equal('First Post'); + done(); + }); + }); + + it('supports sorting', function(done) { + var params = { + query: { + $sort: {author: 1} + } + }; + + this.service.find(params, function(error, data) { + expect(error).to.be.null; + expect(data.length).to.equal(3); + expect(data[0].author).to.equal('Alice'); + expect(data[1].author).to.equal('Bob'); + expect(data[2].author).to.equal('Doug'); + done(); + }); + }); + + it('supports limiting', function(done) { + var params = { + query: { + $limit: 2 + } + }; + + this.service.find(params, function(error, data) { + expect(error).to.be.null; + expect(data.length).to.equal(2); + done(); + }); + }); + + it('supports skipping', function(done) { + var params = { + query: { + $sort: {author: 1}, + $skip: 1 + } + }; + + this.service.find(params, function(error, data) { + expect(error).to.be.null; + expect(data.length).to.equal(2); + expect(data[0].author).to.equal('Bob'); + expect(data[1].author).to.equal('Doug'); + done(); + }); + }); + + it('supports selecting specific fields', function(done) { + var params = { + query: { + author: 'Alice', + $select: ['title'] + } + }; + + this.service.find(params, function(error, data) { + expect(error).to.be.null; + expect(data.length).to.equal(1); + expect(data[0].title).to.equal('First Post'); + expect(data[0].author).to.be.undefined; + done(); + }); + }); + + it('supports populating sub documents', function(done) { + var params = { + query: { + author: 'Bob', + $populate: ['comments'] + } + }; + + this.service.find(params, function(error, data) { + expect(error).to.be.null; + expect(data.length).to.equal(1); + expect(data[0].title).to.equal('Second Post'); + expect(data[0].comments[0].content).to.equal('First comment'); + done(); + }); + }); + }); + + describe('#get', function() { + it('returns a BadRequest error when no id is provided', function(done) { + this.service.get(function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.BadRequest).to.be.ok; + done(); + }); + }); + + describe('when instance exists', function(){ + it('returns the found instance', function(done) { + this.service.get(_ids.FirstPost, function(error, data) { + expect(data._id.toString()).to.equal(_ids.FirstPost.toString()); + done(); + }); + }); + + it('does not return an error', function(done) { + this.service.get(_ids.FirstPost, function(error) { + expect(error).to.be.null; + done(); + }); + }); + }); + + describe('when instance does not exist', function(){ + it('returns NotFound error for non-existing id', function(done) { + this.service.get('55c0f8c7c0846306132077ab', function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.NotFound).to.be.ok; + expect(error.message).to.equal('No record found for id 55c0f8c7c0846306132077ab'); + done(); + }); + }); + }); + }); + + describe('#create', function() { + describe('when invalid', function(){ + it('returns a BadRequest error', function(done) { + this.service.create(Fixtures.invalidPost, function(error) { + expect(error instanceof errors.BadRequest).to.be.ok; + done(); + }); + }); + + it('returns mongoose validation error', function(done) { + this.service.create(Fixtures.invalidPost, function(error) { + expect(error.message).to.equal('Validation failed'); + expect(error.errors.author.path).to.equal('author'); + expect(error.errors.author.message).to.equal('Path `author` is required.'); + done(); + }); + }); + }); + + describe('when valid', function(){ + it('supports creating single instances and returns it', function(done) { + this.service.create(Fixtures.singlePost, function(error, data) { + expect(data.title).to.equal('First Post'); + + this.service.remove(data._id, function(){ + done(); + }); + }.bind(this)); + }); + + it.skip('supports creating multiple instances and returns them', function(done) { + this.service.create(Fixtures.multiplePosts, function(error, data) { + expect(data[0].title).to.equal('Second Post'); + expect(data[1].title).to.equal('Third Post'); + done(); + }); + }); + }); + }); + + describe('#remove', function() { + describe('when instance exists', function(){ + it('deletes instance and returns the deleted instance', function(done) { + this.service.remove(_ids.FirstPost, function(error, data) { + expect(data.title).to.equal('First Post'); + done(); + }); + }); + + it('does not return an error', function(done) { + this.service.remove(_ids.FirstPost, function(error) { + expect(error).to.be.null; + done(); + }); + }); + }); + + describe('when instance does not exist', function(){ + it('returns NotFound error for non-existing id', function(done) { + this.service.remove('55c0f8c7c0846306132077ab', function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.NotFound).to.be.ok; + expect(error.message).to.equal('No record found for id 55c0f8c7c0846306132077ab'); + done(); + }); + }); + }); + }); + + describe('#update', function() { + describe('when instance exists', function(){ + it('updates instance and returns the updated instance', function(done) { + this.service.update(_ids.FirstPost, { title: 'New Title'}, function(error, data) { + expect(data.title).to.equal('New Title'); + done(); + }); + }); + + it('does not return an error', function(done) { + this.service.update(_ids.FirstPost, {}, function(error) { + expect(error).to.be.null; + done(); + }); + }); + }); + + describe('when instance does not exist', function(){ + it('returns NotFound error for non-existing id', function(done) { + this.service.update('55c0f8c7c0846306132077ab', {}, function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.NotFound).to.be.ok; + expect(error.message).to.equal('No record found for id 55c0f8c7c0846306132077ab'); + done(); + }); + }); + }); + + describe('invalid params', function(){ + it('returns BadRequest error when not passing data', function(done) { + this.service.update(_ids.FirstPost, function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.BadRequest).to.be.ok; + expect(error.message).to.equal('You need to provide data to be updated'); + done(); + }); + }); + }); + }); + + describe('#patch', function() { + describe('when instance exists', function(){ + it('patches instance and returns the patched instance', function(done) { + this.service.patch(_ids.FirstPost, { title: 'New Title'}, function(error, data) { + expect(data.title).to.equal('New Title'); + done(); + }); + }); + + it('does not return an error', function(done) { + this.service.patch(_ids.FirstPost, {}, function(error) { + expect(error).to.be.null; + done(); + }); + }); + }); + + describe('when instance does not exist', function(){ + it('returns NotFound error for non-existing id', function(done) { + this.service.patch('55c0f8c7c0846306132077ab', {}, function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.NotFound).to.be.ok; + expect(error.message).to.equal('No record found for id 55c0f8c7c0846306132077ab'); + done(); + }); + }); + }); - describe('#new(mongoose)', function() { - it('should return a new Mongoose Service', function() { - var service = new mongooseService('test', { - field: {type: String} - }, mongoose); - assert.equal(true, !!service); - assert.equal(true, !!service.find); - assert.equal(true, !!service.get); - assert.equal(true, !!service.create); - assert.equal(true, !!service.update); - assert.equal(true, !!service.remove); + describe('invalid params', function(){ + it('returns BadRequest error when not passing data', function(done) { + this.service.patch(_ids.FirstPost, function(error) { + expect(error).to.be.ok; + expect(error instanceof errors.BadRequest).to.be.ok; + expect(error.message).to.equal('You need to provide data to be updated'); + done(); + }); }); + }); }); -}); \ No newline at end of file + }); +});