├── client.js ├── .babelrc ├── .travis.yml ├── .npmignore ├── src ├── client │ ├── index.js │ └── express.js ├── mixins │ ├── index.js │ ├── normalizer.js │ ├── promise.js │ └── event.js ├── feathers.js └── application.js ├── .jshintrc ├── .gitignore ├── test ├── resources │ ├── certrequest.csr │ ├── privatekey.pem │ └── certificate.pem ├── distributed.test.js ├── mixins │ ├── normalizer.test.js │ ├── promise.test.js │ └── event.test.js ├── client.test.js └── application.test.js ├── LICENSE ├── readme.md ├── package.json ├── contributing.md └── changelog.md /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client'); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "add-module-exports" ], 3 | "presets": [ "es2015" ] 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "node" 7 | - "iojs" -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ./.editorconfig 2 | ./.jshintrc 3 | ./.travis.yml 4 | ./.babelrc 5 | .idea/ 6 | src/ 7 | test/ 8 | !lib/ 9 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import feathers from '../feathers'; 2 | import express from './express'; 3 | 4 | export default function() { 5 | return feathers(express()); 6 | } 7 | -------------------------------------------------------------------------------- /src/mixins/index.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | const mixins = [ 3 | require('./promise'), 4 | require('./event'), 5 | require('./normalizer') 6 | ]; 7 | 8 | // Override push to make sure that normalize is always the last 9 | mixins.push = function() { 10 | const args = [ this.length - 1, 0].concat(Array.from(arguments)); 11 | this.splice.apply(this, args); 12 | return this.length; 13 | }; 14 | 15 | return mixins; 16 | } 17 | -------------------------------------------------------------------------------- /src/mixins/normalizer.js: -------------------------------------------------------------------------------- 1 | import { getArguments } from 'feathers-commons'; 2 | 3 | export default function (service) { 4 | if (typeof service.mixin === 'function') { 5 | const mixin = {}; 6 | 7 | this.methods.forEach(method => { 8 | if(typeof service[method] === 'function') { 9 | mixin[method] = function() { 10 | return this._super.apply(this, getArguments(method, arguments)); 11 | }; 12 | } 13 | }); 14 | 15 | service.mixin(mixin); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/feathers.js: -------------------------------------------------------------------------------- 1 | if(!global._babelPolyfill) { require('babel-polyfill'); } 2 | 3 | import express from 'express'; 4 | import Proto from 'uberproto'; 5 | import Application from './application'; 6 | 7 | /** 8 | * Create a Feathers application that extends Express. 9 | * 10 | * @return {Function} 11 | * @api public 12 | */ 13 | export default function createApplication(app = express()) { 14 | Proto.mixin(Application, app); 15 | app.init(); 16 | return app; 17 | } 18 | 19 | // Framework version 20 | createApplication.version = require('../package.json').version; 21 | 22 | // Expose all express methods (like express.engine()) 23 | Object.assign(createApplication, express); 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": false, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": false, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": false, 21 | "node": true, 22 | "globals": { 23 | "it": true, 24 | "describe": true, 25 | "before": true, 26 | "beforeEach": true, 27 | "after": true, 28 | "afterEach": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | tmp* 31 | .idea/ 32 | 33 | 34 | # The compiled/babelified modules 35 | lib/ 36 | -------------------------------------------------------------------------------- /test/resources/certrequest.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIB0jCCATsCAQAwgZExCzAJBgNVBAYTAkNBMRAwDgYDVQQIEwdBbGJlcnRhMRAw 3 | DgYDVQQHEwdDYWxnYXJ5MREwDwYDVQQKEwhGZWF0aGVyczERMA8GA1UECxMIRmVh 4 | dGhlcnMxEzARBgNVBAMTCkZlYXRoZXJzSlMxIzAhBgkqhkiG9w0BCQEWFGhlbGxv 5 | QGZlYXRoZXJzanMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4sXy8 6 | K7ww2AGmlOtqMnM/M8FKcQoLssl8oVQzfyjJj/yPh35G3CK6o7VtI26UVoi4Z+XM 7 | OCv3FE5HzI+AMGjebds6tBVRSXSHXS8c7mpsiSyRWX7ObD2FrV+pqKz3LvUTO48G 8 | 90dVfOdl41VlI/J3lALu3lMyqEkOe+ZB7Jyq3QIDAQABoAAwDQYJKoZIhvcNAQEF 9 | BQADgYEAFN1xm2Jc5EwDsiJwjUQkVCYLfAPz8FxLx8XCY7JugPCZWxeJ3w9C3Ymz 10 | hET//7uxNg6q7EO9CI33vP5eOdI8oC8XQffh4GzCoSrmGrKpHSqVh3zN/rCoB4BY 11 | f4nJofTka5iENjMdA0R8//Wp7F1u7xhriuxaRiZoFEPaCIsrvK4= 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /src/mixins/promise.js: -------------------------------------------------------------------------------- 1 | function isPromise(result) { 2 | return typeof result !== 'undefined' && 3 | typeof result.then === 'function'; 4 | } 5 | 6 | function wrapper() { 7 | const result = this._super.apply(this, arguments); 8 | const callback = arguments[arguments.length - 1]; 9 | 10 | if(typeof callback === 'function' && isPromise(result)) { 11 | result.then(data => callback(null, data), error => callback(error)); 12 | } 13 | return result; 14 | } 15 | 16 | export default function (service) { 17 | if (typeof service.mixin === 'function') { 18 | const mixin = {}; 19 | 20 | this.methods.forEach(method => { 21 | if(typeof service[method] === 'function') { 22 | mixin[method] = wrapper; 23 | } 24 | }); 25 | 26 | service.mixin(mixin); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/resources/privatekey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC4sXy8K7ww2AGmlOtqMnM/M8FKcQoLssl8oVQzfyjJj/yPh35G 3 | 3CK6o7VtI26UVoi4Z+XMOCv3FE5HzI+AMGjebds6tBVRSXSHXS8c7mpsiSyRWX7O 4 | bD2FrV+pqKz3LvUTO48G90dVfOdl41VlI/J3lALu3lMyqEkOe+ZB7Jyq3QIDAQAB 5 | AoGAYCTkzf/mY3bOxSzYr9u7ardCc8IMfLKBeMNy1avoS6UM0Jqz/acy3P3DwCCl 6 | u8qgOX68fWbwXBrR9UZjnVOWAvAgACS9bSTR4UxXuHve9YHf1s1Idm1Ck8CopiuY 7 | 0PTiuF7OJp6U7fc1RjO5F5tvSMuYbh+68Vpx9SQRfDHYqnECQQD1KnhSRDjLCfoB 8 | lLfTew99W51OTx2NPRKRXwZH/YwlgRl/cAgJhdemah/AAavB6BUdqEXdiIviEHuT 9 | UsfAXhf7AkEAwNrmEI3B4gtMRKJAsyWAKGFxDHuC9wGkhSxCVihQuxXtqEMX7Qnx 10 | ucU9bRRtUgVPcOmFEtpPsI4e0wkTMg+ZBwJAPL+ERuYuqGjVcPTXw+g3Q1mjFddW 11 | vDuI0UqZdNcnlddyaPhqlWl7sPmU2m/PjmGicdHTVfxSpPZumGenpUvrZwJAdodS 12 | 9QObEOmus1Qhfbljne3dhDV5FYTd77d3Aer/Syy8BzlNQDNnbKysBxmR4uI+o//x 13 | +NdSOQnwKfYe5RqvCwJBAMfq911uzlD8Kd9s0n+MJe8b5/duYOtgPZvaIFWOWyNm 14 | 0aJE/VovVhk2JGvIU9kxdgt9O4N0x2XukS2hq7I1Xts= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /src/client/express.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import Proto from 'uberproto'; 3 | 4 | export default function() { 5 | const app = { 6 | settings: {}, 7 | 8 | get(name) { 9 | return this.settings[name]; 10 | }, 11 | 12 | set(name, value) { 13 | this.settings[name] = value; 14 | return this; 15 | }, 16 | 17 | disable(name) { 18 | this.settings[name] = false; 19 | return this; 20 | }, 21 | 22 | disabled(name) { 23 | return !this.settings[name]; 24 | }, 25 | 26 | enable(name) { 27 | this.settings[name] = true; 28 | return this; 29 | }, 30 | 31 | enabled(name) { 32 | return !!this.settings[name]; 33 | }, 34 | 35 | use() { 36 | throw new Error('Middleware functions can not be used in the Feathers client'); 37 | }, 38 | 39 | listen() { 40 | return {}; 41 | } 42 | }; 43 | 44 | Proto.mixin(EventEmitter.prototype, app); 45 | 46 | return app; 47 | } 48 | -------------------------------------------------------------------------------- /test/resources/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICmzCCAgQCCQDugFqITnU/sDANBgkqhkiG9w0BAQUFADCBkTELMAkGA1UEBhMC 3 | Q0ExEDAOBgNVBAgTB0FsYmVydGExEDAOBgNVBAcTB0NhbGdhcnkxETAPBgNVBAoT 4 | CEZlYXRoZXJzMREwDwYDVQQLEwhGZWF0aGVyczETMBEGA1UEAxMKRmVhdGhlcnNK 5 | UzEjMCEGCSqGSIb3DQEJARYUaGVsbG9AZmVhdGhlcnNqcy5jb20wHhcNMTQwMTA0 6 | MDIwNTUyWhcNMTQwMjAzMDIwNTUyWjCBkTELMAkGA1UEBhMCQ0ExEDAOBgNVBAgT 7 | B0FsYmVydGExEDAOBgNVBAcTB0NhbGdhcnkxETAPBgNVBAoTCEZlYXRoZXJzMREw 8 | DwYDVQQLEwhGZWF0aGVyczETMBEGA1UEAxMKRmVhdGhlcnNKUzEjMCEGCSqGSIb3 9 | DQEJARYUaGVsbG9AZmVhdGhlcnNqcy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0A 10 | MIGJAoGBALixfLwrvDDYAaaU62oycz8zwUpxCguyyXyhVDN/KMmP/I+HfkbcIrqj 11 | tW0jbpRWiLhn5cw4K/cUTkfMj4AwaN5t2zq0FVFJdIddLxzuamyJLJFZfs5sPYWt 12 | X6morPcu9RM7jwb3R1V852XjVWUj8neUAu7eUzKoSQ575kHsnKrdAgMBAAEwDQYJ 13 | KoZIhvcNAQEFBQADgYEATVlxNPkSgkqBF4foUYNGnkvaiwhd88Mh/Ya3T3EnknF9 14 | Gz6KrlwWDDI8MkPmqabT2Ijg3LSec7WV+C8SETVFbWLOGV6N1ZVfodFzJ7EKMz5e 15 | VvEIKnHfHpYOEa21E5u02+OfKahtW37eTEVmvcV67vYmW4HNa5QSZ5qfrrqcUhc= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (C) 2015 [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors) 4 | 5 | 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 the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/distributed.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import client from 'feathers-client'; 3 | import io from 'socket.io-client'; 4 | import socketio from 'feathers-socketio'; 5 | import rest from 'feathers-rest'; 6 | import feathers from '../src/feathers'; 7 | 8 | describe('Distributed Feathers applications test', () => { 9 | before(done => { 10 | const app = feathers() 11 | .configure(socketio()) 12 | .use('todos', { 13 | create(data) { 14 | data.id = 42; 15 | return Promise.resolve(data); 16 | } 17 | }); 18 | 19 | app.listen(8888, done); 20 | }); 21 | 22 | it('passes created event between servers', done => { 23 | const socket = io('http://localhost:8888'); 24 | const remoteApp = client().configure(client.socketio(socket)); 25 | const todo = { text: 'Created on alpha server', complete: false }; 26 | const beta = feathers() 27 | .configure(rest()) 28 | .use('todos', remoteApp.service('todos')); 29 | 30 | beta.listen(9999, function() { 31 | beta.service('todos').on('created', function(newTodo) { 32 | assert.deepEqual(newTodo, { 33 | id: 42, 34 | text: 'Created on alpha server', 35 | complete: false 36 | }); 37 | done(); 38 | }); 39 | 40 | socket.emit('todos::create', todo); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/mixins/normalizer.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Proto from 'uberproto'; 3 | import normalizer from '../../src/mixins/normalizer'; 4 | import mixins from '../../src/mixins'; 5 | 6 | describe('Argument normalizer mixin', () => { 7 | it('normalizer mixin is always the last to run', () => { 8 | const arr = mixins(); 9 | const dummy = function() { }; 10 | 11 | assert.equal(arr.length, 3); 12 | 13 | arr.push(dummy); 14 | 15 | assert.equal(arr[arr.length - 1], normalizer, 'Last mixin is still the normalizer'); 16 | assert.equal(arr[arr.length - 2], dummy, 'Dummy got added before last'); 17 | }); 18 | 19 | // The normalization is already tests in all variations in `getArguments` 20 | // so we just so we only test two random samples 21 | 22 | it('normalizes .find without a callback', done => { 23 | const context = { methods: ['find'] }; 24 | const FixtureService = Proto.extend({ 25 | find(params, callback) { 26 | assert.ok(typeof callback === 'function'); 27 | assert.equal(params.test, 'Here'); 28 | done(); 29 | } 30 | }); 31 | 32 | normalizer.call(context, FixtureService); 33 | 34 | const instance = Proto.create.call(FixtureService); 35 | 36 | instance.find({ test: 'Here' }); 37 | }); 38 | 39 | it('normalizes .update without params and callback', done => { 40 | const context = { methods: ['update'] }; 41 | const FixtureService = Proto.extend({ 42 | update(id, data, params, callback) { 43 | assert.equal(id, 1); 44 | assert.ok(typeof callback === 'function'); 45 | assert.deepEqual(data, { test: 'Here' }); 46 | assert.deepEqual(params, {}); 47 | done(); 48 | } 49 | }); 50 | 51 | normalizer.call(context, FixtureService); 52 | 53 | const instance = Proto.create.call(FixtureService); 54 | 55 | instance.update(1, { test: 'Here' }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/mixins/event.js: -------------------------------------------------------------------------------- 1 | import rubberduck from 'rubberduck'; 2 | import { EventEmitter } from 'events'; 3 | import { hooks } from 'feathers-commons'; 4 | 5 | const hookObject = hooks.hookObject; 6 | const eventMappings = { 7 | create: 'created', 8 | update: 'updated', 9 | remove: 'removed', 10 | patch: 'patched' 11 | }; 12 | 13 | function upperCase(name) { 14 | return name.charAt(0).toUpperCase() + name.substring(1); 15 | } 16 | 17 | export default function(service) { 18 | const isEmitter = typeof service.on === 'function' && 19 | typeof service.emit === 'function'; 20 | const emitter = service._rubberDuck = rubberduck.emitter(service); 21 | 22 | if(typeof service.mixin === 'function' && !isEmitter) { 23 | service.mixin(EventEmitter.prototype); 24 | } 25 | 26 | service._serviceEvents = Array.isArray(service.events) ? service.events.slice() : []; 27 | 28 | // Pass the Rubberduck error event through 29 | // TODO deal with error events properly 30 | emitter.on('error', function (errors) { 31 | service.emit('serviceError', errors[0]); 32 | }); 33 | 34 | Object.keys(eventMappings).forEach(method => { 35 | const event = eventMappings[method]; 36 | const alreadyEmits = service._serviceEvents.indexOf(event) !== -1; 37 | 38 | if (typeof service[method] === 'function' && !alreadyEmits) { 39 | // The Rubberduck event name (e.g. afterCreate, afterUpdate or afterDestroy) 40 | var eventName = `after${upperCase(method)}`; 41 | service._serviceEvents.push(event); 42 | // Punch the given method 43 | emitter.punch(method, -1); 44 | // Pass the event and error event through 45 | emitter.on(eventName, function (results, args) { 46 | if (!results[0]) { // callback without error 47 | const hook = hookObject(method, 'after', args); 48 | const data = Array.isArray(results[1]) ? results[1] : [ results[1] ]; 49 | 50 | data.forEach(current => service.emit(event, current, hook)); 51 | } else { 52 | service.emit('serviceError', results[0]); 53 | } 54 | }); 55 | } 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/mixins/promise.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Proto from 'uberproto'; 3 | import mixin from '../../src/mixins/promise'; 4 | 5 | const create = Proto.create; 6 | 7 | describe('Promises/A+ mixin', () => { 8 | it('Calls a callback when a promise is returned from the original service', done => { 9 | // A dummy context (this will normally be the application) 10 | const context = { methods: ['get'] }; 11 | const FixtureService = Proto.extend({ 12 | get: function (id) { 13 | return Promise.resolve({ 14 | id: id, 15 | description: `You have to do ${id}` 16 | }); 17 | } 18 | }); 19 | 20 | mixin.call(context, FixtureService); 21 | 22 | const instance = create.call(FixtureService); 23 | instance.get('dishes', {}, function (error, data) { 24 | assert.deepEqual(data, { 25 | id: 'dishes', 26 | description: 'You have to do dishes' 27 | }); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('calls back with an error for a failing deferred', done => { 33 | // A dummy context (this will normally be the application) 34 | var context = { 35 | methods: ['get'] 36 | }; 37 | var FixtureService = Proto.extend({ 38 | get: function () { 39 | return Promise.reject(new Error('Something went wrong')); 40 | } 41 | }); 42 | 43 | mixin.call(context, FixtureService); 44 | 45 | var instance = create.call(FixtureService); 46 | instance.get('dishes', {}, function (error) { 47 | assert.ok(error); 48 | assert.equal(error.message, 'Something went wrong'); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('does not try to call the callback if it does not exist', function(done) { 54 | // A dummy context (this will normally be the application) 55 | const context = { methods: ['create'] }; 56 | const FixtureService = Proto.extend({ 57 | create(data) { 58 | return Promise.resolve(data); 59 | } 60 | }); 61 | const original = { 62 | id: 'laundry', 63 | description: 'You have to do laundry' 64 | }; 65 | 66 | mixin.call(context, FixtureService); 67 | 68 | const instance = create.call(FixtureService); 69 | instance.create(original, {}).then(data => { 70 | assert.deepEqual(data, original); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | Feathers logo 3 |

