├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── tests └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/*.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # package lock file 61 | package.lock 62 | package-lock.json 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6" 5 | - "7" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 OpenComponents community 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | oc-graphql-client [![Build Status](https://travis-ci.org/opencomponents/oc-graphql-client.svg?branch=master)](https://travis-ci.org/opencomponents/oc-graphql-client) 2 | ========== 3 | 4 | ## NOTICE! 5 | - The current released version (3.*) does not use the Apollo client due to memory consumption issues. 6 | - The client does not expose a querybuilder, instead just use a raw string as the examples does. 7 | 8 | ---- 9 | 10 | A [OpenComponents](https://github.com/opentable/oc) plugin that expose the a graphql client for interacting with a GraphQL based server. 11 | 12 | ## Requirements: 13 | - OC Registry 14 | - GraphQL Server 15 | - Node >= v6 16 | 17 | ## Install 18 | 19 | ````javascript 20 | yarn add oc-graphql-client 21 | ```` 22 | 23 | ## Registry setup 24 | 25 | More info about integrating OC plugins: [here](https://github.com/opentable/oc/wiki/Registry#plugins) 26 | 27 | ````javascript 28 | ... 29 | var registry = new oc.registry(configuration); 30 | 31 | registry.register({ 32 | name: 'graphqlClient', 33 | register: require('oc-graphql-client'), 34 | options: { 35 | serverUrl: 'http://graphql-server.hosts.com' 36 | } 37 | }, function(err){ 38 | if(err){ 39 | console.log('plugin initialisation failed:', err); 40 | } else { 41 | console.log('graphql client now available'); 42 | } 43 | }); 44 | 45 | ... 46 | 47 | registry.start(callback); 48 | ```` 49 | 50 | 51 | ## Register API 52 | 53 | |parameter|type|mandatory|description| 54 | |---------|----|---------|-----------| 55 | |serverUrl|`string`|yes|The Url for the GraphQL server| 56 | 57 | ## Usage 58 | 59 | Example for a components' server.js: 60 | 61 | ````javascript 62 | 63 | module.exports.data = function(context, callback){ 64 | const query = ` 65 | query restaurantInfo($id: Int!) { 66 | restaurant(id: $id) { 67 | name 68 | } 69 | }`; 70 | 71 | const headers = { 72 | 'accept-language': 'en-US, en' 73 | }; 74 | 75 | context.plugins.graphql.query({ query, variables: { id: 4 } }, headers, timeout) 76 | .then(res => { ... }) 77 | .catch(err => { ... }) 78 | ```` 79 | 80 | ## API 81 | 82 | |parameter|type|mandatory|description| 83 | |---------|----|---------|-----------| 84 | |options|`object`|yes|A composite of the query & variables to pass to GraphQL server| 85 | |headers|`object`|no|The headers to pass down to unerlying services| 86 | |timeout|`int`|no|The timeout in ms. It defaults to OS default | 87 | 88 | ## Contributing 89 | 90 | PR's are welcome! 91 | 92 | ## License 93 | 94 | MIT 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const request = require('request'); 3 | const _ = require('lodash'); 4 | 5 | let settings; 6 | 7 | module.exports.register = (opts, dependencies, next) => { 8 | if (!opts.serverUrl) { 9 | return next(new Error('The serverUrl parameter is invalid')); 10 | } 11 | 12 | settings = opts; 13 | return next(); 14 | }; 15 | 16 | module.exports.execute = () => ({ 17 | query: (options, headers, timeout) => new Promise((resolve, reject) => request({ 18 | body: { 19 | query: options.query, 20 | variables: options.variables, 21 | operationName: options.operationName ? options.operationName : null, 22 | }, 23 | headers: _.extend({ 'User-Agent': 'oc' }, headers), 24 | json: true, 25 | method: 'POST', 26 | timeout, 27 | url: settings.serverUrl, 28 | }, (err, result, body) => { 29 | if (err) { 30 | return reject(new Error(err)); 31 | } 32 | 33 | if (typeof body !== 'object' || body === null) { 34 | return reject({ 35 | errors: [{ 36 | message: 'Invalid response from graphql server.', 37 | http: { 38 | status: http.STATUS_CODES[result.statusCode], 39 | code: result.statusCode, 40 | body, 41 | }, 42 | }], 43 | }); 44 | } 45 | 46 | // http://facebook.github.io/graphql/October2016/#sec-Errors 47 | if ('errors' in body || !('data' in body)) { 48 | return reject(body); 49 | } 50 | 51 | return resolve(body); 52 | })), 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oc-graphql-client", 3 | "version": "4.0.2", 4 | "description": "OpenComponents client plugin for GraphQL", 5 | "main": "index.js", 6 | "author": "Chris Cartlidge ", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/opencomponents/oc-graphql-client" 11 | }, 12 | "scripts": { 13 | "lint": "eslint index.js", 14 | "test": "npm run lint && mocha tests" 15 | }, 16 | "dependencies": { 17 | "lodash": "^4.17.4", 18 | "request": "^2.81.0" 19 | }, 20 | "devDependencies": { 21 | "chai": "^3.5.0", 22 | "eslint": "^5.16.0", 23 | "eslint-config-airbnb": "^14.1.0", 24 | "eslint-plugin-import": "^2.2.0", 25 | "eslint-plugin-jsx-a11y": "^4.0.0", 26 | "eslint-plugin-react": "^6.10.0", 27 | "injectr": "^0.5.1", 28 | "mocha": "^6.1.4", 29 | "sinon": "^1.17.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const injectr = require('injectr'); 3 | const sinon = require('sinon'); 4 | 5 | describe('OpenTable OC registry :: plugins :: graphql-plugin ', () => { 6 | 7 | describe('when calling register with an valid batchInterval', () => { 8 | const plugin = injectr('../index.js', { 9 | request: sinon.stub().yields(null, {}, 'ok') 10 | }); 11 | 12 | let error; 13 | beforeEach((done) => { 14 | plugin.register({ serverUrl: 'http://graphql' }, {}, (err) => { 15 | error = err; 16 | done(); 17 | }); 18 | }); 19 | 20 | it('should not return an error', () => { 21 | expect(error).to.be.undefined; 22 | }); 23 | }); 24 | 25 | describe('when calling register with no serverUrl', () => { 26 | const plugin = injectr('../index.js', { 27 | request: sinon.stub().yields(null, {}, 'ok') 28 | }); 29 | let error; 30 | beforeEach((done) => { 31 | plugin.register({}, {}, (err) => { 32 | error = err; 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return an error saying is serverUrl invalid', () => { 38 | expect(error.toString()).to.contain('The serverUrl parameter is invalid'); 39 | }); 40 | }); 41 | 42 | describe('when calling with the correct options', () => { 43 | const plugin = injectr('../index.js', { 44 | request: sinon.stub().yields(null, {}, 'ok') 45 | }); 46 | const next = sinon.spy(); 47 | 48 | beforeEach((done) => { 49 | plugin.register({ 50 | serverUrl: 'http://graphql' 51 | }, {}, next); 52 | done(); 53 | }); 54 | 55 | it('should call next', () => { 56 | expect(next.called).to.be.true; 57 | }); 58 | }); 59 | 60 | describe('when calling execute', () => { 61 | const plugin = injectr('../index.js', { 62 | request: sinon.stub().yields(null, {}, 'ok') 63 | }); 64 | let client; 65 | beforeEach((done) => { 66 | plugin.register({ serverUrl: 'http://graphql' } 67 | , {}, () => { 68 | client = plugin.execute(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should expose a query method', () => { 74 | expect(client).to.have.property('query'); 75 | }); 76 | }); 77 | 78 | describe('when calling query and endpoint fails with an invalid response', () => { 79 | let client; 80 | const plugin = injectr('../index.js', { 81 | request: sinon.stub().yields(null, { statusCode: 500}, null) 82 | }); 83 | 84 | beforeEach((done) => { 85 | plugin.register({ serverUrl: 'http://graphql' } 86 | , {}, () => { 87 | client = plugin.execute(); 88 | done(); 89 | }); 90 | }); 91 | 92 | it('should reject with a graphql compliant object with an error', (done) => { 93 | client.query({ query: {}, variables: { test: 1 } }, { 'accept-language': 'en-US' }) 94 | .catch(resp => { 95 | expect(resp.errors).to.deep.equal([ 96 | { 97 | // http://facebook.github.io/graphql/draft/#sec-Errors 98 | // message is mandatory, other optional fields are irrelevant 99 | // extra custom fields are allowed. 100 | message: 'Invalid response from graphql server.', 101 | http: { 102 | body: null, 103 | status: 'Internal Server Error', 104 | code: 500 105 | } 106 | } 107 | ]) 108 | }).then(done, done) 109 | }); 110 | }); 111 | 112 | describe('when calling query and recieving errors', () => { 113 | let client; 114 | const body = { 115 | errors: [ { message: 'Field "xyz" argument "jkl" of type "Type!" is required but not provided.' } ] 116 | }; 117 | const plugin = injectr('../index.js', { 118 | request: sinon.stub().yields(null, { statusCode: 400 }, body) 119 | }); 120 | 121 | beforeEach((done) => { 122 | plugin.register({ serverUrl: 'http://graphql' } 123 | , {}, () => { 124 | client = plugin.execute(); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('should reject with the body with errors', (done) => { 130 | client.query({ query: {}, variables: { test: 1 } }, { 'accept-language': 'en-US' }) 131 | .catch(error => { 132 | expect(error).to.deep.equal(body) 133 | }).then(done, done) 134 | }); 135 | }); 136 | 137 | describe('when calling query and no data', () => { 138 | let client; 139 | const body = { }; // under sensible conditions, there would be errors, but the spec doesn't mandate this. 140 | const plugin = injectr('../index.js', { 141 | request: sinon.stub().yields(null, { statusCode: 200 }, body) 142 | }); 143 | 144 | beforeEach((done) => { 145 | plugin.register({ serverUrl: 'http://graphql' } 146 | , {}, () => { 147 | client = plugin.execute(); 148 | done(); 149 | }); 150 | }); 151 | 152 | it('should reject with the body', (done) => { 153 | client.query({ query: {}, variables: { test: 1 } }, { 'accept-language': 'en-US' }) 154 | .catch(error => { 155 | expect(error).to.deep.equal(body) 156 | }).then(done, done) 157 | }); 158 | }); 159 | 160 | describe('when calling query successfully', () => { 161 | let client; 162 | const plugin = injectr('../index.js', { 163 | request: sinon.stub().yields(null, { statusCode: 200}, { data: {someJson: true } }) 164 | }); 165 | 166 | beforeEach((done) => { 167 | plugin.register({ serverUrl: 'http://graphql' } 168 | , {}, () => { 169 | client = plugin.execute(); 170 | done(); 171 | }); 172 | }); 173 | 174 | it('should return the json response data', (done) => { 175 | client.query({ query: {}, variables: { test: 1 } }, { 'accept-language': 'en-US' }) 176 | .then(res => { 177 | expect(res).to.eql({ data: { someJson: true } }) 178 | }).then(done, done) 179 | }); 180 | }); 181 | }); --------------------------------------------------------------------------------