├── .eslintignore ├── client.js ├── mocha.opts ├── .babelrc ├── .eslintrc.yml ├── .npmignore ├── .codeclimate.yml ├── .editorconfig ├── index.d.ts ├── .istanbul.yml ├── src ├── client │ ├── axios.js │ ├── request.js │ ├── jquery.js │ ├── superagent.js │ ├── fetch.js │ ├── angular.js │ ├── index.js │ └── base.js ├── index.js └── wrappers.js ├── client.d.ts ├── .gitignore ├── .github ├── issue_template.md ├── pull_request_template.md └── contributing.md ├── .travis.yml ├── LICENSE ├── test ├── client │ ├── axios.test.js │ ├── superagent.test.js │ ├── index.test.js │ ├── request.test.js │ ├── server.js │ ├── fetch.test.js │ ├── jquery.test.js │ └── angular.test.js └── index.test.js ├── README.md ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client/index'); 2 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | --compilers js:babel-core/register -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-assign", "add-module-exports"], 3 | "presets": [ "es2015" ] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: eslint-config-semistandard 3 | env: 4 | es6: true 5 | node: true 6 | mocha: true -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .travis.yml 3 | .istanbul.yml 4 | .babelrc 5 | .idea/ 6 | src/ 7 | test/ 8 | !lib/ 9 | .github/ 10 | coverage -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - javascript 7 | exclude_paths: 8 | - test/ 9 | eslint: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "**.js" 14 | #exclude_paths: 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import 'express'; 2 | 3 | declare function rest(handler?: Formatter): Function; 4 | 5 | declare interface Formatter { 6 | (request: Express.Request, response: Express.Response, next?: Function): void; 7 | } 8 | 9 | declare namespace rest { 10 | const formatter: Formatter; 11 | } 12 | 13 | export = rest; 14 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./src/ 4 | excludes: 5 | - lib/ 6 | include-all-sources: true 7 | reporting: 8 | print: summary 9 | reports: 10 | - html 11 | - text 12 | - lcov 13 | watermarks: 14 | statements: [70, 90] 15 | lines: [70, 90] 16 | functions: [70, 90] 17 | branches: [70, 90] 18 | -------------------------------------------------------------------------------- /src/client/axios.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | const config = { 6 | url: options.url, 7 | method: options.method, 8 | data: options.body, 9 | headers: Object.assign({ 10 | Accept: 'application/json' 11 | }, this.options.headers, options.headers) 12 | }; 13 | 14 | return this.connection.request(config) 15 | .then(res => res.data) 16 | .catch(error => { 17 | const response = error.response || error; 18 | 19 | throw response instanceof Error ? response : (response.data || response); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare function rest(base: string): rest.Transport; 3 | 4 | declare namespace rest { 5 | interface HandlerResult extends Function { 6 | /** 7 | * initialize service 8 | */ 9 | (): void; 10 | /** 11 | * Transport Service 12 | */ 13 | Service: any; 14 | 15 | /** 16 | * default Service 17 | */ 18 | service: any; 19 | } 20 | 21 | interface Handler { 22 | (connection, options?): () => HandlerResult; 23 | } 24 | 25 | interface Transport { 26 | jquery: Handler; 27 | superagent: Handler; 28 | request: Handler; 29 | fetch: Handler; 30 | axios: Handler; 31 | angular: Handler; 32 | } 33 | } 34 | 35 | export = rest; 36 | -------------------------------------------------------------------------------- /src/client/request.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | return new Promise((resolve, reject) => { 6 | this.connection(Object.assign({ 7 | json: true 8 | }, options), function (error, res, data) { 9 | if (error) { 10 | return reject(error); 11 | } 12 | 13 | if (!error && res.statusCode >= 400) { 14 | if (typeof data === 'string') { 15 | return reject(new Error(data)); 16 | } 17 | 18 | data.response = res; 19 | 20 | return reject(Object.assign(new Error(data.message), data)); 21 | } 22 | 23 | resolve(data); 24 | }); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # The compiled/babelified modules 31 | lib/ 32 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /src/client/jquery.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | let opts = Object.assign({ 6 | dataType: options.type || 'json' 7 | }, { 8 | headers: this.options.headers || {} 9 | }, options); 10 | 11 | if (options.body) { 12 | opts.data = JSON.stringify(options.body); 13 | opts.contentType = 'application/json'; 14 | } 15 | 16 | delete opts.type; 17 | delete opts.body; 18 | 19 | return new Promise((resolve, reject) => { 20 | this.connection.ajax(opts).then(resolve, xhr => { 21 | let error = xhr.responseText; 22 | 23 | try { 24 | error = JSON.parse(error); 25 | } catch (e) { 26 | error = new Error(xhr.responseText); 27 | } 28 | 29 | error.xhr = error.response = xhr; 30 | 31 | reject(error); 32 | }); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/superagent.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | const superagent = this.connection(options.method, options.url) 6 | .set(this.options.headers || {}) 7 | .set('Accept', 'application/json') 8 | .set(options.headers || {}) 9 | .type(options.type || 'json'); 10 | 11 | return new Promise((resolve, reject) => { 12 | superagent.set(options.headers); 13 | 14 | if (options.body) { 15 | superagent.send(options.body); 16 | } 17 | 18 | superagent.end(function (error, res) { 19 | if (error) { 20 | try { 21 | const response = error.response; 22 | error = JSON.parse(error.response.text); 23 | error.response = response; 24 | } catch (e) {} 25 | 26 | return reject(error); 27 | } 28 | 29 | resolve(res && res.body); 30 | }); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/client/fetch.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | let fetchOptions = Object.assign({}, options); 6 | 7 | fetchOptions.headers = Object.assign({ 8 | Accept: 'application/json' 9 | }, this.options.headers, fetchOptions.headers); 10 | 11 | if (options.body) { 12 | fetchOptions.body = JSON.stringify(options.body); 13 | } 14 | 15 | const fetch = this.connection; 16 | 17 | return fetch(options.url, fetchOptions) 18 | .then(this.checkStatus) 19 | .then(response => { 20 | if (response.status === 204) { 21 | return null; 22 | } 23 | 24 | return response.json(); 25 | }); 26 | } 27 | 28 | checkStatus (response) { 29 | if (response.ok) { 30 | return response; 31 | } 32 | 33 | return response.json().then(error => { 34 | error.response = response; 35 | throw error; 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/angular.js: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default class Service extends Base { 4 | request (options) { 5 | const http = this.connection; 6 | const Headers = this.options.Headers; 7 | 8 | if (!http || !Headers) { 9 | throw new Error(`Please pass angular's 'http' (instance) and and object with 'Headers' (class) to feathers-rest`); 10 | } 11 | 12 | const url = options.url; 13 | const requestOptions = { 14 | method: options.method, 15 | body: options.body, 16 | headers: new Headers( 17 | Object.assign( 18 | { Accept: 'application/json' }, 19 | this.options.headers, 20 | options.headers 21 | ) 22 | ) 23 | }; 24 | 25 | return new Promise((resolve, reject) => { 26 | http.request(url, requestOptions).subscribe(resolve, reject); 27 | }) 28 | .then(res => res.json()) 29 | .catch(error => { 30 | const response = error.response || error; 31 | 32 | throw response instanceof Error ? response : (response.json() || response); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - node 5 | - '6' 6 | - '4' 7 | addons: 8 | code_climate: 9 | repo_token: 81c775b1dd23640f4f3076537995bb1b0c13a6bd5e9a476bbc5b0f4e6997f576 10 | before_script: 11 | - npm install -g codeclimate-test-reporter 12 | after_script: 13 | - codeclimate-test-reporter < coverage/lcov.info 14 | notifications: 15 | email: false 16 | slack: 17 | rooms: 18 | secure: BlkMY8NVmYClqp9jW3EVeBFRsrhNCodDopRp66C885vXbR0prvgAmb6XylaVcik7Y/P+zbbfHkO8VI9HiGmNQgWQOh4hrXIYicQcuXwh0AUfjU9vNaxPq3xguPWkehKql5D2jvBxro8IjVM+yU9GSbpkZ/KZh/o8GniJY6jI6zdS3jIW9vHcuoKKwoH0afrpt8GR1NL7jO8dK5j4hH/0yf9y0Ay2CZw49Gjc/Pj4nZke41ZRbfBzWU/fVCm0PjcyxqUwWrOOoZicWQC9an4Pk8WuESZ7EL2SLSxJ67EZduc5euGf+xxaRwz2BdZ3h5+31cuRYH0Q6LeKcPvyjVjUKoD1vxGg6m/yPZvItjxqu5PJ+/vp4L0GibJDg5qX0JqkaRsT1kc+7oEfW+KsUlW0+R028n1m019Vb66f7mFMsFe8SebRDiNYlQu6CjnYbBXR0sTh6QthGyG7uBktVru+HvsYOpnKRa8ut8fvLY1mrq6WxJZ26Cv2ybB7TPcXZc5mnXe8uddotrbrtjLoAFBK37OAFFWSesGh2T2WJU2Eprugne/iZHNpzLzlP0xIgc3sIGi1jhaKCcgnVFLXCqPp3h6lmM2so5JZumO3CoHI4nd9tdDa+vDG5Zu0YhrTDvxwFiE/poGA6/uguI0td4cFx/8CAB5mIjDKnjnBF7K92mc= 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import jQuery from './jquery'; 2 | import Superagent from './superagent'; 3 | import Request from './request'; 4 | import Fetch from './fetch'; 5 | import Axios from './axios'; 6 | import Angular from './angular'; 7 | 8 | const transports = { 9 | jquery: jQuery, 10 | superagent: Superagent, 11 | request: Request, 12 | fetch: Fetch, 13 | axios: Axios, 14 | angular: Angular 15 | }; 16 | 17 | export default function (base = '') { 18 | const result = {}; 19 | 20 | Object.keys(transports).forEach(key => { 21 | const Service = transports[key]; 22 | 23 | result[key] = function (connection, options = {}) { 24 | if (!connection) { 25 | throw new Error(`${key} has to be provided to feathers-rest`); 26 | } 27 | 28 | const defaultService = function (name) { 29 | return new Service({ base, name, connection, options }); 30 | }; 31 | 32 | const initialize = function () { 33 | if (typeof this.defaultService === 'function') { 34 | throw new Error('Only one default client provider can be configured'); 35 | } 36 | 37 | this.rest = connection; 38 | this.defaultService = defaultService; 39 | }; 40 | 41 | initialize.Service = Service; 42 | initialize.service = defaultService; 43 | 44 | return initialize; 45 | }; 46 | }); 47 | 48 | return result; 49 | } 50 | -------------------------------------------------------------------------------- /test/client/axios.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import axios from 'axios'; 3 | import feathers from 'feathers/client'; 4 | import baseTests from 'feathers-commons/lib/test/client'; 5 | import errors from 'feathers-errors'; 6 | 7 | import server from './server'; 8 | import rest from '../../src/client'; 9 | 10 | describe('Axios REST connector', function () { 11 | const url = 'http://localhost:8889'; 12 | const setup = rest(url).axios(axios); 13 | const app = feathers().configure(setup); 14 | const service = app.service('todos'); 15 | 16 | before(function (done) { 17 | this.server = server().listen(8889, done); 18 | }); 19 | 20 | after(function (done) { 21 | this.server.close(done); 22 | }); 23 | 24 | baseTests(service); 25 | 26 | it('supports custom headers', () => { 27 | let headers = { 28 | 'Authorization': 'let-me-in' 29 | }; 30 | 31 | return service.get(0, { headers }).then(todo => 32 | assert.deepEqual(todo, { 33 | id: 0, 34 | text: 'some todo', 35 | complete: false, 36 | query: {} 37 | }) 38 | ); 39 | }); 40 | 41 | it('can initialize a client instance', () => { 42 | const init = rest(url).axios(axios); 43 | const todos = init.service('todos'); 44 | 45 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 46 | 47 | return todos.find({}).then(todos => 48 | assert.deepEqual(todos, [ 49 | { 50 | text: 'some todo', 51 | complete: false, 52 | id: 0 53 | } 54 | ]) 55 | ); 56 | }); 57 | 58 | it('supports nested arrays in queries', () => { 59 | const query = { test: { $in: [ 0, 1, 2 ] } }; 60 | 61 | return service.get(0, { query }).then(data => 62 | assert.deepEqual(data.query, query) 63 | ); 64 | }); 65 | 66 | it('remove many', () => { 67 | return service.remove(null).then(todo => { 68 | assert.equal(todo.id, null); 69 | assert.equal(todo.text, 'deleted many'); 70 | }); 71 | }); 72 | 73 | it('converts feathers errors (#50)', () => { 74 | return service.get(0, { query: { feathersError: true } }) 75 | .catch(error => { 76 | assert.ok(error instanceof errors.NotAcceptable); 77 | assert.equal(error.message, 'This is a Feathers error'); 78 | assert.equal(error.code, 406); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/client/superagent.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import superagent from 'superagent'; 3 | import feathers from 'feathers/client'; 4 | import baseTests from 'feathers-commons/lib/test/client'; 5 | import errors from 'feathers-errors'; 6 | 7 | import server from './server'; 8 | import rest from '../../src/client'; 9 | 10 | describe('Superagent REST connector', function () { 11 | const url = 'http://localhost:8889'; 12 | const setup = rest(url).superagent(superagent); 13 | const app = feathers().configure(setup); 14 | const service = app.service('todos'); 15 | 16 | before(function (done) { 17 | this.server = server().listen(8889, done); 18 | }); 19 | 20 | after(function (done) { 21 | this.server.close(done); 22 | }); 23 | 24 | baseTests(service); 25 | 26 | it('supports custom headers', () => { 27 | let headers = { 28 | 'Authorization': 'let-me-in' 29 | }; 30 | 31 | return service.get(0, { headers }).then(todo => 32 | assert.deepEqual(todo, { 33 | id: 0, 34 | text: 'some todo', 35 | complete: false, 36 | query: {} 37 | }) 38 | ); 39 | }); 40 | 41 | it('can initialize a client instance', () => { 42 | const init = rest(url).superagent(superagent); 43 | const todos = init.service('todos'); 44 | 45 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 46 | 47 | return todos.find({}).then(todos => 48 | assert.deepEqual(todos, [ 49 | { 50 | text: 'some todo', 51 | complete: false, 52 | id: 0 53 | } 54 | ]) 55 | ); 56 | }); 57 | 58 | it('supports nested arrays in queries', () => { 59 | const query = { test: { $in: [ 0, 1, 2 ] } }; 60 | 61 | return service.get(0, { query }).then(data => 62 | assert.deepEqual(data.query, query) 63 | ); 64 | }); 65 | 66 | it('remove many', () => { 67 | return service.remove(null).then(todo => { 68 | assert.equal(todo.id, null); 69 | assert.equal(todo.text, 'deleted many'); 70 | }); 71 | }); 72 | 73 | it('converts feathers errors (#50)', () => { 74 | return service.get(0, { query: { feathersError: true } }) 75 | .catch(error => { 76 | assert.ok(error instanceof errors.NotAcceptable); 77 | assert.equal(error.message, 'This is a Feathers error'); 78 | assert.equal(error.code, 406); 79 | assert.ok(error.response); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/client/index.test.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import feathers from 'feathers/client'; 3 | import rest from '../../src/client'; 4 | import assert from 'assert'; 5 | 6 | const init = require('../../client'); 7 | 8 | describe('REST client tests', function () { 9 | it('is built correctly', () => { 10 | const transports = init(); 11 | 12 | assert.equal(typeof init, 'function'); 13 | assert.equal(typeof transports.jquery, 'function'); 14 | assert.equal(typeof transports.request, 'function'); 15 | assert.equal(typeof transports.superagent, 'function'); 16 | assert.equal(typeof transports.fetch, 'function'); 17 | }); 18 | 19 | it('throw errors when no connection is provided', () => { 20 | const transports = init(); 21 | 22 | try { 23 | transports.fetch(); 24 | } catch (e) { 25 | assert.equal(e.message, 'fetch has to be provided to feathers-rest'); 26 | } 27 | }); 28 | 29 | it('app has the rest attribute', () => { 30 | const app = feathers(); 31 | 32 | app.configure(rest('http://localhost:8889').fetch(fetch)); 33 | 34 | assert.ok(app.rest); 35 | }); 36 | 37 | it('throws an error when configured twice', () => { 38 | const app = feathers(); 39 | 40 | app.configure(rest('http://localhost:8889').fetch(fetch)); 41 | 42 | try { 43 | app.configure(rest('http://localhost:8889').fetch(fetch)); 44 | assert.ok(false, 'Should never get here'); 45 | } catch (e) { 46 | assert.equal(e.message, 'Only one default client provider can be configured'); 47 | } 48 | }); 49 | 50 | it('errors when id property for get, patch, update or remove is undefined', () => { 51 | const app = feathers().configure(rest('http://localhost:8889') 52 | .fetch(fetch)); 53 | 54 | const service = app.service('todos'); 55 | 56 | return service.get().catch(error => { 57 | assert.equal(error.message, `id for 'get' can not be undefined`); 58 | 59 | return service.remove(); 60 | }).catch(error => { 61 | assert.equal(error.message, `id for 'remove' can not be undefined, only 'null' when removing multiple entries`); 62 | 63 | return service.update(undefined, {}); 64 | }).catch(error => { 65 | assert.equal(error.message, `id for 'update' can not be undefined, only 'null' when updating multiple entries`); 66 | 67 | return service.patch(undefined, {}); 68 | }).catch(error => { 69 | assert.equal(error.message, `id for 'patch' can not be undefined, only 'null' when updating multiple entries`); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/client/request.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import request from 'request'; 3 | import feathers from 'feathers/client'; 4 | import baseTests from 'feathers-commons/lib/test/client'; 5 | import errors from 'feathers-errors'; 6 | 7 | import server from './server'; 8 | import rest from '../../src/client'; 9 | 10 | describe('node-request REST connector', function () { 11 | const url = 'http://localhost:6777'; 12 | const setup = rest(url).request(request); 13 | const app = feathers().configure(setup); 14 | const service = app.service('todos'); 15 | 16 | before(function (done) { 17 | this.server = server().listen(6777, done); 18 | }); 19 | 20 | after(function (done) { 21 | this.server.close(done); 22 | }); 23 | 24 | baseTests(service); 25 | 26 | it('supports custom headers', () => { 27 | let headers = { 28 | 'Authorization': 'let-me-in' 29 | }; 30 | 31 | return service.get(0, { headers }).then(todo => 32 | assert.deepEqual(todo, { 33 | id: 0, 34 | text: 'some todo', 35 | complete: false, 36 | query: {} 37 | }) 38 | ); 39 | }); 40 | 41 | it('can initialize a client instance', () => { 42 | const init = rest(url).request(request); 43 | const todos = init.service('todos'); 44 | 45 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 46 | 47 | return todos.find({}).then(todos => 48 | assert.deepEqual(todos, [ 49 | { 50 | text: 'some todo', 51 | complete: false, 52 | id: 0 53 | } 54 | ]) 55 | ); 56 | }); 57 | 58 | it('supports nested arrays in queries', () => { 59 | const query = { test: { $in: [ 0, 1, 2 ] } }; 60 | 61 | return service.get(0, { query }).then(data => 62 | assert.deepEqual(data.query, query) 63 | ); 64 | }); 65 | 66 | it('converts errors properly', () => { 67 | return service.get(1, { query: { error: true } }).catch(e => 68 | assert.equal(e.message, 'Something went wrong') 69 | ); 70 | }); 71 | 72 | it('remove many', () => { 73 | return service.remove(null).then(todo => { 74 | assert.equal(todo.id, null); 75 | assert.equal(todo.text, 'deleted many'); 76 | }); 77 | }); 78 | 79 | it('converts feathers errors (#50)', () => { 80 | return service.get(0, { query: { feathersError: true } }).catch(error => { 81 | assert.ok(error instanceof errors.NotAcceptable); 82 | assert.equal(error.message, 'This is a Feathers error'); 83 | assert.equal(error.code, 406); 84 | assert.deepEqual(error.data, { data: true }); 85 | assert.ok(error.response); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/client/server.js: -------------------------------------------------------------------------------- 1 | import feathers from 'feathers'; 2 | import bodyParser from 'body-parser'; 3 | import memory from 'feathers-memory'; 4 | import errors from 'feathers-errors'; 5 | import rest from '../../src'; 6 | 7 | // eslint-disable-next-line no-extend-native 8 | Object.defineProperty(Error.prototype, 'toJSON', { 9 | value: function () { 10 | var alt = {}; 11 | 12 | Object.getOwnPropertyNames(this).forEach(function (key) { 13 | alt[key] = this[key]; 14 | }, this); 15 | 16 | return alt; 17 | }, 18 | configurable: true, 19 | writable: true 20 | }); 21 | 22 | let errorHandler = function (error, req, res, next) { 23 | const code = !isNaN(parseInt(error.code, 10)) ? parseInt(error.code, 10) : 500; 24 | res.status(code); 25 | 26 | res.format({ 27 | 'application/json': function () { 28 | let output = Object.assign({}, error.toJSON()); 29 | res.json(output); 30 | } 31 | }); 32 | }; 33 | 34 | module.exports = function (configurer) { 35 | // Create an in-memory CRUD service for our Todos 36 | var todoService = memory().extend({ 37 | get (id, params) { 38 | if (params.query.error) { 39 | throw new Error('Something went wrong'); 40 | } 41 | 42 | if (params.query.feathersError) { 43 | throw new errors.NotAcceptable('This is a Feathers error', { data: true }); 44 | } 45 | 46 | return this._super(id, params) 47 | .then(data => Object.assign({ query: params.query }, data)); 48 | }, 49 | 50 | remove (id, params) { 51 | if (id === null) { 52 | return Promise.resolve({ 53 | id, text: 'deleted many' 54 | }); 55 | } 56 | 57 | if (params.query.noContent) { 58 | return Promise.resolve(); 59 | } 60 | 61 | return this._super.apply(this, arguments); 62 | } 63 | }); 64 | 65 | var app = feathers() 66 | .configure(rest(function formatter (req, res, next) { 67 | if (!res.data) { 68 | next(); 69 | } 70 | 71 | res.format({ 72 | html () { 73 | res.end('

This is HTML content. You should not see it.

'); 74 | }, 75 | 76 | json () { 77 | res.json(res.data); 78 | } 79 | }); 80 | })) 81 | // Parse HTTP bodies 82 | .use(bodyParser.json()) 83 | .use(bodyParser.urlencoded({ extended: true })) 84 | // Host the current directory (for index.html) 85 | .use(feathers.static(__dirname)) 86 | // Host our Todos service on the /todos path 87 | .use('/todos', todoService) 88 | .use(errorHandler); 89 | 90 | if (typeof configurer === 'function') { 91 | configurer.call(app); 92 | } 93 | 94 | app.service('todos').create({ text: 'some todo', complete: false }, {}, function () {}); 95 | 96 | return app; 97 | }; 98 | -------------------------------------------------------------------------------- /test/client/fetch.test.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import assert from 'assert'; 3 | import feathers from 'feathers/client'; 4 | import errors from 'feathers-errors'; 5 | import baseTests from 'feathers-commons/lib/test/client'; 6 | 7 | import server from './server'; 8 | import rest from '../../src/client'; 9 | 10 | describe('fetch REST connector', function () { 11 | const url = 'http://localhost:8889'; 12 | const setup = rest(url).fetch(fetch); 13 | const app = feathers().configure(setup); 14 | const service = app.service('todos'); 15 | 16 | before(function (done) { 17 | this.server = server().listen(8889, done); 18 | }); 19 | 20 | after(function (done) { 21 | this.server.close(done); 22 | }); 23 | 24 | baseTests(service); 25 | 26 | it('supports custom headers', () => { 27 | let headers = { 28 | 'Authorization': 'let-me-in' 29 | }; 30 | 31 | return service.get(0, { headers }).then(todo => 32 | assert.deepEqual(todo, { 33 | id: 0, 34 | text: 'some todo', 35 | complete: false, 36 | query: {} 37 | }) 38 | ); 39 | }); 40 | 41 | it('handles errors properly', () => { 42 | return service.get(-1, {}).catch(error => 43 | assert.equal(error.code, 404) 44 | ); 45 | }); 46 | 47 | it('supports nested arrays in queries', () => { 48 | const query = { test: { $in: [ 0, 1, 2 ] } }; 49 | 50 | return service.get(0, { query }).then(data => 51 | assert.deepEqual(data.query, query) 52 | ); 53 | }); 54 | 55 | it('can initialize a client instance', () => { 56 | const init = rest(url).fetch(fetch); 57 | const todos = init.service('todos'); 58 | 59 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 60 | 61 | return todos.find({}).then(todos => 62 | assert.deepEqual(todos, [ 63 | { 64 | text: 'some todo', 65 | complete: false, 66 | id: 0 67 | } 68 | ]) 69 | ); 70 | }); 71 | 72 | it('remove many', () => { 73 | return service.remove(null).then(todo => { 74 | assert.equal(todo.id, null); 75 | assert.equal(todo.text, 'deleted many'); 76 | }); 77 | }); 78 | 79 | it('converts feathers errors (#50)', () => { 80 | return service.get(0, { query: { feathersError: true } }).catch(error => { 81 | assert.ok(error instanceof errors.NotAcceptable); 82 | assert.equal(error.message, 'This is a Feathers error'); 83 | assert.equal(error.code, 406); 84 | assert.deepEqual(error.data, { data: true }); 85 | assert.ok(error.response); 86 | }); 87 | }); 88 | 89 | it('returns null for 204 responses', () => { 90 | return service.remove(0, { query: { noContent: true } }) 91 | .then(response => 92 | assert.strictEqual(response, null) 93 | ); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/client/jquery.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import feathers from 'feathers'; 3 | import jsdom from 'jsdom'; 4 | import baseTests from 'feathers-commons/lib/test/client'; 5 | import errors from 'feathers-errors'; 6 | 7 | import server from './server'; 8 | import rest from '../../src/client'; 9 | 10 | describe('jQuery REST connector', function () { 11 | const url = 'http://localhost:7676'; 12 | const setup = rest(url).jquery({}); 13 | const app = feathers().configure(setup); 14 | const service = app.service('todos'); 15 | 16 | before(function (done) { 17 | this.server = server().listen(7676, function () { 18 | jsdom.env( 19 | '', 20 | [ 'http://code.jquery.com/jquery-2.1.4.js' ], 21 | function (err, window) { // eslint-disable-line handle-callback-err 22 | window.jQuery.support.cors = true; 23 | service.connection = window.jQuery; 24 | done(); 25 | } 26 | ); 27 | }); 28 | }); 29 | 30 | after(function () { 31 | this.server.close(); 32 | }); 33 | 34 | baseTests(service); 35 | 36 | it('supports custom headers', () => { 37 | let headers = { 38 | 'Authorization': 'let-me-in' 39 | }; 40 | 41 | return service.get(0, { headers }).then(todo => 42 | assert.deepEqual(todo, { 43 | id: 0, 44 | text: 'some todo', 45 | complete: false, 46 | query: {} 47 | }) 48 | ); 49 | }); 50 | 51 | it('can initialize a client instance', () => { 52 | const init = rest(url).jquery(service.connection); 53 | const todos = init.service('todos'); 54 | 55 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 56 | 57 | return todos.find({}).then(todos => 58 | assert.deepEqual(todos, [ 59 | { 60 | text: 'some todo', 61 | complete: false, 62 | id: 0 63 | } 64 | ]) 65 | ); 66 | }); 67 | 68 | it('supports nested arrays in queries', () => { 69 | const query = { test: { $in: [ 0, 1, 2 ] } }; 70 | 71 | return service.get(0, { query }).then(data => 72 | assert.deepEqual(data.query, query) 73 | ); 74 | }); 75 | 76 | it('remove many', () => { 77 | return service.remove(null).then(todo => { 78 | assert.equal(todo.id, null); 79 | assert.equal(todo.text, 'deleted many'); 80 | }); 81 | }); 82 | 83 | it('converts feathers errors (#50)', () => { 84 | return service.get(0, { query: { feathersError: true } }) 85 | .catch(error => { 86 | assert.ok(error instanceof errors.NotAcceptable); 87 | assert.equal(error.message, 'This is a Feathers error'); 88 | assert.equal(error.code, 406); 89 | assert.deepEqual(error.data, { data: true }); 90 | assert.ok(error.response); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | import wrappers from './wrappers'; 3 | 4 | const debug = makeDebug('feathers-rest'); 5 | 6 | function formatter (req, res, next) { 7 | if (res.data === undefined) { 8 | return next(); 9 | } 10 | 11 | res.format({ 12 | 'application/json': function () { 13 | res.json(res.data); 14 | } 15 | }); 16 | } 17 | 18 | export default function rest (handler = formatter) { 19 | return function () { 20 | const app = this; 21 | 22 | if (app.version && app.version >= '3.0.0') { 23 | throw new Error(`feathers-rest is not compatible with Feathers v${app.version}. Use the Express framework bindings and REST adapter at @feathersjs/express instead.`); 24 | } 25 | 26 | app.use(function (req, res, next) { 27 | req.feathers = { provider: 'rest' }; 28 | next(); 29 | }); 30 | 31 | app.rest = wrappers; 32 | 33 | // Register the REST provider 34 | app.providers.push(function (path, service, options) { 35 | const uri = path.indexOf('/') === 0 ? path : `/${path}`; 36 | const baseRoute = app.route(uri); 37 | const idRoute = app.route(`${uri}/:__feathersId`); 38 | 39 | let middleware = (options || {}).middleware || {}; 40 | let before = middleware.before || []; 41 | let after = middleware.after || []; 42 | 43 | if (typeof handler === 'function') { 44 | after = after.concat(handler); 45 | } 46 | 47 | debug(`Adding REST provider for service \`${path}\` at base route \`${uri}\``); 48 | 49 | // GET / -> service.find(cb, params) 50 | baseRoute.get.apply(baseRoute, before.concat(app.rest.find(service), after)); 51 | // POST / -> service.create(data, params, cb) 52 | baseRoute.post.apply(baseRoute, before.concat(app.rest.create(service), after)); 53 | // PATCH / -> service.patch(null, data, params) 54 | baseRoute.patch.apply(baseRoute, before.concat(app.rest.patch(service), after)); 55 | // PUT / -> service.update(null, data, params) 56 | baseRoute.put.apply(baseRoute, before.concat(app.rest.update(service), after)); 57 | // DELETE / -> service.remove(null, params) 58 | baseRoute.delete.apply(baseRoute, before.concat(app.rest.remove(service), after)); 59 | 60 | // GET /:id -> service.get(id, params, cb) 61 | idRoute.get.apply(idRoute, before.concat(app.rest.get(service), after)); 62 | // PUT /:id -> service.update(id, data, params, cb) 63 | idRoute.put.apply(idRoute, before.concat(app.rest.update(service), after)); 64 | // PATCH /:id -> service.patch(id, data, params, callback) 65 | idRoute.patch.apply(idRoute, before.concat(app.rest.patch(service), after)); 66 | // DELETE /:id -> service.remove(id, params, cb) 67 | idRoute.delete.apply(idRoute, before.concat(app.rest.remove(service), after)); 68 | }); 69 | }; 70 | } 71 | 72 | rest.formatter = formatter; 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-rest 2 | 3 | > _Important:_ For Feathers v3.0.0 and later, `feathers-rest` is has been replaced by framework specific adapters. Use [`@feathersjs/express`](https://github.com/feathersjs/express) and its REST adapter instead of this module. 4 | 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs-ecosystem/feathers-rest.svg)](https://greenkeeper.io/) 6 | 7 | [![Build Status](https://travis-ci.org/feathersjs-ecosystem/feathers-rest.png?branch=master)](https://travis-ci.org/feathersjs-ecosystem/feathers-rest) 8 | [![Dependency Status](https://img.shields.io/david/feathersjs-ecosystem/feathers-rest.svg?style=flat-square)](https://david-dm.org/feathersjs-ecosystem/feathers-rest) 9 | [![Download Status](https://img.shields.io/npm/dm/feathers-rest.svg?style=flat-square)](https://www.npmjs.com/package/feathers-rest) 10 | 11 | > The Feathers REST API provider 12 | 13 | ## About 14 | 15 | This provider exposes [Feathers](http://feathersjs.com) services through a RESTful API using [Express](http://expressjs.com) that can be used with Feathers 1.x and 2.x as well as client support for Fetch, jQuery, Request, Superagent, axios and angular2+'s HTTP Service. 16 | 17 | __Note:__ For the full API documentation go to [https://docs.feathersjs.com/api/rest.html](https://docs.feathersjs.com/api/rest.html). 18 | 19 | ## Quick example 20 | 21 | ```js 22 | import feathers from 'feathers'; 23 | import bodyParser from 'body-parser'; 24 | import rest from 'feathers-rest'; 25 | 26 | const app = feathers() 27 | .configure(rest()) 28 | .use(bodyParser.json()) 29 | .use(bodyParser.urlencoded({ extended: true })) 30 | .use(function(req, res, next) { 31 | req.feathers.data = 'Hello world'; 32 | next(); 33 | }); 34 | 35 | app.use('/:app/todos', { 36 | get: function(id, params) { 37 | console.log(params.data); // -> 'Hello world' 38 | console.log(params.app); // will be `my` for GET /my/todos/dishes 39 | 40 | return Promise.resolve({ 41 | id, 42 | params, 43 | description: `You have to do ${name}!` 44 | }); 45 | } 46 | }); 47 | ``` 48 | 49 | ## Client use 50 | 51 | ```js 52 | import feathers from 'feathers/client'; 53 | import rest from 'feathers-rest/client'; 54 | 55 | import jQuery from 'jquery'; 56 | import request from 'request'; 57 | import superagent from 'superagent'; 58 | import axios from 'axios'; 59 | import {Http, Headers} from '@angular/http'; 60 | 61 | 62 | const app = feathers() 63 | .configure(rest('http://baseUrl').jquery(jQuery)) 64 | // or 65 | .configure(rest('http://baseUrl').fetch(window.fetch.bind(window))) 66 | // or 67 | .configure(rest('http://baseUrl').request(request)) 68 | // or 69 | .configure(rest('http://baseUrl').superagent(superagent)) 70 | // or 71 | .configure(rest('http://baseUrl').axios(axios)) 72 | // or (using injected Http instance) 73 | .configure(rest('http://baseUrl').angular(http, { Headers })) 74 | ``` 75 | 76 | ## License 77 | 78 | Copyright (c) 2015 79 | 80 | Licensed under the [MIT license](LICENSE). 81 | -------------------------------------------------------------------------------- /test/client/angular.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import feathers from 'feathers/client'; 3 | import baseTests from 'feathers-commons/lib/test/client'; 4 | import errors from 'feathers-errors'; 5 | 6 | import server from './server'; 7 | import rest from '../../src/client'; 8 | 9 | import { 10 | Http, 11 | Headers, 12 | BaseRequestOptions, 13 | XHRBackend 14 | } from '@angular/http'; 15 | 16 | const xhr2 = require('xhr2'); 17 | 18 | function createAngularHTTP () { 19 | const XSRFStrategyShim = { configureRequest: () => {} }; 20 | const serverXHR = { build: () => new xhr2.XMLHttpRequest() }; 21 | 22 | return new Http( 23 | new XHRBackend( 24 | serverXHR, 25 | new BaseRequestOptions(), 26 | XSRFStrategyShim 27 | ), 28 | new BaseRequestOptions() 29 | ); 30 | } 31 | 32 | describe('angular REST connector', function () { 33 | const angularHttp = createAngularHTTP(); 34 | 35 | const url = 'http://localhost:8889'; 36 | const setup = rest(url).angular(angularHttp, {Headers}); 37 | const app = feathers().configure(setup); 38 | const service = app.service('todos'); 39 | 40 | before(function (done) { 41 | this.server = server().listen(8889, done); 42 | }); 43 | 44 | after(function (done) { 45 | this.server.close(done); 46 | }); 47 | 48 | baseTests(service); 49 | 50 | it('supports custom headers', () => { 51 | let headers = { 52 | 'Authorization': 'let-me-in' 53 | }; 54 | 55 | return service.get(0, {headers}).then(todo => 56 | assert.deepEqual(todo, { 57 | id: 0, 58 | text: 'some todo', 59 | complete: false, 60 | query: {} 61 | }) 62 | ); 63 | }); 64 | 65 | it('can initialize a client instance', () => { 66 | const init = rest(url).angular(angularHttp, {Headers}); 67 | const todos = init.service('todos'); 68 | 69 | assert.ok(todos instanceof init.Service, 'Returned service is a client'); 70 | 71 | return todos.find({}).then(todos => 72 | assert.deepEqual(todos, [ 73 | { 74 | text: 'some todo', 75 | complete: false, 76 | id: 0 77 | } 78 | ]) 79 | ); 80 | }); 81 | 82 | it('supports nested arrays in queries', () => { 83 | const query = {test: {$in: [0, 1, 2]}}; 84 | 85 | return service.get(0, {query}).then(data => 86 | assert.deepEqual(data.query, query) 87 | ); 88 | }); 89 | 90 | it('remove many', () => { 91 | return service.remove(null).then(todo => { 92 | assert.equal(todo.id, null); 93 | assert.equal(todo.text, 'deleted many'); 94 | }); 95 | }); 96 | 97 | it('converts feathers errors (#50)', () => { 98 | return service.get(0, {query: {feathersError: true}}) 99 | .catch(error => { 100 | assert.ok(error instanceof errors.NotAcceptable); 101 | assert.equal(error.message, 'This is a Feathers error'); 102 | assert.equal(error.code, 406); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/client/base.js: -------------------------------------------------------------------------------- 1 | import query from 'qs'; 2 | import { stripSlashes } from 'feathers-commons'; 3 | import { convert } from 'feathers-errors'; 4 | 5 | function toError (error) { 6 | throw convert(error); 7 | } 8 | 9 | export default class Base { 10 | constructor (settings) { 11 | this.name = stripSlashes(settings.name); 12 | this.options = settings.options; 13 | this.connection = settings.connection; 14 | this.base = `${settings.base}/${this.name}`; 15 | } 16 | 17 | makeUrl (params, id) { 18 | params = params || {}; 19 | let url = this.base; 20 | 21 | if (typeof id !== 'undefined' && id !== null) { 22 | url += `/${id}`; 23 | } 24 | 25 | if (Object.keys(params).length !== 0) { 26 | const queryString = query.stringify(params); 27 | 28 | url += `?${queryString}`; 29 | } 30 | 31 | return url; 32 | } 33 | 34 | find (params = {}) { 35 | return this.request({ 36 | url: this.makeUrl(params.query), 37 | method: 'GET', 38 | headers: Object.assign({}, params.headers) 39 | }).catch(toError); 40 | } 41 | 42 | get (id, params = {}) { 43 | if (typeof id === 'undefined') { 44 | return Promise.reject(new Error(`id for 'get' can not be undefined`)); 45 | } 46 | 47 | return this.request({ 48 | url: this.makeUrl(params.query, id), 49 | method: 'GET', 50 | headers: Object.assign({}, params.headers) 51 | }).catch(toError); 52 | } 53 | 54 | create (body, params = {}) { 55 | return this.request({ 56 | url: this.makeUrl(params.query), 57 | body, 58 | method: 'POST', 59 | headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers) 60 | }).catch(toError); 61 | } 62 | 63 | update (id, body, params = {}) { 64 | if (typeof id === 'undefined') { 65 | return Promise.reject(new Error(`id for 'update' can not be undefined, only 'null' when updating multiple entries`)); 66 | } 67 | 68 | return this.request({ 69 | url: this.makeUrl(params.query, id), 70 | body, 71 | method: 'PUT', 72 | headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers) 73 | }).catch(toError); 74 | } 75 | 76 | patch (id, body, params = {}) { 77 | if (typeof id === 'undefined') { 78 | return Promise.reject(new Error(`id for 'patch' can not be undefined, only 'null' when updating multiple entries`)); 79 | } 80 | 81 | return this.request({ 82 | url: this.makeUrl(params.query, id), 83 | body, 84 | method: 'PATCH', 85 | headers: Object.assign({ 'Content-Type': 'application/json' }, params.headers) 86 | }).catch(toError); 87 | } 88 | 89 | remove (id, params = {}) { 90 | if (typeof id === 'undefined') { 91 | return Promise.reject(new Error(`id for 'remove' can not be undefined, only 'null' when removing multiple entries`)); 92 | } 93 | 94 | return this.request({ 95 | url: this.makeUrl(params.query, id), 96 | method: 'DELETE', 97 | headers: Object.assign({}, params.headers) 98 | }).catch(toError); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-rest", 3 | "description": "The Feathers REST API provider", 4 | "version": "1.8.1", 5 | "homepage": "https://github.com/feathersjs-ecosystem/feathers-rest", 6 | "main": "lib/", 7 | "types": [ 8 | "./index.d.ts", 9 | "./client.d.ts" 10 | ], 11 | "keywords": [ 12 | "feathers", 13 | "feathers-plugin" 14 | ], 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/feathersjs-ecosystem/feathers-rest.git" 19 | }, 20 | "author": { 21 | "name": "Feathers contributors", 22 | "email": "hello@feathersjs.com", 23 | "url": "https://feathersjs.com" 24 | }, 25 | "contributors": [], 26 | "bugs": { 27 | "url": "https://github.com/feathersjs-ecosystem/feathers-rest/issues" 28 | }, 29 | "engines": { 30 | "node": ">= 4" 31 | }, 32 | "scripts": { 33 | "prepublish": "npm run compile", 34 | "publish": "git push origin --tags && npm run changelog && git push origin", 35 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 36 | "release:patch": "npm version patch && npm publish", 37 | "release:minor": "npm version minor && npm publish", 38 | "release:major": "npm version major && npm publish", 39 | "compile": "rimraf -rf lib/ && babel -d lib/ src/", 40 | "watch": "babel --watch -d lib/ src/", 41 | "lint": "eslint-if-supported semistandard --fix", 42 | "mocha": "mocha --opts mocha.opts", 43 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --opts mocha.opts", 44 | "test": "npm run compile && npm run lint && npm run coverage" 45 | }, 46 | "semistandard": { 47 | "env": [ 48 | "mocha" 49 | ], 50 | "ignore": [ 51 | "/lib" 52 | ] 53 | }, 54 | "directories": { 55 | "lib": "lib" 56 | }, 57 | "browser": { 58 | "./lib/index": "./lib/client/index" 59 | }, 60 | "greenkeeper": { 61 | "ignore": "jsdom" 62 | }, 63 | "dependencies": { 64 | "debug": "^3.1.0", 65 | "feathers-commons": "^0.8.0", 66 | "feathers-errors": "^2.0.1", 67 | "qs": "^6.4.0" 68 | }, 69 | "greenkeeper": { 70 | "ignore": [ 71 | "@angular/common", 72 | "@angular/core", 73 | "@angular/http", 74 | "@angular/platform-browser" 75 | ] 76 | }, 77 | "devDependencies": { 78 | "@angular/common": "^4.2.5", 79 | "@angular/core": "^4.2.5", 80 | "@angular/http": "^4.2.5", 81 | "@angular/platform-browser": "^4.2.5", 82 | "@feathersjs/feathers": "^3.0.0-pre.3", 83 | "axios": "^0.17.0", 84 | "babel-cli": "^6.3.17", 85 | "babel-core": "^6.3.26", 86 | "babel-plugin-add-module-exports": "^0.2.0", 87 | "babel-plugin-transform-object-assign": "^6.3.13", 88 | "babel-preset-es2015": "^6.3.13", 89 | "body-parser": "^1.14.2", 90 | "eslint-if-supported": "^1.0.1", 91 | "feathers": "^2.0.0-pre.1", 92 | "feathers-memory": "^1.0.0", 93 | "istanbul": "^1.1.0-alpha.1", 94 | "jsdom": "8.1.0", 95 | "mocha": "^4.0.0", 96 | "node-fetch": "^1.3.3", 97 | "request": "^2.67.0", 98 | "rimraf": "^2.5.4", 99 | "rxjs": "^5.4.1", 100 | "semistandard": "^11.0.0", 101 | "superagent": "^3.0.0", 102 | "xhr2": "^0.1.4" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/wrappers.js: -------------------------------------------------------------------------------- 1 | import makeDebug from 'debug'; 2 | import errors from 'feathers-errors'; 3 | import { hooks } from 'feathers-commons'; 4 | 5 | const debug = makeDebug('feathers:rest'); 6 | const hookObject = hooks.hookObject; 7 | const statusCodes = { 8 | created: 201, 9 | noContent: 204, 10 | methodNotAllowed: 405 11 | }; 12 | const methodMap = { 13 | find: 'GET', 14 | get: 'GET', 15 | create: 'POST', 16 | update: 'PUT', 17 | patch: 'PATCH', 18 | remove: 'DELETE' 19 | }; 20 | const allowedMethods = function (service) { 21 | return Object.keys(methodMap) 22 | .filter(method => typeof service[method] === 'function') 23 | .map(method => methodMap[method]) 24 | // Filter out duplicates 25 | .filter((value, index, list) => list.indexOf(value) === index); 26 | }; 27 | 28 | // A function that returns the middleware for a given method and service 29 | // `getArgs` is a function that should return additional leading service arguments 30 | function getHandler (method, getArgs, service) { 31 | return function (req, res, next) { 32 | res.setHeader('Allow', allowedMethods(service).join(',')); 33 | 34 | // Check if the method exists on the service at all. Send 405 (Method not allowed) if not 35 | if (typeof service[method] !== 'function') { 36 | debug(`Method '${method}' not allowed on '${req.url}'`); 37 | res.status(statusCodes.methodNotAllowed); 38 | return next(new errors.MethodNotAllowed(`Method \`${method}\` is not supported by this endpoint.`)); 39 | } 40 | 41 | let params = Object.assign({}, req.params || {}); 42 | delete params.__feathersId; 43 | 44 | // Grab the service parameters. Use req.feathers and set the query to req.query 45 | params = Object.assign({ query: req.query || {} }, params, req.feathers); 46 | 47 | // Run the getArgs callback, if available, for additional parameters 48 | const args = getArgs(req, res, next); 49 | 50 | // The service success callback which sets res.data or calls next() with the error 51 | const callback = function (error, data) { 52 | const hookArgs = args.concat([ params, callback ]); 53 | 54 | if (error) { 55 | debug(`Error in REST handler: \`${error.message || error}\``); 56 | res.hook = hookObject(method, 'error', hookArgs); 57 | return next(error); 58 | } 59 | 60 | res.data = data; 61 | res.hook = hookObject(method, 'after', hookArgs); 62 | 63 | if (!data) { 64 | debug(`No content returned for '${req.url}'`); 65 | res.status(statusCodes.noContent); 66 | } else if (method === 'create') { 67 | res.status(statusCodes.created); 68 | } 69 | 70 | return next(); 71 | }; 72 | 73 | debug(`REST handler calling \`${method}\` from \`${req.url}\``); 74 | service[method].apply(service, args.concat([ params, callback ])); 75 | }; 76 | } 77 | 78 | // Returns no leading parameters 79 | function reqNone () { 80 | return []; 81 | } 82 | 83 | // Returns the leading parameters for a `get` or `remove` request (the id) 84 | function reqId (req) { 85 | return [ req.params.__feathersId || null ]; 86 | } 87 | 88 | // Returns the leading parameters for an `update` or `patch` request (id, data) 89 | function reqUpdate (req) { 90 | return [ req.params.__feathersId || null, req.body ]; 91 | } 92 | 93 | // Returns the leading parameters for a `create` request (data) 94 | function reqCreate (req) { 95 | return [ req.body ]; 96 | } 97 | 98 | // Returns wrapped middleware for a service method. 99 | // Doing some fancy ES 5 .bind argument currying for .getHandler() 100 | // Basically what you are getting for each is a function(service) {} 101 | export default { 102 | find: getHandler.bind(null, 'find', reqNone), 103 | get: getHandler.bind(null, 'get', reqId), 104 | create: getHandler.bind(null, 'create', reqCreate), 105 | update: getHandler.bind(null, 'update', reqUpdate), 106 | patch: getHandler.bind(null, 'patch', reqUpdate), 107 | remove: getHandler.bind(null, 'remove', reqId) 108 | }; 109 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | 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 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | 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. 62 | 63 | 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. 64 | 65 | 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. 66 | 67 | 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. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | 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/) 72 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable handle-callback-err */ 2 | 3 | import assert from 'assert'; 4 | import request from 'request'; 5 | import feathers from 'feathers'; 6 | import bodyParser from 'body-parser'; 7 | import { 8 | Service as todoService, verify 9 | } from 'feathers-commons/lib/test-fixture'; 10 | import rest from '../src'; 11 | 12 | describe('REST provider', function () { 13 | describe('CRUD', function () { 14 | let server, app; 15 | 16 | before(function () { 17 | app = feathers().configure(rest(rest.formatter)) 18 | .use(bodyParser.json()) 19 | .use('codes', { 20 | get (id, params, callback) { 21 | callback(); 22 | }, 23 | 24 | create (data, params, callback) { 25 | callback(null, data); 26 | } 27 | }) 28 | .use('todo', todoService); 29 | 30 | server = app.listen(4777, () => app.use('tasks', todoService)); 31 | }); 32 | 33 | after(done => server.close(done)); 34 | 35 | describe('Services', () => { 36 | it('sets the hook object in res.hook', done => { 37 | app.use('/hook', { 38 | get (id, params, callback) { 39 | callback(null, { description: `You have to do ${id}` }); 40 | } 41 | }, function (req, res, next) { 42 | res.data.hook = res.hook; 43 | next(); 44 | }); 45 | 46 | request('http://localhost:4777/hook/dishes', (error, response, body) => { 47 | const hook = JSON.parse(body).hook; 48 | assert.deepEqual(hook, { 49 | id: 'dishes', 50 | params: { 51 | query: {}, 52 | provider: 'rest' 53 | }, 54 | method: 'get', 55 | type: 'after' 56 | }); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('sets the hook object in res.hook on error', done => { 62 | app.use('/hook-error', { 63 | get () { 64 | return Promise.reject(new Error('I blew up')); 65 | } 66 | }, function (error, req, res, next) { 67 | res.status(500); 68 | res.json({ 69 | hook: res.hook, 70 | error 71 | }); 72 | }); 73 | 74 | request('http://localhost:4777/hook-error/dishes', (error, response, body) => { 75 | const hook = JSON.parse(body).hook; 76 | assert.deepEqual(hook, { 77 | id: 'dishes', 78 | params: { 79 | query: {}, 80 | provider: 'rest' 81 | }, 82 | method: 'get', 83 | type: 'error' 84 | }); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('GET .find', done => { 90 | request('http://localhost:4777/todo', (error, response, body) => { 91 | assert.ok(response.statusCode === 200, 'Got OK status code'); 92 | verify.find(JSON.parse(body)); 93 | done(error); 94 | }); 95 | }); 96 | 97 | it('GET .get', done => { 98 | request('http://localhost:4777/todo/dishes', (error, response, body) => { 99 | assert.ok(response.statusCode === 200, 'Got OK status code'); 100 | verify.get('dishes', JSON.parse(body)); 101 | done(error); 102 | }); 103 | }); 104 | 105 | it('POST .create', done => { 106 | let original = { 107 | description: 'POST .create' 108 | }; 109 | 110 | request({ 111 | url: 'http://localhost:4777/todo', 112 | method: 'post', 113 | body: JSON.stringify(original), 114 | headers: { 115 | 'Content-Type': 'application/json' 116 | } 117 | }, (error, response, body) => { 118 | assert.ok(response.statusCode === 201, 'Got CREATED status code'); 119 | verify.create(original, JSON.parse(body)); 120 | 121 | done(error); 122 | }); 123 | }); 124 | 125 | it('PUT .update', done => { 126 | let original = { 127 | description: 'PUT .update' 128 | }; 129 | 130 | request({ 131 | url: 'http://localhost:4777/todo/544', 132 | method: 'put', 133 | body: JSON.stringify(original), 134 | headers: { 135 | 'Content-Type': 'application/json' 136 | } 137 | }, (error, response, body) => { 138 | assert.ok(response.statusCode === 200, 'Got OK status code'); 139 | verify.update(544, original, JSON.parse(body)); 140 | 141 | done(error); 142 | }); 143 | }); 144 | 145 | it('PUT .update many', done => { 146 | let original = { 147 | description: 'PUT .update', 148 | many: true 149 | }; 150 | 151 | request({ 152 | url: 'http://localhost:4777/todo', 153 | method: 'put', 154 | body: JSON.stringify(original), 155 | headers: { 156 | 'Content-Type': 'application/json' 157 | } 158 | }, (error, response, body) => { 159 | let data = JSON.parse(body); 160 | assert.ok(response.statusCode === 200, 'Got OK status code'); 161 | verify.update(null, original, data); 162 | 163 | done(error); 164 | }); 165 | }); 166 | 167 | it('PATCH .patch', done => { 168 | let original = { 169 | description: 'PATCH .patch' 170 | }; 171 | 172 | request({ 173 | url: 'http://localhost:4777/todo/544', 174 | method: 'patch', 175 | body: JSON.stringify(original), 176 | headers: { 177 | 'Content-Type': 'application/json' 178 | } 179 | }, (error, response, body) => { 180 | assert.ok(response.statusCode === 200, 'Got OK status code'); 181 | verify.patch(544, original, JSON.parse(body)); 182 | 183 | done(error); 184 | }); 185 | }); 186 | 187 | it('PATCH .patch many', done => { 188 | let original = { 189 | description: 'PATCH .patch', 190 | many: true 191 | }; 192 | 193 | request({ 194 | url: 'http://localhost:4777/todo', 195 | method: 'patch', 196 | body: JSON.stringify(original), 197 | headers: { 198 | 'Content-Type': 'application/json' 199 | } 200 | }, (error, response, body) => { 201 | assert.ok(response.statusCode === 200, 'Got OK status code'); 202 | verify.patch(null, original, JSON.parse(body)); 203 | 204 | done(error); 205 | }); 206 | }); 207 | 208 | it('DELETE .remove', done => { 209 | request({ 210 | url: 'http://localhost:4777/tasks/233', 211 | method: 'delete' 212 | }, function (error, response, body) { 213 | assert.ok(response.statusCode === 200, 'Got OK status code'); 214 | verify.remove(233, JSON.parse(body)); 215 | 216 | done(error); 217 | }); 218 | }); 219 | 220 | it('DELETE .remove many', done => { 221 | request({ 222 | url: 'http://localhost:4777/tasks', 223 | method: 'delete' 224 | }, (error, response, body) => { 225 | assert.ok(response.statusCode === 200, 'Got OK status code'); 226 | verify.remove(null, JSON.parse(body)); 227 | 228 | done(error); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('Dynamic Services', () => { 234 | it('GET .find', done => { 235 | request('http://localhost:4777/tasks', (error, response, body) => { 236 | assert.ok(response.statusCode === 200, 'Got OK status code'); 237 | verify.find(JSON.parse(body)); 238 | done(error); 239 | }); 240 | }); 241 | 242 | it('GET .get', done => { 243 | request('http://localhost:4777/tasks/dishes', (error, response, body) => { 244 | assert.ok(response.statusCode === 200, 'Got OK status code'); 245 | verify.get('dishes', JSON.parse(body)); 246 | done(error); 247 | }); 248 | }); 249 | 250 | it('POST .create', done => { 251 | let original = { 252 | description: 'Dynamic POST .create' 253 | }; 254 | 255 | request({ 256 | url: 'http://localhost:4777/tasks', 257 | method: 'post', 258 | body: JSON.stringify(original), 259 | headers: { 260 | 'Content-Type': 'application/json' 261 | } 262 | }, (error, response, body) => { 263 | assert.ok(response.statusCode === 201, 'Got CREATED status code'); 264 | verify.create(original, JSON.parse(body)); 265 | 266 | done(error); 267 | }); 268 | }); 269 | 270 | it('PUT .update', done => { 271 | let original = { 272 | description: 'Dynamic PUT .update' 273 | }; 274 | 275 | request({ 276 | url: 'http://localhost:4777/tasks/544', 277 | method: 'put', 278 | body: JSON.stringify(original), 279 | headers: { 280 | 'Content-Type': 'application/json' 281 | } 282 | }, (error, response, body) => { 283 | assert.ok(response.statusCode === 200, 'Got OK status code'); 284 | verify.update(544, original, JSON.parse(body)); 285 | 286 | done(error); 287 | }); 288 | }); 289 | 290 | it('PATCH .patch', done => { 291 | let original = { 292 | description: 'Dynamic PATCH .patch' 293 | }; 294 | 295 | request({ 296 | url: 'http://localhost:4777/tasks/544', 297 | method: 'patch', 298 | body: JSON.stringify(original), 299 | headers: { 300 | 'Content-Type': 'application/json' 301 | } 302 | }, (error, response, body) => { 303 | assert.ok(response.statusCode === 200, 'Got OK status code'); 304 | verify.patch(544, original, JSON.parse(body)); 305 | 306 | done(error); 307 | }); 308 | }); 309 | 310 | it('DELETE .remove', done => { 311 | request({ 312 | url: 'http://localhost:4777/tasks/233', 313 | method: 'delete' 314 | }, (error, response, body) => { 315 | assert.ok(response.statusCode === 200, 'Got OK status code'); 316 | verify.remove(233, JSON.parse(body)); 317 | 318 | done(error); 319 | }); 320 | }); 321 | }); 322 | }); 323 | 324 | describe('HTTP status codes', () => { 325 | let app; 326 | let server; 327 | 328 | before(function () { 329 | app = feathers().configure(rest(rest.formatter)) 330 | .use('todo', { 331 | get (id) { 332 | return Promise.resolve({ description: `You have to do ${id}` }); 333 | }, 334 | 335 | patch () { 336 | return Promise.reject(new Error('Not implemented')); 337 | }, 338 | 339 | find () { 340 | return Promise.resolve(null); 341 | } 342 | }); 343 | 344 | app.use(function (req, res, next) { 345 | if (typeof res.data !== 'undefined') { 346 | next(new Error('Should never get here')); 347 | } else { 348 | next(); 349 | } 350 | }); 351 | 352 | // Error handler 353 | app.use(function (error, req, res, next) { 354 | if (res.statusCode < 400) { 355 | res.status(500); 356 | } 357 | 358 | res.json({ message: error.message }); 359 | }); 360 | 361 | server = app.listen(4780); 362 | }); 363 | 364 | after(done => server.close(done)); 365 | 366 | it('throws a 405 for undefined service methods and sets Allow header (#99)', done => { 367 | request('http://localhost:4780/todo/dishes', (error, response, body) => { 368 | assert.ok(response.statusCode === 200, 'Got OK status code for .get'); 369 | assert.deepEqual(JSON.parse(body), { description: 'You have to do dishes' }, 'Got expected object'); 370 | request({ 371 | method: 'post', 372 | url: 'http://localhost:4780/todo' 373 | }, (error, response, body) => { 374 | assert.equal(response.headers.allow, 'GET,PATCH'); 375 | assert.ok(response.statusCode === 405, 'Got 405 for .create'); 376 | assert.deepEqual(JSON.parse(body), { message: 'Method `create` is not supported by this endpoint.' }, 'Error serialized as expected'); 377 | done(); 378 | }); 379 | }); 380 | }); 381 | 382 | it('throws a 404 for undefined route', done => { 383 | request('http://localhost:4780/todo/foo/bar', (error, response) => { 384 | assert.ok(response.statusCode === 404, 'Got Not Found code'); 385 | 386 | done(error); 387 | }); 388 | }); 389 | 390 | it('empty response sets 204 status codes, does not run other middleware (#391)', done => { 391 | request('http://localhost:4780/todo', (error, response) => { 392 | assert.ok(response.statusCode === 204, 'Got empty status code'); 393 | 394 | done(error); 395 | }); 396 | }); 397 | }); 398 | 399 | it('sets service parameters and provider type', done => { 400 | let service = { 401 | get (id, params, callback) { 402 | callback(null, params); 403 | } 404 | }; 405 | 406 | let server = feathers().configure(rest(rest.formatter)) 407 | .use(function (req, res, next) { 408 | assert.ok(req.feathers, 'Feathers object initialized'); 409 | req.feathers.test = 'Happy'; 410 | next(); 411 | }) 412 | .use('service', service) 413 | .listen(4778); 414 | 415 | request('http://localhost:4778/service/bla?some=param&another=thing', 416 | (error, response, body) => { 417 | let expected = { 418 | test: 'Happy', 419 | provider: 'rest', 420 | query: { 421 | some: 'param', 422 | another: 'thing' 423 | } 424 | }; 425 | 426 | assert.ok(response.statusCode === 200, 'Got OK status code'); 427 | assert.deepEqual(JSON.parse(body), expected, 'Got params object back'); 428 | server.close(done); 429 | }); 430 | }); 431 | 432 | it('lets you set the handler manually', done => { 433 | let app = feathers(); 434 | 435 | app.configure(rest(function (req, res) { 436 | res.format({ 437 | 'text/plain': function () { 438 | res.end(`The todo is: ${res.data.description}`); 439 | } 440 | }); 441 | })) 442 | .use('/todo', { 443 | get (id, params, callback) { 444 | callback(null, { description: `You have to do ${id}` }); 445 | } 446 | }); 447 | 448 | let server = app.listen(4776); 449 | request('http://localhost:4776/todo/dishes', (error, response, body) => { 450 | assert.equal(body, 'The todo is: You have to do dishes'); 451 | server.close(done); 452 | }); 453 | }); 454 | 455 | it('Lets you configure your own middleware before the handler (#40)', done => { 456 | let data = { description: 'Do dishes!', id: 'dishes' }; 457 | let app = feathers(); 458 | 459 | app.use(function defaultContentTypeMiddleware (req, res, next) { 460 | req.headers['content-type'] = req.headers['content-type'] || 'application/json'; 461 | next(); 462 | }) 463 | .configure(rest(rest.formatter)) 464 | .use(bodyParser.json()) 465 | .use('/todo', { 466 | create (data, params, callback) { 467 | callback(null, data); 468 | } 469 | }); 470 | 471 | let server = app.listen(4775); 472 | request({ 473 | method: 'POST', 474 | url: 'http://localhost:4775/todo', 475 | body: JSON.stringify(data) 476 | }, (error, response, body) => { 477 | assert.deepEqual(JSON.parse(body), data); 478 | server.close(done); 479 | }); 480 | }); 481 | 482 | it('Extend params with route params and allows id property (#76, #407)', done => { 483 | const todoService = { 484 | get (id, params) { 485 | return Promise.resolve({ 486 | id, 487 | appId: params.appId, 488 | paramsId: params.id 489 | }); 490 | } 491 | }; 492 | 493 | const app = feathers() 494 | .configure(rest()) 495 | .use('/:appId/:id/todo', todoService); 496 | 497 | const expected = { 498 | id: 'dishes', 499 | appId: 'theApp', 500 | paramsId: 'myId' 501 | }; 502 | 503 | const server = app.listen(6880).on('listening', function () { 504 | request('http://localhost:6880/theApp/myId/todo/' + expected.id, (error, response, body) => { 505 | assert.ok(response.statusCode === 200, 'Got OK status code'); 506 | assert.deepEqual(expected, JSON.parse(body)); 507 | server.close(done); 508 | }); 509 | }); 510 | }); 511 | 512 | if (process.version > 'v6.0.0') { 513 | it('throws an error with Feathers >= v3', () => { 514 | const feathers3 = require('@feathersjs/feathers'); 515 | 516 | try { 517 | feathers3().configure(rest()); 518 | assert.ok(false, 'Should never get here'); 519 | } catch (e) { 520 | assert.equal(e.message, `feathers-rest is not compatible with Feathers v${feathers3.version}. Use the Express framework bindings and REST adapter at @feathersjs/express instead.`); 521 | } 522 | }); 523 | } 524 | }); 525 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.8.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.8.1) (2017-10-31) 4 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v2.0.0-pre.1...v1.8.1) 5 | 6 | **Closed issues:** 7 | 8 | - Angular http implementation error [\#136](https://github.com/feathersjs-ecosystem/feathers-rest/issues/136) 9 | - Prepare for feathers v3 [\#133](https://github.com/feathersjs-ecosystem/feathers-rest/issues/133) 10 | - An in-range update of feathers is breaking the build 🚨 [\#129](https://github.com/feathersjs-ecosystem/feathers-rest/issues/129) 11 | 12 | **Merged pull requests:** 13 | 14 | - Add a warning when using with Feathers v3 [\#137](https://github.com/feathersjs-ecosystem/feathers-rest/pull/137) ([daffl](https://github.com/daffl)) 15 | - Update axios to the latest version 🚀 [\#135](https://github.com/feathersjs-ecosystem/feathers-rest/pull/135) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 16 | - adding codeclimate eslint to test [\#134](https://github.com/feathersjs-ecosystem/feathers-rest/pull/134) ([ekryski](https://github.com/ekryski)) 17 | - Update mocha to the latest version 🚀 [\#132](https://github.com/feathersjs-ecosystem/feathers-rest/pull/132) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 18 | - Always include route parameter [\#131](https://github.com/feathersjs-ecosystem/feathers-rest/pull/131) ([daffl](https://github.com/daffl)) 19 | - Remove babel-polyfill [\#130](https://github.com/feathersjs-ecosystem/feathers-rest/pull/130) ([daffl](https://github.com/daffl)) 20 | - Update debug to the latest version 🚀 [\#128](https://github.com/feathersjs-ecosystem/feathers-rest/pull/128) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 21 | 22 | ## [v2.0.0-pre.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v2.0.0-pre.1) (2017-07-23) 23 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.8.0...v2.0.0-pre.1) 24 | 25 | **Closed issues:** 26 | 27 | - How to return a pdf download file instead of data ? [\#125](https://github.com/feathersjs-ecosystem/feathers-rest/issues/125) 28 | - Map route parameters to `params.route` [\#124](https://github.com/feathersjs-ecosystem/feathers-rest/issues/124) 29 | 30 | **Merged pull requests:** 31 | 32 | - Map route parameters to params.route [\#126](https://github.com/feathersjs-ecosystem/feathers-rest/pull/126) ([daffl](https://github.com/daffl)) 33 | - Update REST provider to Feathers v3 [\#123](https://github.com/feathersjs-ecosystem/feathers-rest/pull/123) ([daffl](https://github.com/daffl)) 34 | - Update plugin infrastructure and remove client functionality [\#122](https://github.com/feathersjs-ecosystem/feathers-rest/pull/122) ([daffl](https://github.com/daffl)) 35 | 36 | ## [v1.8.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.8.0) (2017-07-04) 37 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.7.4...v1.8.0) 38 | 39 | **Closed issues:** 40 | 41 | - Consider how to handle file upload [\#102](https://github.com/feathersjs-ecosystem/feathers-rest/issues/102) 42 | 43 | **Merged pull requests:** 44 | 45 | - add support for @angular/http [\#120](https://github.com/feathersjs-ecosystem/feathers-rest/pull/120) ([j2L4e](https://github.com/j2L4e)) 46 | 47 | ## [v1.7.4](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.7.4) (2017-06-27) 48 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.7.3...v1.7.4) 49 | 50 | **Closed issues:** 51 | 52 | - Property 'axios' does not exist on type 'Transport' [\#116](https://github.com/feathersjs-ecosystem/feathers-rest/issues/116) 53 | 54 | **Merged pull requests:** 55 | 56 | - Add missing return-type annotation [\#119](https://github.com/feathersjs-ecosystem/feathers-rest/pull/119) ([couac](https://github.com/couac)) 57 | 58 | ## [v1.7.3](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.7.3) (2017-06-07) 59 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.7.2...v1.7.3) 60 | 61 | **Closed issues:** 62 | 63 | - $gte query example? [\#118](https://github.com/feathersjs-ecosystem/feathers-rest/issues/118) 64 | - JSON-API + feathers-rest [\#115](https://github.com/feathersjs-ecosystem/feathers-rest/issues/115) 65 | - $in: \[\] returning unexpected results [\#113](https://github.com/feathersjs-ecosystem/feathers-rest/issues/113) 66 | - Setting 'content-type' header properly with boundary [\#110](https://github.com/feathersjs-ecosystem/feathers-rest/issues/110) 67 | 68 | **Merged pull requests:** 69 | 70 | - Add typing support for axios [\#117](https://github.com/feathersjs-ecosystem/feathers-rest/pull/117) ([elixiao](https://github.com/elixiao)) 71 | - Update semistandard to the latest version 🚀 [\#111](https://github.com/feathersjs-ecosystem/feathers-rest/pull/111) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 72 | - explicitly updating qs dependency due to security vulnerability [\#109](https://github.com/feathersjs-ecosystem/feathers-rest/pull/109) ([ekryski](https://github.com/ekryski)) 73 | - Update dependencies to enable Greenkeeper 🌴 [\#108](https://github.com/feathersjs-ecosystem/feathers-rest/pull/108) ([greenkeeper[bot]](https://github.com/apps/greenkeeper)) 74 | 75 | ## [v1.7.2](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.7.2) (2017-04-11) 76 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.7.1...v1.7.2) 77 | 78 | **Closed issues:** 79 | 80 | - res.hook should exist in middleware after errors [\#106](https://github.com/feathersjs-ecosystem/feathers-rest/issues/106) 81 | - Add headers and remote IP [\#105](https://github.com/feathersjs-ecosystem/feathers-rest/issues/105) 82 | 83 | **Merged pull requests:** 84 | 85 | - Set res.hook on errors as well [\#107](https://github.com/feathersjs-ecosystem/feathers-rest/pull/107) ([daffl](https://github.com/daffl)) 86 | - Update axios to version 0.16.0 🚀 [\#104](https://github.com/feathersjs-ecosystem/feathers-rest/pull/104) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 87 | 88 | ## [v1.7.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.7.1) (2017-03-02) 89 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.7.0...v1.7.1) 90 | 91 | **Merged pull requests:** 92 | 93 | - Improve typedefs [\#103](https://github.com/feathersjs-ecosystem/feathers-rest/pull/103) ([myknbani](https://github.com/myknbani)) 94 | 95 | ## [v1.7.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.7.0) (2017-03-01) 96 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.6.0...v1.7.0) 97 | 98 | **Closed issues:** 99 | 100 | - Custom HTTP status code response [\#99](https://github.com/feathersjs-ecosystem/feathers-rest/issues/99) 101 | 102 | **Merged pull requests:** 103 | 104 | - Typescript Definitions [\#101](https://github.com/feathersjs-ecosystem/feathers-rest/pull/101) ([AbraaoAlves](https://github.com/AbraaoAlves)) 105 | - Adding Axios to readme [\#100](https://github.com/feathersjs-ecosystem/feathers-rest/pull/100) ([corymsmith](https://github.com/corymsmith)) 106 | 107 | ## [v1.6.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.6.0) (2016-12-31) 108 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.5.3...v1.6.0) 109 | 110 | **Closed issues:** 111 | 112 | - support axios? [\#94](https://github.com/feathersjs-ecosystem/feathers-rest/issues/94) 113 | 114 | **Merged pull requests:** 115 | 116 | - Add support for Axios as the client [\#98](https://github.com/feathersjs-ecosystem/feathers-rest/pull/98) ([daffl](https://github.com/daffl)) 117 | 118 | ## [v1.5.3](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.5.3) (2016-12-30) 119 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.5.2...v1.5.3) 120 | 121 | **Closed issues:** 122 | 123 | - Client: Support Service Middleware / Generic Service [\#91](https://github.com/feathersjs-ecosystem/feathers-rest/issues/91) 124 | - fn.call is not a function\(…\) on client side with webpack [\#89](https://github.com/feathersjs-ecosystem/feathers-rest/issues/89) 125 | - $in operator doesn't work correctly through rest [\#88](https://github.com/feathersjs-ecosystem/feathers-rest/issues/88) 126 | - add angular2 http client support [\#64](https://github.com/feathersjs-ecosystem/feathers-rest/issues/64) 127 | 128 | **Merged pull requests:** 129 | 130 | - Test and fix for undefined id on get [\#97](https://github.com/feathersjs-ecosystem/feathers-rest/pull/97) ([daffl](https://github.com/daffl)) 131 | - Create .codeclimate.yml [\#92](https://github.com/feathersjs-ecosystem/feathers-rest/pull/92) ([larkinscott](https://github.com/larkinscott)) 132 | - Tests for nested arrays and Mocha 3 updates [\#90](https://github.com/feathersjs-ecosystem/feathers-rest/pull/90) ([daffl](https://github.com/daffl)) 133 | - Update superagent to version 3.0.0 🚀 [\#87](https://github.com/feathersjs-ecosystem/feathers-rest/pull/87) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 134 | - Update feathers-memory to version 1.0.0 🚀 [\#86](https://github.com/feathersjs-ecosystem/feathers-rest/pull/86) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 135 | - Update feathers-commons to version 0.8.0 🚀 [\#85](https://github.com/feathersjs-ecosystem/feathers-rest/pull/85) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 136 | 137 | ## [v1.5.2](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.5.2) (2016-11-08) 138 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.5.1...v1.5.2) 139 | 140 | **Closed issues:** 141 | 142 | - Client fails to parse json response on 204 status using fetch [\#83](https://github.com/feathersjs-ecosystem/feathers-rest/issues/83) 143 | 144 | **Merged pull requests:** 145 | 146 | - Return null for no-content responses with fetch [\#84](https://github.com/feathersjs-ecosystem/feathers-rest/pull/84) ([bedeoverend](https://github.com/bedeoverend)) 147 | - Swapping rm to rifraf and using relative path to \_mocha for windows support [\#82](https://github.com/feathersjs-ecosystem/feathers-rest/pull/82) ([corymsmith](https://github.com/corymsmith)) 148 | - jshint —\> semistandard [\#81](https://github.com/feathersjs-ecosystem/feathers-rest/pull/81) ([corymsmith](https://github.com/corymsmith)) 149 | 150 | ## [v1.5.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.5.1) (2016-10-21) 151 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.5.0...v1.5.1) 152 | 153 | **Closed issues:** 154 | 155 | - Rest clients should not delete/patch/update everything if `id` is undefined [\#79](https://github.com/feathersjs-ecosystem/feathers-rest/issues/79) 156 | - Distinct [\#76](https://github.com/feathersjs-ecosystem/feathers-rest/issues/76) 157 | - Sequelize Params not passed through when using REST Client [\#75](https://github.com/feathersjs-ecosystem/feathers-rest/issues/75) 158 | 159 | **Merged pull requests:** 160 | 161 | - Throw error when update, patch or remove id is undefined [\#80](https://github.com/feathersjs-ecosystem/feathers-rest/pull/80) ([daffl](https://github.com/daffl)) 162 | - adding code coverage [\#78](https://github.com/feathersjs-ecosystem/feathers-rest/pull/78) ([ekryski](https://github.com/ekryski)) 163 | - Rename internal route id property [\#74](https://github.com/feathersjs-ecosystem/feathers-rest/pull/74) ([daffl](https://github.com/daffl)) 164 | 165 | ## [v1.5.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.5.0) (2016-09-09) 166 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.4.4...v1.5.0) 167 | 168 | **Merged pull requests:** 169 | 170 | - Update feathers-memory to version 0.8.0 🚀 [\#73](https://github.com/feathersjs-ecosystem/feathers-rest/pull/73) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 171 | - Allow using default fetch headers for all requests [\#72](https://github.com/feathersjs-ecosystem/feathers-rest/pull/72) ([bahmutov](https://github.com/bahmutov)) 172 | 173 | ## [v1.4.4](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.4.4) (2016-08-18) 174 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.4.3...v1.4.4) 175 | 176 | **Merged pull requests:** 177 | 178 | - Call `next` properly in formatter [\#67](https://github.com/feathersjs-ecosystem/feathers-rest/pull/67) ([daffl](https://github.com/daffl)) 179 | - Update mocha to version 3.0.0 🚀 [\#65](https://github.com/feathersjs-ecosystem/feathers-rest/pull/65) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 180 | 181 | ## [v1.4.3](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.4.3) (2016-07-14) 182 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.4.2...v1.4.3) 183 | 184 | **Closed issues:** 185 | 186 | - Implementation of fetch doesn't work in browser? [\#62](https://github.com/feathersjs-ecosystem/feathers-rest/issues/62) 187 | - Setting hook.method should direct the request to the chosen method. [\#60](https://github.com/feathersjs-ecosystem/feathers-rest/issues/60) 188 | - Error in REST handler: `Invalid atomic update value for $\_\_original\_remove. Expected an object, received function` [\#59](https://github.com/feathersjs-ecosystem/feathers-rest/issues/59) 189 | - Return `error.response.json\(\)` doesn't resolve using fetch [\#56](https://github.com/feathersjs-ecosystem/feathers-rest/issues/56) 190 | 191 | **Merged pull requests:** 192 | 193 | - Use response.json\(\) for converting errors [\#63](https://github.com/feathersjs-ecosystem/feathers-rest/pull/63) ([daffl](https://github.com/daffl)) 194 | 195 | ## [v1.4.2](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.4.2) (2016-06-04) 196 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.4.1...v1.4.2) 197 | 198 | **Closed issues:** 199 | 200 | - Support native browser fetch [\#54](https://github.com/feathersjs-ecosystem/feathers-rest/issues/54) 201 | 202 | **Merged pull requests:** 203 | 204 | - Fix fetch reference so that it works in the browser [\#55](https://github.com/feathersjs-ecosystem/feathers-rest/pull/55) ([daffl](https://github.com/daffl)) 205 | 206 | ## [v1.4.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.4.1) (2016-05-30) 207 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.4.0...v1.4.1) 208 | 209 | ## [v1.4.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.4.0) (2016-05-30) 210 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.3.0...v1.4.0) 211 | 212 | **Closed issues:** 213 | 214 | - Clients should convert error objects to feathers-errors [\#50](https://github.com/feathersjs-ecosystem/feathers-rest/issues/50) 215 | 216 | **Merged pull requests:** 217 | 218 | - Convert to Feathers errors [\#53](https://github.com/feathersjs-ecosystem/feathers-rest/pull/53) ([daffl](https://github.com/daffl)) 219 | - Update superagent to version 2.0.0 🚀 [\#52](https://github.com/feathersjs-ecosystem/feathers-rest/pull/52) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 220 | - mocha@2.5.0 breaks build 🚨 [\#48](https://github.com/feathersjs-ecosystem/feathers-rest/pull/48) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 221 | - Update babel-plugin-add-module-exports to version 0.2.0 🚀 [\#45](https://github.com/feathersjs-ecosystem/feathers-rest/pull/45) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 222 | 223 | ## [v1.3.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.3.0) (2016-04-28) 224 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.5...v1.3.0) 225 | 226 | **Closed issues:** 227 | 228 | - Support patch, remove and update many [\#40](https://github.com/feathersjs-ecosystem/feathers-rest/issues/40) 229 | - Fix link in README for providers docs [\#38](https://github.com/feathersjs-ecosystem/feathers-rest/issues/38) 230 | - feathers-rest returns Internal Server Error on invalid login [\#36](https://github.com/feathersjs-ecosystem/feathers-rest/issues/36) 231 | - Set Allow response headers [\#21](https://github.com/feathersjs-ecosystem/feathers-rest/issues/21) 232 | 233 | **Merged pull requests:** 234 | 235 | - Add Allow HTTP headers [\#42](https://github.com/feathersjs-ecosystem/feathers-rest/pull/42) ([daffl](https://github.com/daffl)) 236 | - Support patch, update and remove many in the client [\#41](https://github.com/feathersjs-ecosystem/feathers-rest/pull/41) ([daffl](https://github.com/daffl)) 237 | 238 | ## [v1.2.5](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.5) (2016-04-13) 239 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.4...v1.2.5) 240 | 241 | **Fixed bugs:** 242 | 243 | - superagent doesn't set JSON Accept header [\#29](https://github.com/feathersjs-ecosystem/feathers-rest/issues/29) 244 | 245 | **Closed issues:** 246 | 247 | - Set Accept header as default [\#28](https://github.com/feathersjs-ecosystem/feathers-rest/issues/28) 248 | - Remove JSDOM as devDependency [\#25](https://github.com/feathersjs-ecosystem/feathers-rest/issues/25) 249 | - Add the ability for services to be internal only. [\#23](https://github.com/feathersjs-ecosystem/feathers-rest/issues/23) 250 | 251 | **Merged pull requests:** 252 | 253 | - Test and fix that makes sure that all cliens are accepting JSON [\#35](https://github.com/feathersjs-ecosystem/feathers-rest/pull/35) ([daffl](https://github.com/daffl)) 254 | - steal-compatibility [\#33](https://github.com/feathersjs-ecosystem/feathers-rest/pull/33) ([marshallswain](https://github.com/marshallswain)) 255 | - Update feathers-memory to version 0.7.0 🚀 [\#32](https://github.com/feathersjs-ecosystem/feathers-rest/pull/32) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 256 | - Readme: Use native 'window.fetch' [\#30](https://github.com/feathersjs-ecosystem/feathers-rest/pull/30) ([queckezz](https://github.com/queckezz)) 257 | 258 | ## [v1.2.4](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.4) (2016-03-13) 259 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.3...v1.2.4) 260 | 261 | **Merged pull requests:** 262 | 263 | - Test and fix for request to serialize errors properly [\#22](https://github.com/feathersjs-ecosystem/feathers-rest/pull/22) ([daffl](https://github.com/daffl)) 264 | 265 | ## [v1.2.3](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.3) (2016-02-24) 266 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.2...v1.2.3) 267 | 268 | **Merged pull requests:** 269 | 270 | - Update feathers-errors to version 2.0.1 🚀 [\#20](https://github.com/feathersjs-ecosystem/feathers-rest/pull/20) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 271 | - Update jsdom to version 8.0.4 🚀 [\#18](https://github.com/feathersjs-ecosystem/feathers-rest/pull/18) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 272 | 273 | ## [v1.2.2](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.2) (2016-02-11) 274 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.1...v1.2.2) 275 | 276 | **Merged pull requests:** 277 | 278 | - Allow to instantiate a client instance [\#16](https://github.com/feathersjs-ecosystem/feathers-rest/pull/16) ([daffl](https://github.com/daffl)) 279 | 280 | ## [v1.2.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.1) (2016-02-09) 281 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.2.0...v1.2.1) 282 | 283 | ## [v1.2.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.2.0) (2016-02-09) 284 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.1.1...v1.2.0) 285 | 286 | **Closed issues:** 287 | 288 | - Support setting headers [\#12](https://github.com/feathersjs-ecosystem/feathers-rest/issues/12) 289 | - Errors aren't returned correctly when using fetch [\#4](https://github.com/feathersjs-ecosystem/feathers-rest/issues/4) 290 | 291 | **Merged pull requests:** 292 | 293 | - Adds support for custom headers via params [\#14](https://github.com/feathersjs-ecosystem/feathers-rest/pull/14) ([ekryski](https://github.com/ekryski)) 294 | - Update feathers-memory to version 0.6.0 🚀 [\#10](https://github.com/feathersjs-ecosystem/feathers-rest/pull/10) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 295 | - Adding nsp check [\#8](https://github.com/feathersjs-ecosystem/feathers-rest/pull/8) ([marshallswain](https://github.com/marshallswain)) 296 | - Update feathers-commons to version 0.6.0 🚀 [\#6](https://github.com/feathersjs-ecosystem/feathers-rest/pull/6) ([greenkeeperio-bot](https://github.com/greenkeeperio-bot)) 297 | - Correctly handling status codes with fetch [\#5](https://github.com/feathersjs-ecosystem/feathers-rest/pull/5) ([corymsmith](https://github.com/corymsmith)) 298 | 299 | ## [v1.1.1](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.1.1) (2016-01-16) 300 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.1.0...v1.1.1) 301 | 302 | **Merged pull requests:** 303 | 304 | - Fixing .npmignore entries [\#3](https://github.com/feathersjs-ecosystem/feathers-rest/pull/3) ([corymsmith](https://github.com/corymsmith)) 305 | 306 | ## [v1.1.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.1.0) (2016-01-10) 307 | [Full Changelog](https://github.com/feathersjs-ecosystem/feathers-rest/compare/v1.0.0...v1.1.0) 308 | 309 | **Merged pull requests:** 310 | 311 | - Adding REST Feathers clients for Fetch, jQuery, Request and Superagent [\#1](https://github.com/feathersjs-ecosystem/feathers-rest/pull/1) ([daffl](https://github.com/daffl)) 312 | 313 | ## [v1.0.0](https://github.com/feathersjs-ecosystem/feathers-rest/tree/v1.0.0) (2016-01-03) 314 | 315 | 316 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* --------------------------------------------------------------------------------