Build Better APIs, Faster than Ever

4 |
5 | 6 | [![NPM](https://nodei.co/npm/feathers.png?downloads=true&stars=true)](https://nodei.co/npm/feathers/) 7 | 8 | [![Build Status](https://travis-ci.org/feathersjs/feathers.png?branch=master)](https://travis-ci.org/feathersjs/feathers) 9 | [![Code Climate](https://codeclimate.com/github/feathersjs/feathers.png)](https://codeclimate.com/github/feathersjs/feathers) 10 | [![Slack Status](http://slack.feathersjs.com/badge.svg)](http://slack.feathersjs.com) 11 | 12 | Feathers is a real-time, micro-service web framework for NodeJS that gives you control over your data via RESTful resources, sockets and flexible plug-ins. 13 | 14 | ## Getting started 15 | 16 | Visit the website at [feathersjs.com](http://feathersjs.com) to read the [Getting started guide](http://feathersjs.com/quick-start/) or learn how to build real-time applications with jQuery, Angular, React, CanJS, iOS, Android - you name it - and Feathers as the backend [in our guides](http://feathersjs.com/learn/). 17 | 18 | ## A MongoDB REST and real-time API 19 | 20 | Want to see it in action? Here is a full REST and real-time todo API that uses MongoDB: 21 | 22 | ```js 23 | // app.js 24 | import feathers from 'feathers'; 25 | import rest from 'feathers-rest'; 26 | import socketio from 'feathers-socketio'; 27 | import mongodb from 'feathers-mongodb'; 28 | import bodyParser from 'body-parser'; 29 | 30 | const app = feathers(); 31 | const todoService = mongodb({ 32 | db: 'feathers-demo', 33 | collection: 'todos' 34 | }); 35 | 36 | app.configure(rest()) 37 | .configure(socketio()) 38 | .use(bodyParser.json()) 39 | .use('/todos', todoService) 40 | .use('/', feathers.static(__dirname)) 41 | .listen(3000); 42 | ``` 43 | 44 | Then run 45 | 46 | ``` 47 | npm install feathers feathers-rest feathers-socketio feathers-mongodb body-parser 48 | node app 49 | ``` 50 | 51 | and go to [http://localhost:3000/todos](http://localhost:3000/todos). That's all the code you need to have a full real-time CRUD API. 52 | 53 | Don't want to use MongoDB? Feathers has plugins for [many other databases](http://feathersjs.com/learn/) and you can easily [write your own adapters](http://feathersjs.com/quick-start/). 54 | 55 | ## License 56 | 57 | [MIT](LICENSE) 58 | 59 | ## Authors 60 | 61 | [Feathers contributors](https://github.com/feathersjs/feathers/graphs/contributors) 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers", 3 | "description": "Build Better APIs, Faster than Ever.", 4 | "version": "2.0.0-pre.2", 5 | "homepage": "http://feathersjs.com", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/feathersjs/feathers.git" 9 | }, 10 | "keywords": [ 11 | "feathers", 12 | "REST", 13 | "socket.io", 14 | "realtime" 15 | ], 16 | "author": "Feathers (http://feathersjs.com)", 17 | "contributors": [ 18 | "Eric Kryski (http://erickryski.com)", 19 | "David Luecke (http://neyeon.com)" 20 | ], 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/feathersjs/feathers/issues" 24 | }, 25 | "main": "lib/feathers", 26 | "directories": { 27 | "lib": "lib" 28 | }, 29 | "scripts": { 30 | "prepublish": "npm run compile", 31 | "publish": "git push origin && git push origin --tags", 32 | "release:patch": "npm version patch && npm publish", 33 | "release:minor": "npm version minor && npm publish", 34 | "release:major": "npm version major && npm publish", 35 | "release:premajor": "npm version premajor && npm publish", 36 | "release:prerelease": "npm version prerelease && npm publish", 37 | "compile": "rm -rf lib/ && babel -d lib/ src/", 38 | "watch": "babel --watch -d lib/ src/", 39 | "jshint": "jshint src/. test/. --config", 40 | "coverage": "istanbul cover _mocha -- test/ --recursive", 41 | "mocha": "mocha test/ --compilers js:babel-core/register --recursive", 42 | "test": "npm run compile && npm run jshint && npm run mocha" 43 | }, 44 | "engines": { 45 | "node": ">= 0.10.0", 46 | "npm": ">= 1.3.0" 47 | }, 48 | "dependencies": { 49 | "babel-polyfill": "^6.3.14", 50 | "debug": "^2.1.1", 51 | "express": "^4.12.3", 52 | "feathers-commons": "^0.5.0", 53 | "rubberduck": "^1.0.0", 54 | "uberproto": "^1.2.0" 55 | }, 56 | "devDependencies": { 57 | "babel-cli": "^6.3.17", 58 | "babel-core": "^6.3.26", 59 | "babel-plugin-add-module-exports": "^0.1.2", 60 | "babel-preset-es2015": "^6.3.13", 61 | "body-parser": "^1.13.2", 62 | "feathers-client": "^0.2.1", 63 | "feathers-rest": "^1.0.0", 64 | "feathers-socketio": "^1.0.0", 65 | "istanbul": "^0.4.0", 66 | "jshint": "^2.6.3", 67 | "mocha": "^2.2.0", 68 | "q": "^1.0.1", 69 | "request": "^2.x", 70 | "socket.io-client": "^1.0.0" 71 | }, 72 | "browser": { 73 | "express": "./lib/client/express", 74 | "./lib/feathers": "./lib/client/index", 75 | "babel-polyfill": false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! 4 | 5 | ## Report a bug 6 | 7 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues) or the [Gitter chat](https://gitter.im/feathersjs/feathers). 8 | 9 | If you have any other questions, feel free to submit post them on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag or join [#feathersjs](http://webchat.freenode.net/?channels=feathersjs) on Freenode IRC. 10 | 11 | ## Code style 12 | 13 | Before running the tests from the `test/` folder `npm test` will run JSHint. You can check your code changes individually by running `npm run jshint`. 14 | 15 | ## ES6 compilation 16 | 17 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 18 | 19 | > npm run compile 20 | 21 | __Note:__ `npm test` will run the compilation automatically before the tests. 22 | 23 | ## Tests 24 | 25 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with JSHint) command. 26 | 27 | ## Documentation 28 | 29 | Feathers documentation is contained Markdown files in the `/docs` folder. To change the documentation submit a pull request and it will be updated with the next release. 30 | 31 | # Contributor Code of Conduct 32 | 33 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 34 | 35 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 36 | 37 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 38 | 39 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 40 | 41 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 44 | -------------------------------------------------------------------------------- /src/application.js: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | import { stripSlashes } from 'feathers-commons'; 3 | import Uberproto from 'uberproto'; 4 | import mixins from './mixins'; 5 | 6 | const debug = makeDebug('feathers:application'); 7 | const methods = ['find', 'get', 'create', 'update', 'patch', 'remove']; 8 | const Proto = Uberproto.extend({ 9 | create: null 10 | }); 11 | 12 | export default { 13 | init() { 14 | Object.assign(this, { 15 | methods, 16 | mixins: mixins(), 17 | services: {}, 18 | providers: [], 19 | _setup: false 20 | }); 21 | }, 22 | 23 | service(location, service, options = {}) { 24 | location = stripSlashes(location); 25 | 26 | if(!service) { 27 | const current = this.services[location]; 28 | 29 | if(typeof current === 'undefined' && typeof this.defaultService === 'function') { 30 | return this.service(location, this.defaultService(location), options); 31 | } 32 | 33 | return current; 34 | } 35 | 36 | let protoService = Proto.extend(service); 37 | 38 | debug(`Registering new service at \`${location}\``); 39 | 40 | // Add all the mixins 41 | this.mixins.forEach(fn => fn.call(this, protoService)); 42 | 43 | if(typeof protoService._setup === 'function') { 44 | protoService._setup(this, location); 45 | } 46 | 47 | // Run the provider functions to register the service 48 | this.providers.forEach(provider => 49 | provider.call(this, location, protoService, options)); 50 | 51 | // If we ran setup already, set this service up explicitly 52 | if (this._isSetup && typeof protoService.setup === 'function') { 53 | debug(`Setting up service for \`${location}\``); 54 | protoService.setup(this, location); 55 | } 56 | 57 | return (this.services[location] = protoService); 58 | }, 59 | 60 | use(location) { 61 | let service, middleware = Array.from(arguments) 62 | .slice(1) 63 | .reduce(function (middleware, arg) { 64 | if (typeof arg === 'function') { 65 | middleware[service ? 'after' : 'before'].push(arg); 66 | } else if (!service) { 67 | service = arg; 68 | } else { 69 | throw new Error('invalid arg passed to app.use'); 70 | } 71 | return middleware; 72 | }, { 73 | before: [], 74 | after: [] 75 | }); 76 | 77 | const hasMethod = methods => methods.some(name => 78 | (service && typeof service[name] === 'function')); 79 | 80 | // Check for service (any object with at least one service method) 81 | if(hasMethod(['handle', 'set']) || !hasMethod(this.methods)) { 82 | return this._super.apply(this, arguments); 83 | } 84 | 85 | // Any arguments left over are other middleware that we want to pass to the providers 86 | this.service(location, service, { middleware }); 87 | 88 | return this; 89 | }, 90 | 91 | setup() { 92 | // Setup each service (pass the app so that they can look up other services etc.) 93 | Object.keys(this.services).forEach(path => { 94 | const service = this.services[path]; 95 | 96 | debug(`Setting up service for \`${path}\``); 97 | if (typeof service.setup === 'function') { 98 | service.setup(this, path); 99 | } 100 | }); 101 | 102 | this._isSetup = true; 103 | 104 | return this; 105 | }, 106 | 107 | // Express 3.x configure is gone in 4.x but we'll keep a more basic version 108 | // That just takes a function in order to keep Feathers plugin configuration easier. 109 | // Environment specific configurations should be done as suggested in the 4.x migration guide: 110 | // https://github.com/visionmedia/express/wiki/Migrating-from-3.x-to-4.x 111 | configure(fn){ 112 | fn.call(this); 113 | 114 | return this; 115 | }, 116 | 117 | listen() { 118 | const server = this._super.apply(this, arguments); 119 | 120 | this.setup(server); 121 | debug('Feathers application listening'); 122 | 123 | return server; 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Proto from 'uberproto'; 3 | import feathers from '../src/client'; 4 | 5 | describe('Feathers universal client', () => { 6 | it('is not an Express application', () => { 7 | const app = feathers(); 8 | // There may be some other better ways to verify this but it works for now 9 | assert.ok(typeof app.render !== 'function'); 10 | }); 11 | 12 | it('is an event emitter', done => { 13 | const app = feathers(); 14 | const original = { hello: 'world' }; 15 | app.on('test', data => { 16 | assert.deepEqual(original, data); 17 | done(); 18 | }); 19 | 20 | app.emit('test', original); 21 | }); 22 | 23 | it('Registers a service, wraps it, runs service.setup(), and adds the event and Promise mixin', done => { 24 | const dummyService = { 25 | setup(app, path){ 26 | this.path = path; 27 | }, 28 | 29 | create(data) { 30 | return Promise.resolve(data); 31 | } 32 | }; 33 | 34 | const app = feathers().use('/dummy', dummyService); 35 | const wrappedService = app.service('dummy'); 36 | 37 | assert.ok(Proto.isPrototypeOf(wrappedService), 'Service got wrapped as Uberproto object'); 38 | assert.ok(typeof wrappedService.on === 'function', 'Wrapped service is an event emitter'); 39 | 40 | wrappedService.on('created', function (data) { 41 | assert.equal(data.message, 'Test message', 'Got created event with test message'); 42 | done(); 43 | }); 44 | 45 | wrappedService.create({ 46 | message: 'Test message' 47 | }).then(data => 48 | assert.equal(data.message, 'Test message', 'Got created event with test message')); 49 | }); 50 | 51 | // Copied from the Express tests (without special cases) 52 | describe('Express app options compatibility', function () { 53 | describe('.set()', () => { 54 | it('should set a value', () => { 55 | var app = feathers(); 56 | app.set('foo', 'bar'); 57 | assert.equal(app.get('foo'), 'bar'); 58 | }); 59 | 60 | it('should return the app', () => { 61 | var app = feathers(); 62 | assert.equal(app.set('foo', 'bar'), app); 63 | }); 64 | 65 | it('should return the app when undefined', () => { 66 | var app = feathers(); 67 | assert.equal(app.set('foo', undefined), app); 68 | }); 69 | }); 70 | 71 | describe('.get()', () => { 72 | it('should return undefined when unset', () => { 73 | var app = feathers(); 74 | assert.strictEqual(app.get('foo'), undefined); 75 | }); 76 | 77 | it('should otherwise return the value', () => { 78 | var app = feathers(); 79 | app.set('foo', 'bar'); 80 | assert.equal(app.get('foo'), 'bar'); 81 | }); 82 | }); 83 | 84 | describe('.enable()', () => { 85 | it('should set the value to true', () => { 86 | var app = feathers(); 87 | assert.equal(app.enable('tobi'), app); 88 | assert.strictEqual(app.get('tobi'), true); 89 | }); 90 | }); 91 | 92 | describe('.disable()', () => { 93 | it('should set the value to false', () => { 94 | var app = feathers(); 95 | assert.equal(app.disable('tobi'), app); 96 | assert.strictEqual(app.get('tobi'), false); 97 | }); 98 | }); 99 | 100 | describe('.enabled()', () => { 101 | it('should default to false', () => { 102 | var app = feathers(); 103 | assert.strictEqual(app.enabled('foo'), false); 104 | }); 105 | 106 | it('should return true when set', () => { 107 | var app = feathers(); 108 | app.set('foo', 'bar'); 109 | assert.strictEqual(app.enabled('foo'), true); 110 | }); 111 | }); 112 | 113 | describe('.disabled()', () => { 114 | it('should default to true', () => { 115 | var app = feathers(); 116 | assert.strictEqual(app.disabled('foo'), true); 117 | }); 118 | 119 | it('should return false when set', () => { 120 | var app = feathers(); 121 | app.set('foo', 'bar'); 122 | assert.strictEqual(app.disabled('foo'), false); 123 | }); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | __1.3.0__ 4 | 5 | - Add ability to create, update, patch and remove many ([#144](https://github.com/feathersjs/feathers/issues/144), [#179](https://github.com/feathersjs/feathers/pull/179)) 6 | - Handle middleware passed after the service to app.use ([#176](https://github.com/feathersjs/feathers/issues/176), [#178](https://github.com/feathersjs/feathers/pull/178)) 7 | 8 | __1.2.0__ 9 | 10 | - Add hook object to service events parameters ([#148](https://github.com/feathersjs/feathers/pull/148)) 11 | - Argument normalization runs before event mixin punched methods ([#150](https://github.com/feathersjs/feathers/issues/150)) 12 | 13 | __1.1.1__ 14 | 15 | - Fix 404 not being properly thrown by REST provider ([#146](https://github.com/feathersjs/feathers/pull/146)) 16 | 17 | __1.1.0__ 18 | 19 | - Service `setup` called before `app.io` instantiated ([#131](https://github.com/feathersjs/feathers/issues/131)) 20 | - Allow to register services that already are event emitter ([#118](https://github.com/feathersjs/feathers/issues/118)) 21 | - Clustering microservices ([#121](https://github.com/feathersjs/feathers/issues/121)) 22 | - Add debug module and messages ([#114](https://github.com/feathersjs/feathers/issues/114)) 23 | - Server hardening with socket message validation and normalization ([#113](https://github.com/feathersjs/feathers/issues/113)) 24 | - Custom service events ([#111](https://github.com/feathersjs/feathers/issues/111)) 25 | - Support for registering services dynamically ([#67](https://github.com/feathersjs/feathers/issues/67), [#96](https://github.com/feathersjs/feathers/issues/96), [#107](https://github.com/feathersjs/feathers/issues/107)) 26 | 27 | __1.0.2__ 28 | 29 | - Use Uberproto extended instance when creating services ([#105](https://github.com/feathersjs/feathers/pull/105)) 30 | - Make sure that mixins are specific to each new app ([#104](https://github.com/feathersjs/feathers/pull/104)) 31 | 32 | __1.0.1__ 33 | 34 | - Rename Uberproto .create to avoid conflicts with service method ([#100](https://github.com/feathersjs/feathers/pull/100), [#99](https://github.com/feathersjs/feathers/issues/99)) 35 | 36 | __[1.0.0](https://github.com/feathersjs/feathers/issues?q=milestone%3A1.0.0)__ 37 | 38 | - Remove app.lookup and make the functionality available as app.service ([#94](https://github.com/feathersjs/feathers/pull/94)) 39 | - Allow not passing parameters in websocket calls ([#92](https://github.com/feathersjs/feathers/pull/91)) 40 | - Add _setup method ([#91](https://github.com/feathersjs/feathers/pull/91)) 41 | - Throw an error when registering a service after application start ([#78](https://github.com/feathersjs/feathers/pull/78)) 42 | - Send socket parameters as params.query ([#72](https://github.com/feathersjs/feathers/pull/72)) 43 | - Send HTTP 201 and 204 status codes ([#71](https://github.com/feathersjs/feathers/pull/71)) 44 | - Upgrade to SocketIO 1.0 ([#70](https://github.com/feathersjs/feathers/pull/70)) 45 | - Upgrade to Express 4.0 ([#55](https://github.com/feathersjs/feathers/pull/55), [#54](https://github.com/feathersjs/feathers/issues/54)) 46 | - Allow service methods to return a promise ([#59](https://github.com/feathersjs/feathers/pull/59)) 47 | - Allow to register services with custom middleware ([#56](https://github.com/feathersjs/feathers/pull/56)) 48 | - REST provider should not be added by default ([#53](https://github.com/feathersjs/feathers/issues/53)) 49 | 50 | __[0.4.0](https://github.com/feathersjs/feathers/issues?q=milestone%3A0.4.0)__ 51 | 52 | - Allow socket provider event filtering and params passthrough ([#49](https://github.com/feathersjs/feathers/pull/49), [#50](https://github.com/feathersjs/feathers/pull/50), [#51](https://github.com/feathersjs/feathers/pull/51)) 53 | - Added `patch` support ([#47](https://github.com/feathersjs/feathers/pull/47)) 54 | - Allow to configure REST handler manually ([#40](https://github.com/feathersjs/feathers/issues/40), [#52](https://github.com/feathersjs/feathers/pull/52)) 55 | 56 | 57 | __0.3.2__ 58 | 59 | - Allows Feathers to use other Express apps ([#46](https://github.com/feathersjs/feathers/pull/46)) 60 | - Updated dependencies and switched to Lodash ([#42](https://github.com/feathersjs/feathers/pull/42)) 61 | 62 | __0.3.1__ 63 | 64 | - REST provider refactoring ([#35](https://github.com/feathersjs/feathers/pull/35)) to make it easier to develop plugins 65 | - HTTP requests now return 405 (Method not allowed) when trying to access unavailable service methods ([#35](https://github.com/feathersjs/feathers/pull/35)) 66 | 67 | __0.3.0__ 68 | 69 | - Added [Primus](https://github.com/primus/primus) provider ([#34](https://github.com/feathersjs/feathers/pull/34)) 70 | - `app.setup(server)` to support HTTPS (and other functionality that requires a custom server) ([#33](https://github.com/feathersjs/feathers/pull/33)) 71 | - Removed bad SocketIO configuration ([#19](https://github.com/feathersjs/feathers/issues/19)) 72 | - Add .npmignore to not publish .idea folder ([#30](https://github.com/feathersjs/feathers/issues/30)) 73 | - Remove middleware: connect.bodyParser() ([#27](https://github.com/feathersjs/feathers/pull/27)) 74 | 75 | __0.2.0__ 76 | 77 | - Pre-initialize `req.feathers` in REST provider to set service parameters 78 | - Allowing to initialize services with or without slashes to be more express-compatible 79 | 80 | __0.1.0__ 81 | 82 | - First beta release 83 | - Directly extends Express 84 | - Removed built in services and moved to [Legs](https://github.com/feathersjs/legs) 85 | - Created [example repository](https://github.com/feathersjs/examples) 86 | 87 | __0.0.x__ 88 | 89 | - Initial test alpha releases 90 | -------------------------------------------------------------------------------- /test/mixins/event.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Proto from 'uberproto'; 3 | import { EventEmitter } from 'events'; 4 | import mixinEvent from '../../src/mixins/event'; 5 | 6 | const create = Proto.create; 7 | 8 | describe('Event mixin', () => { 9 | it('initializes', () => { 10 | const FixtureService = Proto.extend({ 11 | setup(arg) { 12 | return `Original setup: ${arg}`; 13 | } 14 | }); 15 | 16 | mixinEvent(FixtureService); 17 | 18 | assert.equal(typeof FixtureService.setup, 'function'); 19 | assert.equal(typeof FixtureService.on, 'function'); 20 | assert.equal(typeof FixtureService.emit, 'function'); 21 | 22 | const instance = create.call(FixtureService); 23 | assert.equal('Original setup: Test', instance.setup('Test')); 24 | assert.ok(instance._rubberDuck instanceof EventEmitter); 25 | 26 | const existingMethodsService = { 27 | setup(arg) { 28 | return `Original setup from object: ${arg}`; 29 | } 30 | }; 31 | 32 | Proto.mixin(EventEmitter.prototype, existingMethodsService); 33 | 34 | assert.equal('Original setup from object: Test', existingMethodsService.setup('Test')); 35 | assert.equal(typeof existingMethodsService.on, 'function'); 36 | }); 37 | 38 | it('serviceError', function (done) { 39 | var FixtureService = Proto.extend({ 40 | create(data, params, cb) { 41 | cb(new Error('Something went wrong')); 42 | } 43 | }); 44 | 45 | const instance = create.call(FixtureService); 46 | 47 | mixinEvent(instance); 48 | 49 | instance.on('serviceError', function (error) { 50 | assert.ok(error instanceof Error); 51 | assert.equal(error.message, 'Something went wrong'); 52 | done(); 53 | }); 54 | 55 | instance.create({ name: 'Tester' }, {}, 56 | error => assert.ok(error instanceof Error)); 57 | }); 58 | 59 | it('created', done => { 60 | const FixtureService = Proto.extend({ 61 | create: function (data, params, callback) { 62 | callback(null, { 63 | id: 10, 64 | name: data.name 65 | }); 66 | } 67 | }); 68 | 69 | const instance = create.call(FixtureService); 70 | 71 | mixinEvent(instance); 72 | 73 | instance.on('created', function (data, args) { 74 | assert.equal(data.id, 10); 75 | assert.equal(data.name, 'Tester'); 76 | assert.equal(args.data.name, 'Tester'); 77 | assert.equal(args.params.custom, 'value'); 78 | done(); 79 | }); 80 | 81 | instance.create({ 82 | name: 'Tester' 83 | }, { 84 | custom: 'value' 85 | }, (error, data) => assert.equal(data.id, 10)); 86 | }); 87 | 88 | it('updated', done => { 89 | const FixtureService = Proto.extend({ 90 | update(id, data, params, cb) { 91 | setTimeout(function () { 92 | cb(null, { 93 | id: id, 94 | name: data.name 95 | }); 96 | }, 20); 97 | } 98 | }); 99 | 100 | const instance = create.call(FixtureService); 101 | 102 | mixinEvent(instance); 103 | 104 | instance.on('updated', function (data, args) { 105 | assert.equal(data.id, 12); 106 | assert.equal(data.name, 'Updated tester'); 107 | assert.equal(args.id, 12); 108 | assert.equal(args.data.name, 'Updated tester'); 109 | assert.equal(args.params.custom, 'value'); 110 | done(); 111 | }); 112 | 113 | instance.update(12, { 114 | name: 'Updated tester' 115 | }, { 116 | custom: 'value' 117 | }, function (error, data) { 118 | assert.equal(data.id, 12); 119 | }); 120 | }); 121 | 122 | it('removed', done => { 123 | const FixtureService = Proto.extend({ 124 | remove(id, params, cb) { 125 | setTimeout(function () { 126 | cb(null, { 127 | id: id 128 | }); 129 | }, 20); 130 | } 131 | }); 132 | 133 | const instance = create.call(FixtureService); 134 | 135 | mixinEvent(instance); 136 | 137 | instance.on('removed', function (data, args) { 138 | assert.equal(data.id, 27); 139 | assert.equal(args.id, 27); 140 | assert.equal(args.params.custom, 'value'); 141 | done(); 142 | }); 143 | 144 | instance.remove(27, { 145 | custom: 'value' 146 | }, function (error, data) { 147 | assert.equal(data.id, 27); 148 | }); 149 | }); 150 | 151 | it('array event data emits multiple event', done => { 152 | const fixture = [ 153 | { id: 0 }, 154 | { id: 1 }, 155 | { id: 2 }, 156 | ]; 157 | const FixtureService = Proto.extend({ 158 | create(data, params, cb) { 159 | setTimeout(function () { 160 | cb(null, fixture); 161 | }, 20); 162 | } 163 | }); 164 | 165 | const instance = create.call(FixtureService); 166 | let counter = 0; 167 | 168 | mixinEvent(instance); 169 | 170 | instance.on('created', function (data) { 171 | assert.equal(data.id, counter); 172 | counter++; 173 | if(counter === fixture.length) { 174 | done(); 175 | } 176 | }); 177 | 178 | instance.create({}, {}, function () {}); 179 | }); 180 | 181 | it('does not punch when service has an events list (#118)', done => { 182 | const FixtureService = Proto.extend({ 183 | events: [ 'created' ], 184 | create(data, params, cb) { 185 | setTimeout(function () { 186 | cb(null, { 187 | id: 10, 188 | name: data.name 189 | }); 190 | }, 20); 191 | } 192 | }); 193 | 194 | FixtureService.mixin(EventEmitter.prototype); 195 | 196 | const instance = create.call(FixtureService); 197 | 198 | mixinEvent(instance); 199 | 200 | instance.on('created', function (data) { 201 | assert.deepEqual(data, { custom: 'event' }); 202 | done(); 203 | }); 204 | 205 | instance.create({ 206 | name: 'Tester' 207 | }, {}, function (error, data) { 208 | assert.equal(data.id, 10); 209 | instance.emit('created', { custom: 'event' }); 210 | }); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /test/application.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import Proto from 'uberproto'; 5 | import io from 'socket.io-client'; 6 | import request from 'request'; 7 | import https from 'https'; 8 | import rest from 'feathers-rest'; 9 | import socketio from 'feathers-socketio'; 10 | import feathers from '../src/feathers'; 11 | 12 | describe('Feathers application', () => { 13 | it('is CommonJS compatible', () => { 14 | assert.equal(typeof require('../lib/feathers'), 'function'); 15 | }); 16 | 17 | it('Express application should use express apps.', () => { 18 | const app = feathers(); 19 | const child = feathers(); 20 | 21 | app.use('/path', child); 22 | assert.equal(child.parent, app); 23 | }); 24 | 25 | it('Register services and look them up with and without leading and trailing slashes.', () => { 26 | const dummyService = { 27 | find() { 28 | // No need to implement this 29 | } 30 | }; 31 | 32 | const app = feathers().use('/dummy/service/', dummyService); 33 | 34 | app.listen(8012, () => app.use('/another/dummy/service/', dummyService)); 35 | 36 | assert.ok(typeof app.service('dummy/service').find === 'function', 'Could look up without slashes'); 37 | assert.ok(typeof app.service('/dummy/service').find === 'function', 'Could look up with leading slash'); 38 | assert.ok(typeof app.service('dummy/service/').find === 'function', 'Could look up with trailing slash'); 39 | 40 | app.on('listening', function () { 41 | assert.ok(typeof app.service('another/dummy/service').find === 'function', 'Could look up without slashes'); 42 | assert.ok(typeof app.service('/another/dummy/service').find === 'function', 'Could look up with leading slash'); 43 | assert.ok(typeof app.service('another/dummy/service/').find === 'function', 'Could look up with trailing slash'); 44 | }); 45 | }); 46 | 47 | it('uses .defaultService if available', done => { 48 | const app = feathers(); 49 | 50 | assert.ok(!app.service('/todos/')); 51 | 52 | app.defaultService = function(path) { 53 | assert.equal(path, 'todos'); 54 | return { 55 | get(id) { 56 | return Promise.resolve({ 57 | id, description: `You have to do ${id}!` 58 | }); 59 | } 60 | }; 61 | }; 62 | 63 | app.service('/todos/').get('dishes').then(data => { 64 | assert.deepEqual(data, { 65 | id: 'dishes', 66 | description: 'You have to do dishes!' 67 | }); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('Registers a service, wraps it, runs service.setup(), and adds the event and Promise mixin', done => { 73 | const dummyService = { 74 | setup(app, path){ 75 | this.path = path; 76 | }, 77 | 78 | create(data) { 79 | return Promise.resolve(data); 80 | } 81 | }; 82 | 83 | const app = feathers().use('/dummy', dummyService); 84 | const wrappedService = app.service('dummy'); 85 | const server = app.listen(7887, function(){ 86 | app.use('/dumdum', dummyService); 87 | const dynamicService = app.service('dumdum'); 88 | 89 | assert.ok(wrappedService.path === 'dummy', 'Wrapped service setup method ran.'); 90 | assert.ok(dynamicService.path === 'dumdum', 'Dynamic service setup method ran.'); 91 | }); 92 | 93 | assert.ok(Proto.isPrototypeOf(wrappedService), 'Service got wrapped as Uberproto object'); 94 | assert.ok(typeof wrappedService.on === 'function', 'Wrapped service is an event emitter'); 95 | 96 | wrappedService.on('created', function (data) { 97 | assert.equal(data.message, 'Test message', 'Got created event with test message'); 98 | server.close(done); 99 | }); 100 | 101 | wrappedService.create({ 102 | message: 'Test message' 103 | }).then(data => 104 | assert.equal(data.message, 'Test message', 'Got created event with test message')); 105 | }); 106 | 107 | it('Initializes REST and SocketIO providers.', function (done) { 108 | const todoService = { 109 | get(name, params, callback) { 110 | callback(null, { 111 | id: name, 112 | description: `You have to do ${name}!` 113 | }); 114 | } 115 | }; 116 | 117 | const app = feathers() 118 | .configure(rest()) 119 | .configure(socketio()) 120 | .use('/todo', todoService); 121 | const server = app.listen(6999).on('listening', () => { 122 | const socket = io.connect('http://localhost:6999'); 123 | 124 | request('http://localhost:6999/todo/dishes', (error, response, body) => { 125 | assert.ok(response.statusCode === 200, 'Got OK status code'); 126 | const data = JSON.parse(body); 127 | assert.equal(data.description, 'You have to do dishes!'); 128 | 129 | socket.emit('todo::get', 'laundry', {}, function (error, data) { 130 | assert.equal(data.description, 'You have to do laundry!'); 131 | 132 | socket.disconnect(); 133 | server.close(done); 134 | }); 135 | }); 136 | }); 137 | }); 138 | 139 | it('Uses custom middleware. (#21)', done => { 140 | const todoService = { 141 | get(name, params) { 142 | return Promise.resolve({ 143 | id: name, 144 | description: `You have to do ${name}!`, 145 | preService: params.preService 146 | }); 147 | } 148 | }; 149 | 150 | const app = feathers() 151 | .configure(rest()) 152 | .use('/todo', function (req, res, next) { 153 | req.feathers.preService = 'pre-service middleware'; 154 | next(); 155 | }, todoService, function (req, res, next) { 156 | res.set('post-service', res.data.id); 157 | next(); 158 | }) 159 | .use('/otherTodo', todoService); 160 | 161 | const server = app.listen(6995).on('listening', () => { 162 | request('http://localhost:6995/todo/dishes', (error, response, body) => { 163 | assert.ok(response.statusCode === 200, 'Got OK status code'); 164 | const data = JSON.parse(body); 165 | assert.equal(data.preService, 'pre-service middleware', 'Pre-service middleware updated response'); 166 | assert.equal(response.headers['post-service'], 'dishes', 'Post-service middleware updated response'); 167 | 168 | request('http://localhost:6995/otherTodo/dishes', (error, response, body) => { 169 | assert.ok(response.statusCode === 200, 'Got OK status code'); 170 | const data = JSON.parse(body); 171 | assert.ok(!data.preService && !response.headers['post-service'], 'Custom middleware not run for different service.'); 172 | server.close(done); 173 | }); 174 | }); 175 | }); 176 | }); 177 | 178 | it('REST and SocketIO with SSL server (#25)', done => { 179 | // For more info on Request HTTPS settings see https://github.com/mikeal/request/issues/418 180 | // This needs to be set so that the SocektIO client can connect 181 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 182 | 183 | const todoService = { 184 | get(name, params, callback) { 185 | callback(null, { 186 | id: name, 187 | description: `You have to do ${name}!` 188 | }); 189 | } 190 | }; 191 | 192 | const app = feathers() 193 | .configure(rest()) 194 | .configure(socketio()).use('/secureTodos', todoService); 195 | 196 | const httpsServer = https.createServer({ 197 | key: fs.readFileSync(path.join(__dirname, 'resources', 'privatekey.pem')), 198 | cert: fs.readFileSync(path.join(__dirname, 'resources', 'certificate.pem')), 199 | rejectUnauthorized: false, 200 | requestCert: false 201 | }, app).listen(7889); 202 | 203 | app.setup(httpsServer); 204 | 205 | httpsServer.on('listening', function () { 206 | const socket = io('https://localhost:7889', { secure: true, port: 7889 }); 207 | 208 | request({ 209 | url: 'https://localhost:7889/secureTodos/dishes', 210 | strictSSL: false, 211 | rejectUnhauthorized: false 212 | }, function (error, response, body) { 213 | assert.ok(response.statusCode === 200, 'Got OK status code'); 214 | const data = JSON.parse(body); 215 | assert.equal(data.description, 'You have to do dishes!'); 216 | 217 | socket.emit('secureTodos::get', 'laundry', {}, function (error, data) { 218 | assert.equal(data.description, 'You have to do laundry!'); 219 | 220 | socket.disconnect(); 221 | httpsServer.close(); 222 | done(); 223 | }); 224 | }); 225 | }); 226 | }); 227 | 228 | it('Returns the value of a promise. (#41)', function (done) { 229 | let original = {}; 230 | const todoService = { 231 | get(name) { 232 | original = { 233 | id: name, 234 | q: true, 235 | description: `You have to do ${name }!` 236 | }; 237 | return Promise.resolve(original); 238 | } 239 | }; 240 | 241 | const app = feathers() 242 | .configure(rest()) 243 | .use('/todo', todoService); 244 | 245 | const server = app.listen(6880).on('listening', function () { 246 | request('http://localhost:6880/todo/dishes', (error, response, body) => { 247 | assert.ok(response.statusCode === 200, 'Got OK status code'); 248 | assert.deepEqual(original, JSON.parse(body)); 249 | server.close(done); 250 | }); 251 | }); 252 | }); 253 | 254 | it('Extend params with route params. (#76)', done => { 255 | const todoService = { 256 | get(id, params) { 257 | return Promise.resolve({ 258 | id, 259 | appId: params.appId 260 | }); 261 | } 262 | }; 263 | 264 | const app = feathers() 265 | .configure(rest()) 266 | .use('/:appId/todo', todoService); 267 | 268 | const expected = { 269 | id: 'dishes', 270 | appId: 'theApp' 271 | }; 272 | 273 | const server = app.listen(6880).on('listening', function () { 274 | request('http://localhost:6880/theApp/todo/' + expected.id, (error, response, body) => { 275 | assert.ok(response.statusCode === 200, 'Got OK status code'); 276 | assert.deepEqual(expected, JSON.parse(body)); 277 | server.close(done); 278 | }); 279 | }); 280 | }); 281 | 282 | it('Calls _setup in order to set up custom routes with higher priority. (#86)', done => { 283 | const todoService = { 284 | get(name) { 285 | return Promise.resolve({ 286 | id: name, 287 | q: true, 288 | description: `You have to do ${name}!` 289 | }); 290 | }, 291 | 292 | _setup(app, path) { 293 | app.get(`/${path}/count`, function(req, res) { 294 | res.json({ counter: 10 }); 295 | }); 296 | } 297 | }; 298 | 299 | const app = feathers() 300 | .configure(rest()) 301 | .use('/todo', todoService); 302 | 303 | const server = app.listen(8999).on('listening', function () { 304 | request('http://localhost:8999/todo/dishes', (error, response, body) => { 305 | assert.ok(response.statusCode === 200, 'Got OK status code'); 306 | const data = JSON.parse(body); 307 | assert.equal(data.description, 'You have to do dishes!'); 308 | 309 | request('http://localhost:8999/todo/count', (error, response, body) => { 310 | assert.ok(response.statusCode === 200, 'Got OK status code'); 311 | const data = JSON.parse(body); 312 | assert.equal(data.counter, 10); 313 | server.close(done); 314 | }); 315 | }); 316 | }); 317 | }); 318 | 319 | it('mixins are unique to one application', function() { 320 | const app = feathers(); 321 | app.mixins.push(function() {}); 322 | assert.equal(app.mixins.length, 4); 323 | 324 | const otherApp = feathers(); 325 | otherApp.mixins.push(function() {}); 326 | assert.equal(otherApp.mixins.length, 4); 327 | }); 328 | 329 | it('Event punching happens after normalization (#150)', done => { 330 | const todoService = { 331 | create(data) { 332 | return Promise.resolve(data); 333 | } 334 | }; 335 | 336 | const app = feathers() 337 | .configure(rest()) 338 | .use('/todo', todoService); 339 | 340 | const server = app.listen(7001).on('listening', function () { 341 | app.service('todo').create({ 342 | test: 'item' 343 | }); 344 | 345 | server.close(done); 346 | }); 347 | }); 348 | }); 349 | --------------------------------------------------------------------------------