diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..3dbb032182 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,10 @@ +{ + "globals": { + "it": true, + "describe": true, + "before": true, + "after": true, + "exports": true + }, + "node": true +} \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index d5a79e5cbe..122d93ee10 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,67 +1,43 @@ -var exec = require('child_process').exec; +'use strict'; -module.exports = function(grunt) { +module.exports = function (grunt) { // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), - meta: { - banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + - '<%= grunt.template.today("yyyy-mm-dd") %>\n' + - '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + - '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + - ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */' - }, - - /* Testing - =======================================================*/ - simplemocha: { + release: {}, + jshint: { options: { - globals: ['should', 'expect'], - timeout: 3000, - ignoreLeaks: false, - // grep: '*-test', - ui: 'bdd', - reporter: 'spec' + jshintrc: '.jshintrc' }, - - all: { src: 'test/**/*.js' }, - mixins: { src: 'test/mixins/**/*.js' }, - providers: { src: 'test/providers/**/*.js' }, - services: { src: 'test/services/**/*.js' } + lib: ['lib/**/*.js', 'Gruntfile.js'], + test: 'test/**/*.js' }, - - /* Documentation - =======================================================*/ - dox: { + jsbeautifier: { options: { - title: "<%= pkg.title || pkg.name %>" + js: { + indent_size: 2, + jslintHappy: true, + keepArrayIndentation: true, + wrapLineLength: 0 + } }, - files: { - src: ['lib/'], - dest: 'docs' + files: ['lib/**/*.js', 'test/**/*.js', 'Gruntfile.js', 'package.json'] + }, + simplemocha: { + mixins: { + src: ['test/mixins/**/*.test.js'] + }, + providers: { + src: ['test/providers/**/*.test.js'] } } }); - grunt.loadNpmTasks('grunt-simple-mocha'); + grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-release'); - grunt.loadNpmTasks('grunt-dox'); - - // Alias' - // -------------------------------------------------- - grunt.registerTask('test', ['simplemocha:all']); - grunt.registerTask('test:mixins', ['simplemocha:mixins']); - grunt.registerTask('test:providers', ['simplemocha:providers']); - grunt.registerTask('test:services', ['simplemocha:services']); - - // Default Task. - grunt.registerTask("default", ['development']); - - // Development Tasks - // -------------------------------------------------- - grunt.registerTask('development', ['test']); + grunt.loadNpmTasks('grunt-jsbeautifier'); + grunt.loadNpmTasks('grunt-simple-mocha'); - // Release Tasks - // -------------------------------------------------- -}; \ No newline at end of file + grunt.registerTask('default', ['jsbeautifier', 'jshint', 'simplemocha']); +}; diff --git a/example/custom_service/index.js b/example/custom_service/index.js deleted file mode 100644 index eaefd0dafa..0000000000 --- a/example/custom_service/index.js +++ /dev/null @@ -1,41 +0,0 @@ -var feathers = require('../../lib/feathers'); -var Proto = require('uberproto'); -var users = [ - { - id: '1', - name : 'Wayne Campbell', - slogan: 'Party on Garth' - }, - { - id: '2', - name : 'Garth Algar', - slogan: 'Party on Wayne' - } -]; - -var service = { - find: function (params, cb) { - cb(null, users); - }, - - create: function(data, params, cb) { - console.log(data, params); - users.push(data); - cb(null, data); - }, - - get : function(id, params, cb) { - for (var user in users){ - if (users[user] && users[user].id === id) { - return cb(null, users[user]); - } - } - - cb(new Error('User With ID '+ id +' Not Found')); - } -}; - -feathers.createServer({ port: 8000 }) - .service('users', service) - .provide(feathers.rest()) - .start(); \ No newline at end of file diff --git a/example/rest_memory/index.html b/example/rest_memory/index.html deleted file mode 100644 index d1ea2545b3..0000000000 --- a/example/rest_memory/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Feathers REST Memory Example - - -

Feathers REST Memory Example

- - - - \ No newline at end of file diff --git a/example/rest_memory/index.js b/example/rest_memory/index.js deleted file mode 100644 index 5e88072d0f..0000000000 --- a/example/rest_memory/index.js +++ /dev/null @@ -1,10 +0,0 @@ -var feathers = require('../../lib/feathers'); -var Proto = require('uberproto'); -var memoryService = feathers.service.memory(); -var express = require('express'); - -feathers.createServer({ port: 3000 }) - .use(express.static(__dirname)) - .service('users', memoryService) - .provide(feathers.rest()) - .start(); diff --git a/example/rest_mongo/index.html b/example/rest_mongo/index.html deleted file mode 100644 index 373d763f4b..0000000000 --- a/example/rest_mongo/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Feathers REST MongoDB Example - - -

Feathers REST MongoDB Example

- - - - \ No newline at end of file diff --git a/example/rest_mongo/index.js b/example/rest_mongo/index.js deleted file mode 100644 index a4c3faa5f1..0000000000 --- a/example/rest_mongo/index.js +++ /dev/null @@ -1,75 +0,0 @@ -var feathers = require('../../lib/feathers'); -var Proto = require('uberproto'); -var express = require('express'); - -// Services - -var users = feathers.service.mongodb({ collection: 'users' }); -var posts = feathers.service.mongodb({ collection: 'posts' }); -var comments = feathers.service.mongodb({ collection: 'comments' }); - -// Associations -users.has({ - posts: ['posts'], - comments: ['comments'] -}); - -posts.has({ - author: 'users', - comments: ['comments'] -}); - -comments.has({ - author: 'users', - post: 'posts' -}); - -var associations = { - 'users': [ - { - hasMany: 'posts', - key: 'author' - }, - { - hasMany: 'comments', - key: 'author' - } - ], - 'posts': [ - { - hasMany: 'comments', - key: 'post' - }, - { - hasOne: 'users', - key: 'id' - } - ], - 'comments': { - hasOne: 'users', - key: 'id' - } -}; - -function has (association){ - if (hasMany){ - associations[association.key] = association.value; - } -} - -// { -// "title": "The great novel", -// "author": "1", -// "body": "Lorem ipsum", -// "comments": [] -// } - -// Create Server -feathers.createServer() - .use(express.static(__dirname)) - .service('users', users) - .service('posts', posts) - .service('comments', comments) - .provide(feathers.rest()) - .provide(feathers.socketio()) - .start(); \ No newline at end of file diff --git a/lib/application.js b/lib/application.js new file mode 100644 index 0000000000..7b1b785ed3 --- /dev/null +++ b/lib/application.js @@ -0,0 +1,61 @@ +'use strict'; + +var Proto = require('uberproto'); +var _ = require('underscore'); + +var mixins = require('./mixins'); + +module.exports = { + init: function () { + _.extend(this, { + methods: ['find', 'get', 'create', 'update', 'remove'], + mixins: mixins, + services: {}, + providers: [] + }); + }, + + use: function (location, service) { + var hasServiceMethod = function (name) { + return typeof service !== 'undefined' && typeof service[name] === 'function'; + }; + + // Check for service (any object with at least one service method) + if (_.some(this.methods, hasServiceMethod)) { + var protoService = Proto.extend(service); + var self = this; + + // Add all the mixins + _.each(this.mixins, function (fn) { + fn.call(self, protoService); + }); + + // Run the provider functions to register the service + _.each(this.providers, function (provider) { + provider(location, protoService); + }); + + this.services[location] = protoService; + return this; + } + + // Pass to the original express app + return this._super.apply(this, arguments); + }, + + lookup: function (location) { + return this.services[location]; + }, + + listen: function () { + var self = this; + // Setup each service (pass the app so that they can look up other services etc.) + _.each(self.services, function (service, path) { + if (typeof service.setup === 'function') { + service.setup(self, path); + } + }); + + return this._super.apply(this, arguments); + } +}; diff --git a/lib/errors.js b/lib/errors.js deleted file mode 100644 index 01226ebba8..0000000000 --- a/lib/errors.js +++ /dev/null @@ -1,30 +0,0 @@ -function createError(name, defaultMessage) { - function Err(message, data, statusCode) { - this.name = this.type = name; - this.message = message || defaultMessage || "Internal Server Error"; - this.data = data; - this.status = statusCode || 500; - } - - Err.prototype = Object.create(Error); - - return Err; -} - -[ 'MethodNotAllowed', 'NotFound', 'UnsupportedMediaType' ].forEach(function (name) { - exports[name] = createError(name); -}); - -var ValidationError = exports.ValidationError = function Err(message, data) { - if(typeof message !== 'string' && !data) { - data = message; - message = 'Validation failed!'; - } - - this.name = this.type = 'ValidationError'; - this.message = message; - this.data = data; - this.status = 400; -}; - -ValidationError.prototype = Object.create(Error); diff --git a/lib/feathers.js b/lib/feathers.js index 71edac8e1d..da03addfcd 100644 --- a/lib/feathers.js +++ b/lib/feathers.js @@ -1,24 +1,39 @@ -var server = require('./server'); +'use strict'; + var _ = require('underscore'); -var providers = require('./providers'); -var services = require('./services'); +var express = require('express'); var Proto = require('uberproto'); +var Application = require('./application'); +var providers = require('./providers'); + +/** + * Create a Feathers application that extends Express. + * + * @return {Function} + * @api public + */ + +function createApplication() { + var app = express(); + Proto.mixin(Application, app); + app.init(); + // Add REST provider by default, can always be disabled using app.disable('feathers rest') + app.use(express.bodyParser()).configure(providers.rest()); + return app; +} -exports.errors = require('./errors'); -exports.Server = server.Server; -exports.createServer = server.createServer; -var service = exports.service = {}; +/** + * Expose `createApplication()`. + */ +module.exports = createApplication; -_.each(providers, function(Provider, name) { - exports[name] = function(options) { - return Provider.create(options); - }; -}); +/** + * Framework version. + */ +module.exports.version = require('../package.json').version; -_.each(services, function(Service, name) { - service[name] = function(options) { - return Proto.create.call(Service, options); - }; +// Add the providers (REST and SocketIO) +_.defaults(module.exports, providers); - service[name].Service = Service; -}); +// Expose all express methods (like express.static()) +_.defaults(module.exports, express); diff --git a/lib/mixins/association.js b/lib/mixins/association.js deleted file mode 100644 index 6bc89b1ca4..0000000000 --- a/lib/mixins/association.js +++ /dev/null @@ -1,15 +0,0 @@ -var _ = require('underscore'); - -module.exports = { - has: function(name, service) { - if(!this._associations) { - this._associations = {}; - } - - - }, - - get: function(id, params, callback) { - - } -} \ No newline at end of file diff --git a/lib/mixins/event.js b/lib/mixins/event.js index 5a435a409e..1714159eef 100644 --- a/lib/mixins/event.js +++ b/lib/mixins/event.js @@ -1,11 +1,13 @@ +'use strict'; + var Proto = require('uberproto'); var _ = require('underscore'); var rubberduck = require('rubberduck'); var EventEmitter = require('events').EventEmitter; var eventMappings = { - create: 'created', - update: 'updated', - destroy: 'removed' + create: 'created', + update: 'updated', + remove: 'removed' }; /** @@ -14,49 +16,47 @@ var eventMappings = { * @type {{setup: Function}} */ var EventMixin = { - setup: function() { - var emitter = this._rubberDuck = rubberduck.emitter(this); - var self = this; - - self._serviceEvents = []; - // Pass the Rubberduck error event through - emitter.on('error', function(errors) { - self.emit('error', errors[0]); - }); - - _.each(eventMappings, function(event, method) { - if(self[method]) { - // The Rubberduck event name (e.g. afterCreate, afterUpdate or afterDestroy) - var eventName = 'after' + method.charAt(0).toUpperCase() + method.substring(1); - self._serviceEvents.push(event); - // Punch the given method - emitter.punch(method, -1); - // Pass the event and error event through - emitter.on(eventName, function(results) { - if(!results[0]) { // callback without error - self.emit(event, results[1]); - } else { - self.emit('error', results[0]); - } - }); - } - }); - return this._super ? this._super.apply(this, arguments) : this; - } + setup: function () { + var emitter = this._rubberDuck = rubberduck.emitter(this); + var self = this; + + self._serviceEvents = []; + // Pass the Rubberduck error event through + + // TODO deal with error events properly + emitter.on('error', function (errors) { + self.emit('serviceError', errors[0]); + }); + + _.each(eventMappings, function (event, method) { + if (typeof self[method] === 'function') { + // The Rubberduck event name (e.g. afterCreate, afterUpdate or afterDestroy) + var eventName = 'after' + method.charAt(0).toUpperCase() + method.substring(1); + self._serviceEvents.push(event); + // Punch the given method + emitter.punch(method, -1); + // Pass the event and error event through + emitter.on(eventName, function (results) { + if (!results[0]) { // callback without error + self.emit(event, results[1]); + } else { + self.emit('serviceError', results[0]); + } + }); + } + }); + + + return this._super ? this._super.apply(this, arguments) : this; + } }; -// Add EventEmitter prototype methods (if they don't already exist) -_.each(EventEmitter.prototype, function(fn, name) { - EventMixin[name] = function() { - if(this._super) { - return this._super.apply(this, arguments); - } - return EventEmitter.prototype[name].apply(this, arguments); - } -}); - -module.exports = function(service) { - service.mixin && service.mixin(EventMixin); +_.extend(EventMixin, EventEmitter.prototype); + +module.exports = function (service) { + if (typeof service.mixin === 'function') { + service.mixin(EventMixin); + } }; module.exports.Mixin = EventMixin; diff --git a/lib/mixins/index.js b/lib/mixins/index.js index 56c6a3bb19..72bab4bd44 100644 --- a/lib/mixins/index.js +++ b/lib/mixins/index.js @@ -1,4 +1,5 @@ +'use strict'; + module.exports = [ - require('./event'), - require('./validation') + require('./event') ]; diff --git a/lib/mixins/validation.js b/lib/mixins/validation.js deleted file mode 100644 index e367a4e67e..0000000000 --- a/lib/mixins/validation.js +++ /dev/null @@ -1,32 +0,0 @@ -var _ = require('underscore'); -var ValidationError = require('../errors').ValidationError; - -var ValidationMixin = { - create: function(data, params, cb) { - var self = this; - this.validate(data, _.extend({ validates: 'create' }, params), function(errors) { - if(errors) { - return cb(new ValidationError(errors)); - } - return self._super(data, params, cb); - }); - }, - - update: function(id, data, params, cb) { - var self = this; - this.validate(data, _.extend({ validates: 'update' }, params), function(errors) { - if(errors) { - return cb(new ValidationError(errors)); - } - return self._super(id, data, params, cb); - }); - } -}; - -module.exports = function(service) { - if(typeof service.validate === 'function' && service.mixin) { - service.mixin(ValidationMixin); - } -}; - -module.exports.Mixin = ValidationMixin; \ No newline at end of file diff --git a/lib/providers/index.js b/lib/providers/index.js index 51371bded2..278897d473 100644 --- a/lib/providers/index.js +++ b/lib/providers/index.js @@ -1,4 +1,6 @@ +'use strict'; + module.exports = { - rest: require('./rest'), - socketio: require('./socketio') -} + rest: require('./rest'), + socketio: require('./socketio') +}; diff --git a/lib/providers/rest.js b/lib/providers/rest.js index 0b5332633e..c564575ab4 100644 --- a/lib/providers/rest.js +++ b/lib/providers/rest.js @@ -1,118 +1,77 @@ +'use strict'; + var _ = require('underscore'); -var errors = require('../errors'); -var Proto = require('uberproto'); -var wrapper = function (req, res, next) { - return function (error, data) { - if (error) { - return next(error); - } - res.data = data; - return next(); - }; +var _wrapper = function (req, res, next) { + return function (error, data) { + if (error) { + return next(error); + } + res.data = data; + return next(); + }; }; -var errorMappings = { - 404: 'NotFound' +var _getParams = function (req) { + var query = req.query || {}; + return _.extend({ + query: query + }, req.feathers); }; -var toUri = function(name) { - // TODO - return '/' + name; +var toUri = function (name) { + // TODO + return '/' + name; }; -/** - * - `middleware` [Function] An express middleware to use for sending the - * response - * - `app` [Object] The express application to use - * - `port` [Integer] The port to listen on - * - `formatter` [Function] A function that takes the request and response - * and returns an object for content negotiation formatters. - * See the [ExpressJS documentation](http://expressjs.com/api.html#res.format) - * @type {*} - */ -var RestProvider = Proto.extend({ - init: function(config) { - this.config = config || {}; - this.services = {}; - }, - - register: function(path, service) { - this.services[path] = service; - }, - - _getParams: function(req) { - var query = req.query || {}; - return _.extend({ query: query }, req.feathers); - }, - - _service: function(app, service, path) { - var uri = toUri(path); - var self = this; - // TODO throw 405 Method Not Allowed with allowed methods - - // GET / -> resource.index(cb, params) - app.get(uri, function (req, res, next) { - service.find(self._getParams(req), wrapper(req, res, next)); - }); - - // GET /:id -> resource.get(cb, id, params) - app.get(uri + '/:id', function (req, res, next) { - service.get(req.params.id, self._getParams(req), wrapper(req, res, next)); - }); - - // POST -> resource.create(cb, data, params) - app.post(uri, function (req, res, next) { - service.create(req.body, self._getParams(req), wrapper(req, res, next)); - }); - - // PUT /:id -> resource.update(cb, id, data, params) - app.put(uri + '/:id', function (req, res, next) { - service.update(req.params.id, req.body, self._getParams(req), wrapper(req, res, next)); - }); - - // DELETE /:id -> resource.destroy(cb, id, params) - app.del(uri + '/:id', function (req, res, next) { - service.destroy(req.params.id, self._getParams(req), wrapper(req, res, next)); - }); - }, - - start: function(server) { - var config = this.config; - - var _responder = function(req, res) { - res.format(_.extend({ - 'application/json': function(){ - res.json(res.data); - } - }, config.formatters && config.formatters(req, res))); - }; - - var _errorHandler = function(error, req, res) { - res.status(500); - res.data = { error: error }; - responder(req, res); - }; - - if(typeof config.before === 'function') { - config.before.call(this, server.config.app, server.config.engine); - } - - // TODO (EK): We might not need this explicit use of express body parser. - // We might just be able to use config because it is either passed - // through the engine or our default engine (wich is express). - server.use(config.bodyParser || server.config.engine.bodyParser()); - _.each(this.services, _.bind(this._service, this, server.config.app)); - - server.use(config.handler || _responder); - server.use(config.errorHandler || _errorHandler); - - if(typeof config.after === 'function') { - config.after.call(this, server.config.app, server.config.engine); - } - - console.log('Feathers REST provider initialized on port %s', server.get('port')); - - return this; - } -}); - -module.exports = RestProvider; +module.exports = function (config) { + config = config || {}; + + return function () { + var app = this; + var responder = app.get('feathers rest handler') || function (req, res) { + res.format(_.extend({ + 'application/json': function () { + res.json(res.data); + } + }, config.formatters)); + }; + + app.enable('feathers rest'); + + // Register the REST provider + app.providers.push(function (path, service) { + if (app.disabled('feathers rest')) { + return; + } + + var uri = toUri(path); + // TODO throw 405 Method Not Allowed with allowed methods + + // GET / -> resource.index(cb, params) + app.get(uri, function (req, res, next) { + service.find(_getParams(req), _wrapper(req, res, next)); + }); + + // GET /:id -> resource.get(cb, id, params) + app.get(uri + '/:id', function (req, res, next) { + service.get(req.params.id, _getParams(req), _wrapper(req, res, next)); + }); + + // POST -> resource.create(cb, data, params) + app.post(uri, function (req, res, next) { + service.create(req.body, _getParams(req), _wrapper(req, res, next)); + }); + + // PUT /:id -> resource.update(cb, id, data, params) + app.put(uri + '/:id', function (req, res, next) { + service.update(req.params.id, req.body, _getParams(req), _wrapper(req, res, next)); + }); + + // DELETE /:id -> resource.destroy(cb, id, params) + app.del(uri + '/:id', function (req, res, next) { + service.remove(req.params.id, _getParams(req), _wrapper(req, res, next)); + }); + + app.use(uri, responder); + }); + }; +}; diff --git a/lib/providers/socketio.js b/lib/providers/socketio.js index f55d7496a2..5e6f671f71 100644 --- a/lib/providers/socketio.js +++ b/lib/providers/socketio.js @@ -1,62 +1,64 @@ -var Proto = require('uberproto'); +'use strict'; + var _ = require('underscore'); var socketio = require('socket.io'); +var listen = function (httpServer) { + var io = socketio.listen(httpServer); + + io.enable('browser client etag'); + io.set('log level', 0); + + io.set('transports', [ + 'xhr-polling', 'websocket', 'flashsocket', + 'htmlfile', 'jsonp-polling' + ]); + + return io; +}; + +module.exports = function (config) { + return function () { + var app = this; + var oldListen = app.listen; + var services = {}; + + app.enable('feathers socketio'); + // Overwrite Expresss `listen` + app.listen = function () { + var httpServer = oldListen.apply(this, arguments); + if (app.disabled('feathers socketio')) { + return httpServer; + } + + var io = this._io = listen(httpServer); + + _.each(services, function (service, path) { + // If the service emits events that we want to listen to (Event mixin) + if (typeof service.on === 'function' && service._serviceEvents) { + _.each(service._serviceEvents, function (ev) { + service.on(ev, function (data) { + io.sockets.emit(path + ' ' + ev, data); + }); + }); + } + }); + + io.sockets.on('connection', function (socket) { + _.each(services, function (service, path) { + _.each(app.methods, function (method) { + var name = path + '::' + method; + if (service[method]) { + socket.on(name, _.bind(service[method], service)); + } + }); + }); + }); + return httpServer; + }; -var SocketIoProvider = Proto.extend({ - init: function (config) { - this.config = config || {}; - this.services = {}; - }, - - register: function (path, service) { - this.services[path] = service; - }, - - _io: function(server) { - var io = socketio.listen(server.config.http); - - io.enable('browser client etag'); - io.set('log level', 0); - - io.set('transports', [ - 'xhr-polling', 'websocket', 'flashsocket', - 'htmlfile', 'jsonp-polling' - ]); - - return io; - }, - - start: function (server) { - var io = this._io(server); - var services = this.services; - - _.each(services, function(service, path) { - // If the service emits events that we want to listen to (Event mixin) - if(typeof service.on === 'function' && service._serviceEvents) { - _.each(service._serviceEvents, function(ev) { - service.on(ev, function(data) { - io.sockets.emit(path + ' ' + ev, data); - }); - }); - } - }); - - io.sockets.on('connection', function (socket) { - _.each(services, function (service, path) { - _.each(server.config.methods, function(method) { - var name = path + '::' + method; - if(service[method]) { - socket.on(name, _.bind(service[method], service)); - } - }); - }); - }); - - console.log('Feathers Web Socket provider initialized on port %s', server.get('port')); - - return this; - } -}); - -module.exports = SocketIoProvider; + app.providers.push(function (path, service) { + services[path] = service; + }); + }; +}; diff --git a/lib/server.js b/lib/server.js deleted file mode 100644 index 421c7dbbe9..0000000000 --- a/lib/server.js +++ /dev/null @@ -1,130 +0,0 @@ -var Proto = require('uberproto'); -var _ = require('underscore'); -var path = require('path'); -var express = require('express'); -var http = require('http'); -var mixins = require('./mixins'); - -var Server = Proto.extend({ - init: function (config) { - var app = express(); - config = config || {}; - - this.config = _.defaults(config, { - http: http.createServer(app), - app: app, - engine: express, - host: 'localhost', - port: 8080, - env: 'dev', - methods: [ 'find', 'get', 'create', 'update', 'destroy' ], - // Mixins to add when registering a service. - // An array of functions that get passed the service object and can do something with it. - mixins: mixins - }); - - this.services = {}; - this.providers = []; - - // Set Default Attributes - this.set('port', process.env.PORT || this.config.port); - - // Set Default Middleware - // TODO (EK): Abstract this in case we don't want to use express? - this.use(this.config.engine.compress()); - this.use(this.config.engine.favicon()); - this.use(this.config.engine.logger(process.env.NODE_ENV || this.config.env)); - }, - - - /* - * Express wrapper for .use() - */ - - // TODO (DL): Maybe we should just extend Express' - // app.js file and then override or remove what - // we don't need - use: function(route, fn){ - this.config.app.use(route, fn); - return this; - }, - - /* - * Express wrapper for .set() - */ - set: function(setting, value){ - this.config.app.set(setting, value); - return this; - }, - - /* - * Express wrapper for .get() - */ - get: function(setting){ - return this.config.app.get(setting); - }, - - service: function (location, service) { - var protoService = Proto.extend(service); - - // Add all the mixins - _.each(this.config.mixins, function(fn) { - fn(protoService); - }); - - this.services[location] = protoService; - - return this; - }, - - lookup: function(location) { - return this.services[location]; - }, - - provide: function (provider) { - this.providers.push(provider); - return this; - }, - - start: function () { - var self = this; - - _.each(self.services, function (service, path) { - if(typeof service.setup === 'function') { - service.setup(self, path); - } - }); - - this.providers.forEach(function (provider) { - _.each(self.services, function (service, path) { - provider.register(path, service); - }); - provider.start && provider.start(self); - }); - - this.httpServer = this.config.http.listen(this.config.app.get('port')); - - return this; - }, - - stop: function(callback) { - var self = this; - - this.providers.forEach(function (provider) { - provider.stop && provider.stop(self); - }); - - if(this.httpServer) { - this.httpServer.close(callback); - } else { - callback(); - } - - return this; - } -}); - -exports.Server = Server; -exports.createServer = function (options) { - return Server.create(options); -}; diff --git a/lib/services/index.js b/lib/services/index.js deleted file mode 100644 index 3b9ced248e..0000000000 --- a/lib/services/index.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - memory: require('./memory'), - mongodb: require('./mongodb') - // redis: require('./redis'), - // couch: require('./couch'), -}; \ No newline at end of file diff --git a/lib/services/memory.js b/lib/services/memory.js deleted file mode 100644 index 0cfe751d2f..0000000000 --- a/lib/services/memory.js +++ /dev/null @@ -1,116 +0,0 @@ -var Proto = require('uberproto'); -var util = require('util'); -var error = require('../errors'); -var _ = require('underscore'); - -// TODO (EK): Should we allow the ability to specify ascending -// or descending order for sort and/or order functions? Should -// you be able to sort by multiple attributes? -// -// ie. { sort: ['name', 'birthday'], order: 'ascending'} -var filters = { - sort: function (values, param) { - return _.sortBy(values, function (item) { - return item[param]; - }); - }, - order: function (values) { - return values.reverse(); - }, - skip: function (values, param) { - return values.slice(param); - }, - limit: function (values, param) { - return values.slice(0, param); - } -}; - -var MemoryService = Proto.extend({ - init: function (options) { - options = options || {}; - - this.type = 'memory'; - this._id = options.idField || 'id'; - this._uId = options.startId || 0; - this.store = options.store || {}; - }, - - find: function (params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - params.query = params.query || {}; - - var values = _.values(this.store); - - _.each(filters, function(handler, name) { - values = params.query[name] ? handler(values, params.query[name]) : values; - }); - - cb(null, values); - }, - - // TODO: This should support more than id - get: function (id, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - if (id in this.store) { - return cb(null, this.store[id]); - } - cb(new error.NotFound('Could not find record', { id: id })); - }, - - create: function (data, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - var id = data[this._id] || this._uId++; - data[this._id] = id; - - if (this.store[id]){ - return cb(new Error('A record with id: ' + id + ' already exists')); - } - - this.store[id] = data; - - cb(null, data); - }, - - update: function (id, data, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - var self = this; - if (id in this.store) { - _.each(_.omit(data, this._id), function(value, key){ - self.store[id][key] = value; - }); - - return cb(null, this.store[id]); - } - - cb('Could not find record with ' + id); - }, - - destroy: function (id, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - if (id in this.store) { - var deleted = this.store[id]; - delete this.store[id]; - - return cb(null, deleted); - } - - cb('Could not find record with ' + id); - } -}); - -module.exports = MemoryService; diff --git a/lib/services/mongodb.js b/lib/services/mongodb.js deleted file mode 100644 index a84393f6c6..0000000000 --- a/lib/services/mongodb.js +++ /dev/null @@ -1,156 +0,0 @@ -var Proto = require('uberproto'); -var error = require('../errors'); -var mongo = require('mongoskin'); -var _ = require('underscore'); - -// TODO (EK): Does order matter for how these filters -// are applied? I think it does or at least it should. - -var filters = { - sort: function (values, param) { - return _.sortBy(values, function (item) { - return item[param]; - }); - }, - order: function (values) { - return values.reverse(); - }, - skip: function (values, param) { - return values.slice(param); - }, - limit: function (values, param) { - return values.slice(0, param); - } -}; - -var MongoService = Proto.extend({ - - // TODO (EK): How do we handle indexes? - init: function (options) { - options = options || {}; - - this.type = 'mongodb'; - this._id = options.idField || '_id'; - this.connectionString = options.connectionString || null; - this.collection = options.collection || null; - - // NOTE (EK): We need to get the collection somehow. - // We have 3 options: - // 1. Pass in the path on each request - // 2. Initialize separate instances and pass it in there - // 3. Set the collection when we register each service - // - // We are currently using option number 3. This could be a bad assumption. - - if (this.connectionString){ - this.store = mongo.db(this.connectionString); - } - else { - this._connect(options); - } - }, - - // NOTE (EK): We create a new database connection for every MongoService. - // This may not be good but... in the mean time the rational for this - // design is because each user of a MongoService instance could be a separate - // app residing on a totally different server. - - // TODO (EK): We need to handle replica sets. - _connect: function(options){ - this.host = options.host || process.env.MONGODB_HOST || 'localhost'; - this.port = options.port || process.env.MONGODB_PORT || 27017; - this.db = options.db || process.env.MONGODB_DB || 'feathers'; - - ackOptions = { - w: options.w || 1, // write acknowledgment - journal: options.journal || false, // doesn't wait for journal before acknowledgment - fsync: options.fsync || false // doesn't wait for syncing to disk before acknowledgment - }; - - if (options.safe) { - ackOptions = { safe: options.safe }; - } - - var connectionString = this.host + ':' + this.port + '/' + this.db; - - if (options.username && options.password){ - connectionString =+ options.username + ':' + options.password + '@'; - } - - if (options.reconnect) connectionString += '?auto_reconnect=true'; - - this.store = mongo.db(connectionString, ackOptions); - }, - - find: function (params, cb) { - var id = null; - - if (_.isFunction(params)){ - cb = params; - } - else { - id = params.id; - } - - if (!this.collection) return cb(new Error('No collection specified')); - - // TODO (EK): sort out filters. - // ie. sort, limit, fields, skip, etc... - - if (id){ - this.store.collection(this.collection).findById(id, params.query, cb); - } - else { - this.store.collection(this.collection).find(params.query).toArray(cb); - } - }, - - get: function (id, params, cb) { - - if (_.isString(id)){ - id = id.toLowerCase(); - } - - if (_.isFunction(params)){ - cb = params; - params = {}; - } - - if (!this.collection) return cb(new Error('No collection specified')); - - this.store.collection(this.collection).findById(id, params, cb); - }, - - // TODO (EK): Batch support for create, update, delete. - create: function (data, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - if (!this.collection) return cb(new Error('No collection specified')); - - this.store.collection(this.collection).insert(data, params, cb); - }, - - update: function (id, data, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - if (!this.collection) return cb(new Error('No collection specified')); - - this.store.collection(this.collection).updateById(id, data, params, cb); - }, - - destroy: function (id, params, cb) { - if (_.isFunction(params)){ - cb = params; - } - - if (!this.collection) return cb(new Error('No collection specified')); - - this.store.collection(this.collection).removeById(id, params, cb); - } -}); - -module.exports = MongoService; diff --git a/package.json b/package.json index bd707b1882..c0759f88e3 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,33 @@ { "name": "feathers", "description": "An ultra scalable, feather weight, data oriented framework", - "version": "0.0.3", - "homepage": "https://github.com/yycjs/feathers", + "version": "0.0.4", + "homepage": "https://feathersjs.com", "repository": { "type": "git", - "url": "git://github.com/yycjs/feathers.git" + "url": "git://github.com/feathersjs/feathers.git" }, - "author": "YYCjs (http://yycjs.com)", + "keywords": [ + "feathers", + "REST", + "socket.io", + "realtime" + ], + "author": "Feathers (http://feathersjs.com)", "contributors": [ "Eric Kryski (http://erickryski.com)", "David Luecke (http://neyeon.com)" ], + "license": "MIT", + "bugs": { + "url": "https://github.com/feathersjs/feathers/issues" + }, "main": "lib/feathers", "directories": { "lib": "lib" }, "scripts": { - "start": "node example/rest/app.js", - "test": "grunt test" + "test": "grunt" }, "engines": { "node": "*", @@ -26,23 +35,20 @@ }, "dependencies": { "uberproto": ">= 1.0.0", - "express": "~3.1.1", + "express": "~ 3.1.1", "rubberduck": "~0.2.0", "underscore": "~1.4.4", - "socket.io": "~0.9.14", - "mongoskin": "~0.5.0" + "socket.io": "~0.9.14" }, "devDependencies": { "request": "~2.21.0", "socket.io-client": "~0.9.11", - "grunt-simple-mocha": "~0.4.0", "grunt-cli": "~0.1.7", "grunt": "~0.4.1", - "grunt-release": "~0.3.5", - "grunt-dox": "~0.4.2", - "sinon-chai": "~2.4.0", - "chai": "~1.7.0", + "grunt-release": "~0.5.1", "mocha": "~1.11.0", - "sinon": "~1.7.2" + "grunt-contrib-jshint": "~0.6.3", + "grunt-simple-mocha": "~0.4.0", + "grunt-jsbeautifier": "~0.2.2" } } diff --git a/readme.md b/readme.md index c1c58e34fc..787c68296d 100644 --- a/readme.md +++ b/readme.md @@ -1,31 +1,20 @@ # Feathers -> An ultra scalable, feather weight, data oriented framework built for tomorrow's web. +> Let your web app fly. -[![Build Status](https://travis-ci.org/yycjs/feathers.png)](https://travis-ci.org/yycjs/feathers) +[![Build Status](https://travis-ci.org/feathersjs/feathers.png)](https://travis-ci.org/feathersjs/feathers) -The core focus of Feathers is **your data**. We believe that ultimately your app's purpose is to manage data in some fashion and so that's all you should really need to deal with. Managing your data. Feathers provides a deadly simple way of managing your data and allows you to provide this data via REST and SocketIO APIs with NodeJS. +Feathers is a light weight web application framework that rides on top of [Express](http://expressjs.com). It makes it easy to create RESTful web services and real-time applications using [socket.io](http://socket.io). -## Why Another NodeJS Framework? - -We know... Oh God another bloody NodeJS framework! Yes we are also very tired of seeing all these NodeJS frameworks. All the rails clones are getting a bit boring and really aren't taking advantage of the real strengths of NodeJS. We wanted to take a different approach than every other framework we have seen, because we believe that data is core to the web and should be the core focus of web applications. - -We also think that your data resources can and should be encapsulated in such a way that they can be ultra scalable and self contained. The MVC pattern works well but it is becoming antiquated in today's web. Frankly you don't need it and they tend to become bloated. - -With that being said there are some amazing frameworks already out there and we wanted to leverage the ideas that have been put into them, which is why Feathers is built on top of [Express](http://expressjs.com) and is inspired in part by [Sails](http://sailsjs.org), [Flatiron](http://flatironjs.org) and [Derby](http://derbyjs.com). - - -## Key Concepts +The core focus of Feathers is **your data**. We believe that ultimately your app's purpose is to manage data in some fashion and so that's all you should really need to deal with. Managing your data. -At the core to Feathers are 3 simple but important concepts, **Providers**, **Services** and **Mixins**. +## Install -A **Provider** is simply a module that *provides* your data to clients (ie. via REST or Web Sockets). +As with any NodeJS module, just install it as a dependency in your application: -A **Service** is a module that defines the API functionality for a given resource and is exposed to clients via a provider. (ie. A definition of create, update, etc.) +> npm install feathers --save -A **Mixin** is like a utility or middleware that you can use to improve your service (ie. validation or authentication) - -## Getting Started is Easy +## Getting Started Is Easy Like we said, services are just simple modules that expose certain methods to the providers. This makes it easy to initialize a service that say... provides a single TODO: @@ -41,18 +30,14 @@ var todoService = { } }; -feathers.createServer({ port: 8000 }) - .service('todo', todoService) - .provide(feathers.rest()) - .provide(feathers.socketio()) - .start(); +feathers() + .configure(feathers.socketio()) + .use('todo', todoService) + .listen(8000); ``` That's all there really is to building an app with Feathers. - -## Built In Providers - ### REST You can access the REST service by going to `http://localhost:8000/todo/dishes` in your browser @@ -65,9 +50,11 @@ and will see: } ``` +> Note: Query parameters like `http://localhost:8000/todo/dishes?type=dirty` will be passed as `params.query` + ### SocketIO -Since, in the above example, you added it as a provider, you can also connect to your service via SocketIO. +Since we configured our app with `feathers.socketio()`, you can also connect to your service via SocketIO. Create an HTML page and insert the following code to see the response data logged on the console: ```html @@ -92,72 +79,204 @@ var myService = { create: function(data, params, callback) {}, update: function(id, data, params, callback) {}, destroy: function(id, params, callback) {}, - setup: function(server) {} + setup: function(app) {} } ``` -All callbacks follow the `function(error, data)` NodeJS convention. `params` contains additional -parameters like the query parameters of a REST API call. For example `http://localhost:8000/todo/dishes?done=true` -from the getting started example would result in `{ done: 'true' }` as the `params` object. +All callbacks follow the `function(error, data)` NodeJS convention. `params` can contain any additional parameters, for +example the currently authenticated user. REST service calls set `params.query` with the query parameters (e.g. a query string +like `?status=active&type=user` becomes `{ status: "active", type: "user" }`). + +### `find(params, callback)` + +Retrieves a list of all resources from the service. Ideally use `params.query` for things like filtering and paging so +that REST calls like `todo?status=completed&user=10` work right out of the box. + +__REST__ + +> GET todo?status=completed&user=10 + +__SocketIO__ + +```js +socket.emit('todo::find', { + status: 'completed' + user: 10 +}, function(error, data) { +}); +``` + +### `get(id, params, callback)` + +Retrieves a single resource with the given `id` from the service. -### find(params, callback) +__REST__ -Retrieves a list of all resources of the service. `params` contains additional parameters such -as URL query parameters (like `http://localhost:8000/todo?sort=status`). +> GET todo/1 -### get(id, params, callback) +__SocketIO__ -Retrieves a single resource of the service. `params` contains additional parameters such -as URL query parameters (like `http://localhost:8000/todo?sort=status`). +```js +socket.emit('todo::get', 1, {}, function(error, data) { + +}); +``` ### create(data, params, callback) +Creates a new resource with `data`. The callback should be called with that resource (and the id initialized). + +__REST__ + +> POST todo +> { "description": "I really have to iron" } + +By default the body can be eihter JSON or form encoded as long as the content type is set accordingly. + +__SocketIO__ + +```js +socket.emit('todo::create', { + description: 'I really have to iron' +}, function(error, data) { +}); +``` + ### update(id, data, params, callback) +Updates the resource identified by `id` using `data`. + +__REST__ + +> PUT todo/2 +> { "description": "I really have to do laundry" } + +__SocketIO__ + +```js +socket.emit('todo::update', 2, { + description: 'I really have to do laundry' +}, {}, function(error, data) { + // data -> { id: 2, description: "I really have to do laundry" } +}); +``` + ### remove(id, params, callback) -### setup(registry) +Remove the resource with `id`. -## Built In Services +__REST__ -To make it easier to get started, Feathers comes with several standard service implementations to extend -from. All built in services follow the same parameter conventions for things like sorting and filtering. +> DELETE todo/2 -### Memory +__SocketIO__ -### MongoDB (TODO) +```js +socket.emit('todo::delete', 2, {}, function(error, data) { +}); +``` -### Redis (TODO) +### setup(app) -## Service mixins +Initializes the service passing an instance of the Feathers application. +`app` can do everything a normal Express application does and additionally provides `app.lookup(path)` +to retrieve another service by its path. `setup` is a great way to connect services: -### Event +```js +var todoService = { + get: function(name, params, callback) { + callback(null, { + id: name, + description: 'You have to ' + name + '!' + }); + } +}; -### Associations +var myService = { + setup: function(app) { + this.todo = app.lookup('todo'); + }, -* register associated services via `has({ 'attr_name': ['service path']})` - - - ```js - post.has({ - 'author': '/users', - 'comments': ['/comments'] + get: function(name, params, callback) { + this.todo.get('take out trash', {}, function(error, todo) { + callback(null, { + name: name, + todo: todo }); + }); + } +} - comment.has({ - 'author': '/users', - 'post': '/posts' - }); - ``` +feathers() + .use('todo', todoService) + .use('my', myService) + .listen(8000); +``` + +You can see the combination when going to `http://localhost:8000/my/test`. - - This then gives *user* event listeners for 'post created', 'post updated', 'post removed' - - This then gives *user* event listeners for 'comment created', 'comment updated', 'comment removed' - - This then gives *post* event listeners for 'comment created', 'comment updated', 'comment removed' - - This then gives *post* event listeners for 'user created', 'user updated', 'user removed' +## Getting Real, Time -* We could use /posts?expand=true to get the posts with all of its comment objects, otherwise we get the ids. +The secret ingredient to create real time applications using Feathers and SocketIO is the +`created`, `updated` and `removed` events every Feathers service automatically emits. +Here is another simple Todo service, that just passes the data through `create`: +```js +var feathers = require('feathers'); + +var todoService = { + create: function(data, params, callback) { + callback(null, data); + } +}; +var app = feathers() + .configure(feathers.socketio()) + .use('todo', todoService) + .listen(8000); +``` + +Lets make an HTML file that creates a new Todo using SocketIO every two seconds: -### Validation +```html + + +``` + +In another file we just listen to the `todo created` event and log it: + +```html + + +``` +When visiting both HTMl files in a browser at the same time you should see a new Todo being logged every +two seconds on both pages. + + +## Why Another NodeJS Framework? + +We know... Oh God another bloody NodeJS framework! Yes we are also very tired of seeing all these NodeJS frameworks. All the rails clones are getting a bit boring and really aren't taking advantage of the real strengths of NodeJS. We wanted to take a different approach than every other framework we have seen, because we believe that data is core to the web and should be the core focus of web applications. + +We also think that your data resources can and should be encapsulated in such a way that they can be ultra scalable and self contained. The MVC pattern works well but it is becoming antiquated in today's web. Frankly you don't need it and they tend to become bloated. + +With that being said there are some amazing frameworks already out there and we wanted to leverage the ideas that have been put into them, which is why Feathers is built on top of [Express](http://expressjs.com) and is inspired in part by [Sails](http://sailsjs.org), [Flatiron](http://flatironjs.org) and [Derby](http://derbyjs.com). diff --git a/test/mixins/event.test.js b/test/mixins/event.test.js index 446df0adb3..ce74274aea 100644 --- a/test/mixins/event.test.js +++ b/test/mixins/event.test.js @@ -1,3 +1,5 @@ +'use strict'; + var assert = require('assert'); var _ = require('underscore'); var Proto = require('uberproto'); @@ -5,142 +7,145 @@ var mixinEvent = require('../../lib/mixins/event'); var EventMixin = mixinEvent.Mixin; describe('Event mixin', function () { - it('initializes', function () { - var FixtureService = Proto.extend({ - setup: function(arg) { - return 'Original setup: ' + arg; - } - }); - - mixinEvent(FixtureService); - - assert.equal(typeof FixtureService.setup, 'function'); - assert.equal(typeof FixtureService.on, 'function'); - assert.equal(typeof FixtureService.emit, 'function'); - - var instance = FixtureService.create(); - assert.equal('Original setup: Test', instance.setup('Test')); - assert.ok(instance._rubberDuck instanceof require('events').EventEmitter); - - var existingMethodsService = { - setup: function(arg) { - return 'Original setup from object: ' + arg; - }, - - emit: function() { - return 'Original emit'; - } - } - - Proto.mixin(EventMixin, existingMethodsService); - - assert.equal('Original setup from object: Test', existingMethodsService.setup('Test')); - assert.equal(existingMethodsService.emit(), 'Original emit'); - assert.equal(typeof existingMethodsService.on, 'function'); - }); - - it('error', function (done) { - var FixtureService = Proto.extend({ - create: function (data, params, cb) { - _.defer(function() { - cb(new Error('Something went wrong')); - }); - } - }); - - mixinEvent(FixtureService); - - var instance = Proto.create.call(FixtureService); - instance.setup(); - - instance.on('error', function(error) { - assert.ok(error instanceof Error); - assert.equal(error.message, 'Something went wrong'); - done(); - }); - - instance.create({ name: 'Tester' }, {}, function(error) { - assert.ok(error instanceof Error); - }); - }); - - it('created', function (done) { - var FixtureService = Proto.extend({ - create: function (data, params, cb) { - _.defer(function() { - cb(null, { - id: 10, - name: data.name - }); - }); - } - }); - - mixinEvent(FixtureService); - - var instance = Proto.create.call(FixtureService); - instance.setup(); - - instance.on('created', function(data) { - assert.equal(data.id, 10); - assert.equal(data.name, 'Tester'); - done(); - }); - - instance.create({ name: 'Tester' }, {}, function(error, data) { - assert.equal(data.id, 10); - }); - }); - - it('updated', function (done) { - var FixtureService = Proto.extend({ - update: function (id, data, params, cb) { - _.defer(function() { - cb(null, { - id: id, - name: data.name - }); - }, 20); - } - }); - - mixinEvent(FixtureService); - - var instance = Proto.create.call(FixtureService); - instance.setup(); - - instance.on('updated', function(data) { - assert.equal(data.id, 12); - assert.equal(data.name, 'Updated tester'); - done(); - }); - - instance.update(12, { name: 'Updated tester' }, {}, function(error, data) { - assert.equal(data.id, 12); - }); - }); - - it('removed', function (done) { - var FixtureService = Proto.extend({ - destroy: function (id, params, cb) { - _.defer(function() { - cb(null, { id: id }); - }, 20); - } - }); - - mixinEvent(FixtureService); - - var instance = Proto.create.call(FixtureService); - instance.setup(); - - instance.on('removed', function(data) { - assert.equal(data.id, 27); - done(); - }); - - instance.destroy(27, {}, function(error, data) { - assert.equal(data.id, 27); - }); - }); + it('initializes', function () { + var FixtureService = Proto.extend({ + setup: function (arg) { + return 'Original setup: ' + arg; + } + }); + + mixinEvent(FixtureService); + + assert.equal(typeof FixtureService.setup, 'function'); + assert.equal(typeof FixtureService.on, 'function'); + assert.equal(typeof FixtureService.emit, 'function'); + + var instance = FixtureService.create(); + assert.equal('Original setup: Test', instance.setup('Test')); + assert.ok(instance._rubberDuck instanceof require('events').EventEmitter); + + var existingMethodsService = { + setup: function (arg) { + return 'Original setup from object: ' + arg; + } + }; + + Proto.mixin(EventMixin, existingMethodsService); + + assert.equal('Original setup from object: Test', existingMethodsService.setup('Test')); + assert.equal(typeof existingMethodsService.on, 'function'); + }); + + it('serviceError', function (done) { + var FixtureService = Proto.extend({ + create: function (data, params, cb) { + _.defer(function () { + cb(new Error('Something went wrong')); + }); + } + }); + + mixinEvent(FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.setup(); + + instance.on('serviceError', function (error) { + assert.ok(error instanceof Error); + assert.equal(error.message, 'Something went wrong'); + done(); + }); + + instance.create({ + name: 'Tester' + }, {}, function (error) { + assert.ok(error instanceof Error); + }); + }); + + it('created', function (done) { + var FixtureService = Proto.extend({ + create: function (data, params, cb) { + _.defer(function () { + cb(null, { + id: 10, + name: data.name + }); + }); + } + }); + + mixinEvent(FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.setup(); + + instance.on('created', function (data) { + assert.equal(data.id, 10); + assert.equal(data.name, 'Tester'); + done(); + }); + + instance.create({ + name: 'Tester' + }, {}, function (error, data) { + assert.equal(data.id, 10); + }); + }); + + it('updated', function (done) { + var FixtureService = Proto.extend({ + update: function (id, data, params, cb) { + _.defer(function () { + cb(null, { + id: id, + name: data.name + }); + }, 20); + } + }); + + mixinEvent(FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.setup(); + + instance.on('updated', function (data) { + assert.equal(data.id, 12); + assert.equal(data.name, 'Updated tester'); + done(); + }); + + instance.update(12, { + name: 'Updated tester' + }, {}, function (error, data) { + assert.equal(data.id, 12); + }); + }); + + it('removed', function (done) { + var FixtureService = Proto.extend({ + remove: function (id, params, cb) { + _.defer(function () { + cb(null, { + id: id + }); + }, 20); + } + }); + + mixinEvent(FixtureService); + + var instance = Proto.create.call(FixtureService); + instance.setup(); + + instance.on('removed', function (data) { + assert.equal(data.id, 27); + done(); + }); + + instance.remove(27, {}, function (error, data) { + assert.equal(data.id, 27); + }); + }); }); diff --git a/test/mixins/validations.test.js b/test/mixins/validations.test.js deleted file mode 100644 index 40ee26b9a2..0000000000 --- a/test/mixins/validations.test.js +++ /dev/null @@ -1,116 +0,0 @@ -var assert = require('assert'); -var _ = require('underscore'); -var Proto = require('uberproto'); -var errors = require('../../lib/errors'); -var mixinValidation = require('../../lib/mixins/validation'); - -describe('Validation mixin', function () { - it('initializes', function () { - var ValidationService = Proto.extend({ - validate: function(data, params, cb) { - if(!data.name || data.name === '') { - return cb({ - name: ['Name can not be empty'] - }); - } - return cb(); - } - }); - - mixinValidation(ValidationService); - - assert.equal(typeof ValidationService.create, 'function'); - assert.equal(typeof ValidationService.update, 'function'); - - var instance = Proto.create.call(ValidationService); - instance.validate({ - name: 'Test' - }, {}, function(errors) { - assert.ok(!errors); - }); - - instance.validate({ - fullName: 'Testing' - }, {}, function(errors) { - assert.deepEqual(errors, { - name: [ 'Name can not be empty' ] - }); - }); - }); - - it('.create', function () { - var ValidationService = Proto.extend({ - validate: function(data, params, cb) { - assert.equal(params.validates, 'create'); - if(!data.name || data.name === '') { - return cb({ - name: ['Name can not be empty'] - }); - } - return cb(); - }, - - create: function(data, params, cb) { - cb(null, { - id: 23, - name: data.name - }); - } - }); - - mixinValidation(ValidationService); - - var instance = Proto.create.call(ValidationService); - instance.create({ name: 'Tester' }, {}, function(error, data) { - assert.ok(!error); - assert.equal(data.id, 23); - }); - - instance.create({ fullName: 'Tester' }, {}, function(error) { - assert.ok(error); - assert.ok(error instanceof errors.ValidationError); - assert.equal(error.type, 'ValidationError'); - assert.deepEqual(error.data, { - name: ['Name can not be empty'] - }); - }); - }); - - it('.update', function () { - var ValidationService = Proto.extend({ - validate: function(data, params, cb) { - assert.equal(params.validates, 'update'); - if(!data.name || data.name === '') { - return cb({ - name: ['Name can not be empty'] - }); - } - return cb(); - }, - - update: function(id, data, params, cb) { - cb(null, { - id: id, - name: data.name - }); - } - }); - - mixinValidation(ValidationService); - - var instance = Proto.create.call(ValidationService); - instance.update(14, { name: 'Tester' }, {}, function(error, data) { - assert.ok(!error); - assert.equal(data.id, 14); - }); - - instance.update('14', { fullName: 'Tester' }, {}, function(error) { - assert.ok(error); - assert.ok(error instanceof errors.ValidationError); - assert.equal(error.type, 'ValidationError'); - assert.deepEqual(error.data, { - name: ['Name can not be empty'] - }); - }); - }); -}); diff --git a/test/providers/rest.test.js b/test/providers/rest.test.js index 9ab0c754a6..c1c0d8f31d 100644 --- a/test/providers/rest.test.js +++ b/test/providers/rest.test.js @@ -1,49 +1,93 @@ -var assert = require('assert'); +'use strict'; + var request = require('request'); +var assert = require('assert'); var feathers = require('../../lib/feathers'); +var fixture = require('./service-fixture'); +var todoService = fixture.Service; +var verify = fixture.verify; + describe('REST provider', function () { - it('GET', function (done) { - var todoService = { - get: function(name, params, callback) { - callback(null, { - id: name, - description: "You have to do " + name + "!" - }); - } - }; - - var server = feathers.createServer({ port: 8000 }) - .service('todo', todoService) - .provide(feathers.rest()) - .provide(feathers.socketio()) - .start(); - - request('http://localhost:8000/todo/dishes', function (error, response, body) { - server.stop(); - done(); - }) - }); - - it('PUT', function (done) { - var todoService = { - get: function(name, params, callback) { - callback(null, { - id: name, - description: "You have to do " + name + "!" - }); - } - }; - - var server = feathers.createServer({ port: 8000 }) - .service('todo', todoService) - .provide(feathers.rest()) - .start(); - - request('http://localhost:8000/todo/dishes', function (error, response, body) { - // console.log(arguments); - server.stop(); - done(); - }) - }); + describe('CRUD', function () { + var server; + + before(function () { + server = feathers() + .use('todo', todoService) + .listen(3000); + }); + + after(function (done) { + server.close(done); + }); + + it('GET .find', function (done) { + request('http://localhost:3000/todo', function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.find(JSON.parse(body)); + done(error); + }); + }); + + it('GET .get', function (done) { + request('http://localhost:3000/todo/dishes', function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.get('dishes', JSON.parse(body)); + done(error); + }); + }); + + it('POST .create', function (done) { + var original = { + description: 'POST .create' + }; + + request({ + url: 'http://localhost:3000/todo', + method: 'post', + body: JSON.stringify(original), + headers: { + 'Content-Type': 'application/json' + } + }, function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.create(original, JSON.parse(body)); + + done(error); + }); + }); + + it('PUT .update', function (done) { + var original = { + description: 'PUT .update' + }; + + request({ + url: 'http://localhost:3000/todo/544', + method: 'put', + body: JSON.stringify(original), + headers: { + 'Content-Type': 'application/json' + } + }, function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.update(544, original, JSON.parse(body)); + + done(error); + }); + }); + + it('DELETE .remove', function (done) { + request({ + url: 'http://localhost:3000/todo/233', + method: 'delete' + }, function (error, response, body) { + assert.ok(response.statusCode === 200, 'Got OK status code'); + verify.remove(233, JSON.parse(body)); + + done(error); + }); + }); + }); }); diff --git a/test/providers/service-fixture.js b/test/providers/service-fixture.js new file mode 100644 index 0000000000..49a7628de7 --- /dev/null +++ b/test/providers/service-fixture.js @@ -0,0 +1,78 @@ +'use strict'; + +var _ = require('underscore'); +var assert = require('assert'); + +var findAllData = [{ + id: 0, + description: 'You have to do something' +}, { + id: 1, + description: 'You have to do laundry' +}]; + +exports.Service = { + find: function (params, callback) { + callback(null, findAllData); + }, + + get: function (name, params, callback) { + callback(null, { + id: name, + description: "You have to do " + name + "!" + }); + }, + + create: function (data, params, callback) { + var result = _.clone(data); + result.id = 42; + result.status = 'created'; + callback(null, result); + }, + + update: function (id, data, params, callback) { + var result = _.clone(data); + result.id = id; + result.status = 'updated'; + callback(null, result); + }, + + remove: function (id, params, callback) { + callback(null, { + id: id + }); + } +}; + +exports.verify = { + find: function (data) { + assert.deepEqual(findAllData, data, 'Data as expected'); + }, + + get: function (id, data) { + assert.equal(data.id, id, 'Got id in data'); + assert.equal(data.description, 'You have to do ' + id + '!', 'Got description'); + }, + + create: function (original, current) { + var expected = _.extend({}, original, { + id: 42, + status: 'created' + }); + assert.deepEqual(expected, current, 'Data ran through .create as expected'); + }, + + update: function (id, original, current) { + var expected = _.extend({}, original, { + id: id, + status: 'updated' + }); + assert.deepEqual(expected, current, 'Data ran through .update as expected'); + }, + + remove: function (id, data) { + assert.deepEqual({ + id: id + }, data, '.remove called'); + } +}; diff --git a/test/providers/socketio.test.js b/test/providers/socketio.test.js index 699e2d508f..912df36904 100644 --- a/test/providers/socketio.test.js +++ b/test/providers/socketio.test.js @@ -1,64 +1,121 @@ -var expect = require('chai').expect; -var request = require('request'); +'use strict'; + +var assert = require('assert'); var feathers = require('../../lib/feathers'); var io = require('socket.io-client'); -var _ = require('underscore'); + +var fixture = require('./service-fixture'); +var todoService = fixture.Service; +var verify = fixture.verify; describe('SocketIO provider', function () { -// it('get', function (done) { -// var todoService = { -// get: function(name, params, callback) { -// callback(null, { -// id: name, -// description: 'You have to do ' + name + '!' -// }); -// } -// }; -// -// var server = feathers.createServer({ port: 8000 }) -// .service('todo', todoService) -// .provide(feathers.socketio()) -// .start(); -// -// var socket = io.connect('http://localhost:8000'); -// -// socket.emit('todo::get', 'dishes', {}, function(error, data) { -// expect(error).to.be.null; -// expect(data.id).to.equal('dishes'); -// expect(data.description).to.equal('You have to do dishes!'); -// server.stop(); -// done(); -// }); -// }); - - it('create and created event', function (done) { - var todoService = { - create: function(data, params, callback) { - _.defer(function() { - callback(null, data); - }, 200); - } - }; - - var server = feathers.createServer({ port: 8000 }) - .service('todo', todoService) - .provide(feathers.socketio()) - .start(); - - var socket = io.connect('http://localhost:8000'); - - socket.on('todo created', function(data) { - expect(data.id).to.equal(1); - expect(data.name).to.equal('Create dishes'); - server.stop(); - done(); - }); - - socket.emit('todo::create', { - id: 1, - name: 'Create dishes' - }, {}, function(error, data) { - expect(error).to.be.null; - }); - }); + var server, socket; + + before(function () { + // This seems to be the only way to not get the + // socket.io started log messing up the test output + var oldlog = console.log; + console.log = function () {}; + + server = feathers() + .configure(feathers.socketio()) + .use('todo', todoService) + .listen(3000); + + console.log = oldlog; + + socket = io.connect('http://localhost:3000'); + }); + + after(function (done) { + socket.disconnect(); + server.close(done); + }); + + describe('CRUD', function () { + it('::find', function (done) { + socket.emit('todo::find', {}, function (error, data) { + verify.find(data); + + done(error); + }); + }); + + it('::get', function (done) { + socket.emit('todo::get', 'laundry', {}, function (error, data) { + verify.get('laundry', data); + + done(error); + }); + }); + + it('::create', function (done) { + var original = { + name: 'creating' + }; + + socket.emit('todo::create', original, {}, function (error, data) { + verify.create(original, data); + + done(error); + }); + }); + + it('::update', function (done) { + var original = { + name: 'updating' + }; + + socket.emit('todo::update', 23, original, {}, function (error, data) { + verify.update(23, original, data); + + done(error); + }); + }); + + it('::remove', function (done) { + socket.emit('todo::remove', 11, {}, function (error, data) { + verify.remove(11, data); + + done(error); + }); + }); + }); + + describe('Events', function () { + it('created', function (done) { + var original = { + name: 'created event' + }; + + socket.on('todo created', function (data) { + verify.create(original, data); + done(); + }); + + socket.emit('todo::create', original, {}, function () {}); + }); + + it('updated', function (done) { + var original = { + name: 'updated event' + }; + + socket.on('todo updated', function (data) { + verify.update(10, original, data); + done(); + }); + + socket.emit('todo::update', 10, original, {}, function () {}); + }); + + it('removed', function (done) { + socket.on('todo removed', function (data) { + verify.remove(333, data); + done(); + }); + + socket.emit('todo::remove', 333, {}, function () {}); + }); + }); }); diff --git a/test/services/memory.test.js b/test/services/memory.test.js deleted file mode 100644 index 7033947a1d..0000000000 --- a/test/services/memory.test.js +++ /dev/null @@ -1,257 +0,0 @@ -var chai = require('chai'); -var expect = chai.expect; -var MemoryService = require('../../lib/services/memory'); -var Proto = require('uberproto'); -var service; - -describe('Memory Service', function () { - beforeEach(function(done){ - service = Proto.create.call(MemoryService); - service.create({ - id: 1, - name: 'Test 1' - }, function(error, data) { - done(); - }); - }); - - afterEach(function(done){ - service.destroy(1, function(err){ - done(); - }); - }); - - describe('get', function () { - - it('should return an instance that exists', function (done){ - service.get(1, function(error, data) { - expect(data.id).to.equal(1); - expect(data.name).to.equal('Test 1'); - done(); - }); - }); - - it('should return an error when requested instance doesn\'t exist', function (done){ - service.get(2, function(error, data) { - expect(error).to.not.be.null; - expect(data).to.be.undefined; - done(); - }); - }); - - }); - - describe('create', function () { - - it('should create a new instance', function (done) { - service.create({ - id: 2, - name: 'Test 2' - }, function(error, data) { - expect(data.id).to.equal(2); - expect(data.name).to.equal('Test 2'); - done(); - }); - }); - - it('should return an error when it can\'t create', function (done) { - service.create({ - id: 1, - name: 'Test 2' - }, function(error, data) { - expect(error).to.not.be.null; - expect(data).to.be.undefined; - done(); - }); - }); - }); - - describe('update', function () { - - it('should update an existing instance', function (done) { - service.update(1, { - name: 'Test 1 Updated' - }, {}, function(error, data) { - expect(data.id).to.equal(1); - expect(data.name).to.equal('Test 1 Updated'); - done(); - }); - }); - - it('should return an error on db when instance to update doesn\'t exist', function (done) { - service.update(3, { - name: 'Test 2' - }, {}, function(error, data) { - expect(error).to.not.be.null; - expect(data).to.be.undefined; - done(); - }); - }); - - }); - - describe('destroy', function () { - - it('should delete an existing instance', function (done) { - service.destroy(1, function(error, data) { - expect(data.id).to.equal(1); - expect(data.name).to.equal('Test 1'); - done(); - }); - }); - - it('should return an error on db when instance to delete doesn\'t exist', function (done) { - service.destroy(3, function(error, data) { - expect(error).to.not.be.null; - expect(data).to.be.undefined; - done(); - }); - }); - - }); - - describe('find', function () { - - beforeEach(function(done){ - service.create({ - id: 2, - name: 'Bob' - }, function(){ - service.create({ - id: 3, - name: 'Alice' - }, function(){ - done(); - }); - }); - }); - - afterEach(function(done){ - service.destroy(2, function(){ - service.destroy(3, function(){ - done(); - }); - }); - }); - - it('should return all items', function(done){ - var expected = [ - { - id: 1, - name: 'Test 1' - }, - { - id: 2, - name: 'Bob' - }, - { - id: 3, - name: 'Alice' - } - ]; - - service.find({ query: {} }, function(err, items){ - expect(err).to.be.null; - expect(items).to.deep.equal(expected); - done(); - }); - }); - - it('should return all items sorted in ascending order by sort value', function(done){ - var expected = [ - { - id: 3, - name: 'Alice' - }, - { - id: 2, - name: 'Bob' - }, - { - id: 1, - name: 'Test 1' - } - ]; - - var query = { - query: { sort: 'name' } - }; - - service.find(query, function(err, items){ - expect(err).to.be.null; - expect(items).to.deep.equal(expected); - done(); - }); - }); - - it('should return all items sorted in descending order by sort value', function(done){ - var expected = [ - { - id: 1, - name: 'Test 1' - }, - { - id: 2, - name: 'Bob' - }, - { - id: 3, - name: 'Alice' - } - ]; - - var query = { - query: { sort: 'name', order: true } - }; - - service.find(query, function(err, items){ - expect(err).to.be.null; - expect(items).to.deep.equal(expected); - done(); - }); - }); - - it('should return the number of items set by the limit', function(done){ - var expected = [ - { - id: 1, - name: 'Test 1' - }, - { - id: 2, - name: 'Bob' - } - ]; - - var query = { - query: { limit: 2 } - }; - - service.find(query, function(err, items){ - expect(err).to.be.null; - expect(items).to.deep.equal(expected); - done(); - }); - }); - - it('should skip over the number of items set by skip', function(done){ - var expected = [ - { - id: 3, - name: 'Alice' - } - ]; - - var query = { - query: { skip: 2 } - }; - - service.find(query, function(err, items){ - expect(err).to.be.null; - expect(items).to.deep.equal(expected); - done(); - }); - }); - - }); -}); diff --git a/test/services/mongodb.test.js b/test/services/mongodb.test.js deleted file mode 100644 index a927c047aa..0000000000 --- a/test/services/mongodb.test.js +++ /dev/null @@ -1,104 +0,0 @@ -var chai = require('chai'); -var expect = chai.expect; -var MongoService = require('../../lib/services/mongodb'); -var Proto = require('uberproto'); -var service; -var _id; - -// TODO (EK): Mock out mongodb or something so that we -// can actually run these tests on CI - -describe('Mongo Service', function () { - beforeEach(function(done){ - service = Proto.create.call(MongoService, { - collection: 'test' - }); - service.create({ - // _id: '51d2325334244ade98000001', - name: 'Test 1' - }, function(error, data) { - _id = data[0]._id; - done(); - }); - }); - - afterEach(function(done){ - service.destroy(_id, function(err){ - done(); - }); - }); - - describe('init', function () { - it('should setup a mongo connection based on config'); - it('should setup a mongo connection based on ENV vars'); - it('should setup a mongo connection based on a connection string'); - }); - - describe('index', function () { - it('should return all items'); - it('should return all items sorted in ascending order'); - it('should return all items sorted in descending order'); - it('should return the number of items set by the limit'); - it('should skip over the number of items set by skip'); - }); - - describe('get', function () { - it('should return an instance that exists', function(done){ - service.get(_id, function(error, data) { - - expect(error).to.be.null; - expect(data._id.toString()).to.equal(_id.toString()); - expect(data.name).to.equal('Test 1'); - done(); - }); - }); - - it('should return an error on db error'); - }); - - describe('create', function () { - it('should create a single new instance', function(done){ - service.create({ - name: 'Test 2' - }, function(error, data) { - expect(error).to.be.null; - expect(data).to.be.instanceof(Array); - expect(data).to.not.be.empty; - expect(data[0].name).to.equal('Test 2'); - done(); - }); - }); - - it('should create multiple new instances', function(done){ - var items = [ - { - name: 'Test 3' - }, - { - name: 'Test 4' - } - ]; - - service.create(items, function(error, data) { - expect(error).to.be.null; - expect(data).to.be.instanceof(Array); - expect(data).to.not.be.empty; - expect(data[0].name).to.equal('Test 3'); - expect(data[1].name).to.equal('Test 4'); - done(); - }); - }); - - it('should return an error on db error'); - }); - - describe('update', function () { - it('should update an existing instance'); - it('should return an error on db error'); - }); - - describe('destroy', function () { - it('should delete an existing instance'); - it('should return an error on db error'); - }); -});