From 542243eb7c5b34475d01b5bad781ddf20081f9af Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 20 Jul 2016 02:12:02 +0300 Subject: [PATCH 01/20] onRequest and onResponse hooks --- index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 38b2cb7..509d392 100644 --- a/index.js +++ b/index.js @@ -40,7 +40,7 @@ module.exports = function (params) { var headers = new Headers() headers.append('Content-Type', 'application/json') - return fetch(params.url, { + var req = { method: 'POST', body: JSON.stringify({ query: query, @@ -48,7 +48,12 @@ module.exports = function (params) { }), headers: headers, credentials: params.credentials - }).then(function (res) { + } + + if (params.onRequest) params.onRequest(req) + + return fetch(params.url, req).then(function (res) { + if (params.onResponse) params.onResponse(res) return res.json() }).then(function (data) { if (data.errors && data.errors.length) { From 5178b2d959956508091405093650ce84326d8ee8 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 20 Jul 2016 02:26:05 +0300 Subject: [PATCH 02/20] Readme --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f92d8a7..8e940d8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,20 @@ npm install graphql-client -S ## How To ```javascript -var client = require('graphql-client')({url: 'http://your-host/graphql'}) +var client = require('graphql-client')({ + url: 'http://your-host/graphql', + + // before request hook + onRequest (req) { + // Do whatever you want with `req`, e.g. add JWT auth header + req.headers.set('Authentication', 'Bearer ' + token) + }, + + // response hook + onResponse (res) { + ... + } +}) var query = ` query search ($query: String, $from: Int, $limit: Int) { From 7efc6f3373d0046c255874e01b576efd503def92 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Sun, 24 Jul 2016 16:22:34 +0300 Subject: [PATCH 03/20] Fixed "TypeError: Cannot read property 'line' of undefined" --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 509d392..a70d0f0 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ function highlightQuery (query, errors) { query.split('\n').forEach(function (row, index) { var line = index + 1 - var lineErrors = locations.filter(function (loc) { return loc.line === line }) + var lineErrors = locations.filter(function (loc) { return loc && loc.line === line }) queryHighlight += row + '\n' From 099ed741b1a158fc2e3a16a95b2cc57de17188f2 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Sun, 24 Jul 2016 17:11:49 +0300 Subject: [PATCH 04/20] Instead of throwning, return GraphQL errors array --- index.js | 54 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index a70d0f0..8ce97dd 100644 --- a/index.js +++ b/index.js @@ -1,34 +1,36 @@ -function highlightQuery (query, errors) { - var locations = errors.map(function (e) { return e.locations }) - .reduce(function (a, b) { - return a.concat(b) - }, []) +if (process.env.ENV !== 'production') { + function highlightQuery (query, errors) { + var locations = errors.map(function (e) { return e.locations }) + .reduce(function (a, b) { + return a.concat(b) + }, []) - var queryHighlight = '' + var queryHighlight = '' - query.split('\n').forEach(function (row, index) { - var line = index + 1 - var lineErrors = locations.filter(function (loc) { return loc && loc.line === line }) + query.split('\n').forEach(function (row, index) { + var line = index + 1 + var lineErrors = locations.filter(function (loc) { return loc && loc.line === line }) - queryHighlight += row + '\n' + queryHighlight += row + '\n' - if (lineErrors.length) { - var errorHighlight = [] + if (lineErrors.length) { + var errorHighlight = [] - lineErrors.forEach(function (line) { - for (var i = 0; i < 8; i++) { - errorHighlight[line.column + i] = '~' - } - }) + lineErrors.forEach(function (line) { + for (var i = 0; i < 8; i++) { + errorHighlight[line.column + i] = '~' + } + }) - for (var i = 0; i < errorHighlight.length; i++) { - queryHighlight += errorHighlight[i] || ' ' + for (var i = 0; i < errorHighlight.length; i++) { + queryHighlight += errorHighlight[i] || ' ' + } + queryHighlight += '\n' } - queryHighlight += '\n' - } - }) + }) - return queryHighlight + return queryHighlight + } } module.exports = function (params) { @@ -57,7 +59,11 @@ module.exports = function (params) { return res.json() }).then(function (data) { if (data.errors && data.errors.length) { - throw new Error(data.errors.map(function (e) { return e.message }).join('\n') + '\n' + highlightQuery(query, data.errors)) + if (process.env.ENV !== 'production') { + console.warn(data.errors.map(function (e) { return e.message }).join('\n') + '\n' + highlightQuery(query, data.errors)) + } else { + console.warn(data.errors.map(function (e) { return e.message }).join('\n')) + } } return data }) From 1a5dd54e2f367a3434008fd71b62543788fdee47 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Sun, 24 Jul 2016 17:17:50 +0300 Subject: [PATCH 05/20] Removed highlightQuery, simply appending query to `console.error` --- index.js | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/index.js b/index.js index 8ce97dd..e0dbc53 100644 --- a/index.js +++ b/index.js @@ -1,38 +1,3 @@ -if (process.env.ENV !== 'production') { - function highlightQuery (query, errors) { - var locations = errors.map(function (e) { return e.locations }) - .reduce(function (a, b) { - return a.concat(b) - }, []) - - var queryHighlight = '' - - query.split('\n').forEach(function (row, index) { - var line = index + 1 - var lineErrors = locations.filter(function (loc) { return loc && loc.line === line }) - - queryHighlight += row + '\n' - - if (lineErrors.length) { - var errorHighlight = [] - - lineErrors.forEach(function (line) { - for (var i = 0; i < 8; i++) { - errorHighlight[line.column + i] = '~' - } - }) - - for (var i = 0; i < errorHighlight.length; i++) { - queryHighlight += errorHighlight[i] || ' ' - } - queryHighlight += '\n' - } - }) - - return queryHighlight - } -} - module.exports = function (params) { require('isomorphic-fetch') if (!params.url) throw new Error('Missing url parameter') @@ -59,11 +24,7 @@ module.exports = function (params) { return res.json() }).then(function (data) { if (data.errors && data.errors.length) { - if (process.env.ENV !== 'production') { - console.warn(data.errors.map(function (e) { return e.message }).join('\n') + '\n' + highlightQuery(query, data.errors)) - } else { - console.warn(data.errors.map(function (e) { return e.message }).join('\n')) - } + console.error(data.errors.map(function (e) { return e.message }).join('\n') + '\n' + query) } return data }) From 64b2892c57e005061ca188601ce09fc878482673 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 00:35:31 +0300 Subject: [PATCH 06/20] Rewritten. Added new methods and added tests --- Makefile | 7 +++ index.js | 123 ++++++++++++++++++++++++++++++++++----------- package.json | 6 ++- test/lib/schema.js | 36 +++++++++++++ test/lib/server.js | 54 ++++++++++++++++++++ test/specs.js | 93 ++++++++++++++++++++++++++++++++++ 6 files changed, 288 insertions(+), 31 deletions(-) create mode 100644 Makefile create mode 100644 test/lib/schema.js create mode 100644 test/lib/server.js create mode 100644 test/specs.js diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4cfdb2c --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +test: + mocha test/specs.js + +lint: + standard . + +.PHONY: test lint \ No newline at end of file diff --git a/index.js b/index.js index e0dbc53..470cd8a 100644 --- a/index.js +++ b/index.js @@ -1,33 +1,96 @@ -module.exports = function (params) { - require('isomorphic-fetch') - if (!params.url) throw new Error('Missing url parameter') - - return { - query: function (query, variables) { - var headers = new Headers() - headers.append('Content-Type', 'application/json') - - var req = { - method: 'POST', - body: JSON.stringify({ - query: query, - variables: variables - }), - headers: headers, - credentials: params.credentials - } - - if (params.onRequest) params.onRequest(req) - - return fetch(params.url, req).then(function (res) { - if (params.onResponse) params.onResponse(res) - return res.json() - }).then(function (data) { - if (data.errors && data.errors.length) { - console.error(data.errors.map(function (e) { return e.message }).join('\n') + '\n' + query) - } - return data - }) +/* global fetch, Headers */ +require('isomorphic-fetch') + +function Client (options) { + if (!options.url) throw new Error('Missing url parameter') + + this.options = options + // A stack of registered listeners + this.listeners = [] +} + +// to decrease file size +var proto = Client.prototype + +/** + * Send a query and get a Promise + * @param {String} query + * @param {Object} variables + * @returns {Promise} + */ +proto.query = function (query, variables) { + var headers = new Headers() + headers.set('Content-Type', 'application/json') + + var req = this.options.request || {} + req.method || (req.method = 'POST') + req.body || (req.body = JSON.stringify({ + query: query, + variables: variables + })) + req.headers || (req.headers = headers) + + return this.fetch(this.options.url, req) +} + +/** + * For making requests + * @param {String} url - GraphQL endpoint + * @param {Array} args + * @returns Promise + */ +proto.fetch = function (url, req) { + var self = this + + self.trigger('request', [req]) + + return fetch(url, req).then(function (res) { + self.trigger('response', [res]) + return res.json() + }).then(function (data) { + self.trigger('data', [data]) + return data + }).catch(function (e) { + self.trigger('error', [e]) + }) +} + +/** + * Register a listener. + * @param {String} eventName - 'request', 'response', 'data', 'error' + * @param {Function} callback + * @returns Client instance + */ +proto.on = function (eventName, callback) { + var allowedNames = ['request', 'response', 'data', 'error'] + + if (~allowedNames.indexOf(eventName)) { + this.listeners.push([ eventName, callback ]) + } + + return this +} + +/** + * Trigger an event. + * @param {String} eventName - 'request', 'response', 'data', 'error' + * @param {Array} args + * @returns Client instance + */ +proto.trigger = function (eventName, args) { + var listeners = this.listeners + + for (var i = 0; i < listeners.length; i++) { + if (listeners[i][0] === eventName) { + listeners[i][1].apply(this, args) } } + + return this } + +module.exports = function (options) { + return new Client(options) +} + +module.exports.Client = Client diff --git a/package.json b/package.json index 35e4e69..e8ea991 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "make test" }, "repository": { "type": "git", @@ -15,5 +15,9 @@ "license": "ISC", "dependencies": { "isomorphic-fetch": "^2.2.1" + }, + "devDependencies": { + "chai": "^3.5.0", + "graphql": "^0.6.2" } } diff --git a/test/lib/schema.js b/test/lib/schema.js new file mode 100644 index 0000000..77da49b --- /dev/null +++ b/test/lib/schema.js @@ -0,0 +1,36 @@ +const { + GraphQLSchema, + GraphQLObjectType, + GraphQLString +} = require('graphql') + +const data = [ + { id: '1', name: 'Dan' }, + { id: '2', name: 'Marie' }, + { id: '3', name: 'Jessie' } +] + +const userType = new GraphQLObjectType({ + name: 'User', + fields: { + id: { type: GraphQLString }, + name: { type: GraphQLString } + } +}) + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: userType, + args: { + id: { type: GraphQLString } + }, + resolve: (_, args) => data.find((u) => u.id === args.id) + } + } + }) +}) + +module.exports = schema diff --git a/test/lib/server.js b/test/lib/server.js new file mode 100644 index 0000000..991142c --- /dev/null +++ b/test/lib/server.js @@ -0,0 +1,54 @@ +const http = require('http') +const schema = require('./schema') +const { graphql } = require('graphql') + +module.exports = http.createServer((req, res) => { + if (req.url === '/graphql') { + let body = '' + + req.on('data', function (data) { + body += data + }) + + req.on('end', function () { + let query = body + let variables + let operationName + + if (~req.headers['content-type'].indexOf('application/json')) { + try { + const obj = JSON.parse(query) + if (obj.query && typeof obj.query === 'string') { + query = obj.query + } + if (obj.variables !== undefined) { + variables = obj.variables + } + // Name of GraphQL operation to execute. + if (typeof obj.operationName === 'string') { + operationName = obj.operationName + } + } catch (err) { + // do nothing + } + } + + res.writeHead(200, {'content-type': 'text/json'}) + + graphql(schema, query, null, variables, operationName).then((result) => { + let response = result + + if (result.errors) { + res.statusCode = 400 + response = { + errors: result.errors.map(String) + } + } + + res.end(JSON.stringify(response)) + }).catch((e) => { + res.end(JSON.stringify(e)) + }) + }) + } +}) diff --git a/test/specs.js b/test/specs.js new file mode 100644 index 0000000..f40947c --- /dev/null +++ b/test/specs.js @@ -0,0 +1,93 @@ +/* eslint-env mocha */ +const { expect } = require('chai') +const { Client } = require('..') +const server = require('./lib/server') + +const url = 'http://127.0.0.1:3099/graphql' + +describe('GraphQL client', () => { + let client + + before(() => { + server.listen(3099) + }) + + after(() => { + server.close() + }) + + beforeEach(() => { + client = new Client({ url }) + }) + + it('should query', () => { + return client.query('{ user(id: "1") { id } }').then((data) => { + expect(data).to.eql({ data: { user: { id: '1' } } }) + }) + }) + + it('should register "request" and "response" listeners', (done) => { + let counter = 0 + client + .on('request', function (req) { + counter++ + expect(this).to.be.an.instanceof(Client) + expect(req.method).to.equal('POST') + }) + .on('response', (res) => { + expect(counter).to.equal(2) + expect(res.status).to.equal(200) + + res.text().then((text) => { + expect(text).to.eql('{"data":{"user":{"id":"1"}}}') + done() + }) + }) + .on('request', (req) => { // 'request' once again + counter++ + expect(req.method).to.equal('POST') + }) + .query('{ user(id: "1") { id } }') + }) + + it('should register "error" listener', (done) => { + client + .on('response', (res) => { + throw new Error('foo') + }) + .on('error', (e) => { + expect(e.message).to.equal('foo') + done() + }) + .query('{}') + }) + + it('should not throw on GraphQL error in response', (done) => { + const timeout = setTimeout(() => { + done() + }, 1000) + + client.on('error', (e) => { + clearTimeout(timeout) + throw new Error('"error" event triggered') + }) + .query('{}') + }) + + it('should modify req before querying', (done) => { + let client = new Client({ + url, + request: { + method: 'GET', + credentials: 'include' + } + }) + + client.on('request', (req) => { + expect(req.method).to.equal('GET') + expect(req.credentials).to.equal('include') + done() + }) + .query('{}') + }) +}) From 33bd2e34b72a74d07ac20d35c3f3b7d3fb648cf5 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 19:48:19 +0300 Subject: [PATCH 07/20] Updated Makefile --- Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4cfdb2c..10d4330 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,15 @@ +MOCHA_TARGET=test/specs.js + test: - mocha test/specs.js + make testonly && make lint + +testonly: + mocha $(MOCHA_TARGET) + +testonly-watch: + mocha -w $(MOCHA_TARGET) lint: standard . -.PHONY: test lint \ No newline at end of file +.PHONY: test testonly testonly-watch lint \ No newline at end of file From 1e991ae1a065d62324e2d420746181c6a8e67495 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 19:52:21 +0300 Subject: [PATCH 08/20] Added `beforeRequest` hook to `query` --- index.js | 4 +++- test/specs.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 470cd8a..7451645 100644 --- a/index.js +++ b/index.js @@ -18,7 +18,7 @@ var proto = Client.prototype * @param {Object} variables * @returns {Promise} */ -proto.query = function (query, variables) { +proto.query = function (query, variables, beforeRequest) { var headers = new Headers() headers.set('Content-Type', 'application/json') @@ -30,6 +30,8 @@ proto.query = function (query, variables) { })) req.headers || (req.headers = headers) + if (beforeRequest) beforeRequest(req) + return this.fetch(this.options.url, req) } diff --git a/test/specs.js b/test/specs.js index f40947c..763fd5c 100644 --- a/test/specs.js +++ b/test/specs.js @@ -90,4 +90,14 @@ describe('GraphQL client', () => { }) .query('{}') }) + + it('should modify req using `beforeRequest` function', (done) => { + client.on('request', (req) => { + expect(req.method).to.equal('GET') + done() + }) + .query('{}', null, function (req) { + req.method = 'GET' + }) + }) }) From c06f100c79f88af21d4f6c838f52f67505c750d8 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 19:55:20 +0300 Subject: [PATCH 09/20] Do not `catch` errors, because try-catch is better --- index.js | 8 +++----- test/specs.js | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 7451645..65ed15d 100644 --- a/index.js +++ b/index.js @@ -52,19 +52,17 @@ proto.fetch = function (url, req) { }).then(function (data) { self.trigger('data', [data]) return data - }).catch(function (e) { - self.trigger('error', [e]) }) } /** * Register a listener. - * @param {String} eventName - 'request', 'response', 'data', 'error' + * @param {String} eventName - 'request', 'response', 'data' * @param {Function} callback * @returns Client instance */ proto.on = function (eventName, callback) { - var allowedNames = ['request', 'response', 'data', 'error'] + var allowedNames = ['request', 'response', 'data'] if (~allowedNames.indexOf(eventName)) { this.listeners.push([ eventName, callback ]) @@ -75,7 +73,7 @@ proto.on = function (eventName, callback) { /** * Trigger an event. - * @param {String} eventName - 'request', 'response', 'data', 'error' + * @param {String} eventName - 'request', 'response', 'data' * @param {Array} args * @returns Client instance */ diff --git a/test/specs.js b/test/specs.js index 763fd5c..456d632 100644 --- a/test/specs.js +++ b/test/specs.js @@ -50,16 +50,16 @@ describe('GraphQL client', () => { .query('{ user(id: "1") { id } }') }) - it('should register "error" listener', (done) => { + it('should catch "error" initiated by "response" hook', (done) => { client .on('response', (res) => { throw new Error('foo') }) - .on('error', (e) => { + .query('{}') + .catch((e) => { expect(e.message).to.equal('foo') done() }) - .query('{}') }) it('should not throw on GraphQL error in response', (done) => { From e784d1d48afb504f4a871fe3836f24b03acce89a Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 19:58:11 +0300 Subject: [PATCH 10/20] minor fixes --- index.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 65ed15d..8a7ae3c 100644 --- a/index.js +++ b/index.js @@ -5,22 +5,24 @@ function Client (options) { if (!options.url) throw new Error('Missing url parameter') this.options = options + this.url = options.url // A stack of registered listeners this.listeners = [] } -// to decrease file size +// to reduce file size var proto = Client.prototype /** * Send a query and get a Promise - * @param {String} query - * @param {Object} variables + * @param {String} query + * @param {Object} variables + * @param {Function} beforeRequest hook * @returns {Promise} */ proto.query = function (query, variables, beforeRequest) { var headers = new Headers() - headers.set('Content-Type', 'application/json') + headers.set('content-type', 'application/json') var req = this.options.request || {} req.method || (req.method = 'POST') @@ -32,21 +34,20 @@ proto.query = function (query, variables, beforeRequest) { if (beforeRequest) beforeRequest(req) - return this.fetch(this.options.url, req) + return this.fetch(req) } /** * For making requests - * @param {String} url - GraphQL endpoint - * @param {Array} args + * @param {Object} req * @returns Promise */ -proto.fetch = function (url, req) { +proto.fetch = function (req) { var self = this self.trigger('request', [req]) - return fetch(url, req).then(function (res) { + return fetch(self.url, req).then(function (res) { self.trigger('response', [res]) return res.json() }).then(function (data) { From 67fb4c5beef669f2c94959650f6828bca22f32d4 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 20:02:34 +0300 Subject: [PATCH 11/20] readme --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8e940d8..fb9c061 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,22 @@ npm install graphql-client -S ## How To ```javascript var client = require('graphql-client')({ - url: 'http://your-host/graphql', - - // before request hook - onRequest (req) { + url: 'http://your-host/graphql' +}) + // Before request hook + .on('request', (req) => { // Do whatever you want with `req`, e.g. add JWT auth header req.headers.set('Authentication', 'Bearer ' + token) - }, - - // response hook - onResponse (res) { + }) + // On response hook. Access `Response` instance before parsing response body + .on('response', (res) => { ... - } -}) + }) + // After response is parsed as JSON + .on('data', (data) => { + console.log('GraphQL response:', data) + }) + var query = ` query search ($query: String, $from: Int, $limit: Int) { From 9cfe90ad75016a32b6b8b029cf2c34c748b9af4b Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 20:08:51 +0300 Subject: [PATCH 12/20] Missing "files" section in npm package --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index e8ea991..dfc8943 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "scripts": { "test": "make test" }, + "files": [ + "index.js", + "README.md" + ], "repository": { "type": "git", "url": "https://github.com/nordsimon/graphql-client" From cf3376fdd103be802fe95615268c2b35fc6f5841 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 21:15:25 +0300 Subject: [PATCH 13/20] Allow to redefine response through `beforeRequest` and `request` hooks --- index.js | 25 ++++++++++++++++++++----- test/specs.js | 24 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 8a7ae3c..2563309 100644 --- a/index.js +++ b/index.js @@ -32,7 +32,14 @@ proto.query = function (query, variables, beforeRequest) { })) req.headers || (req.headers = headers) - if (beforeRequest) beforeRequest(req) + if (beforeRequest) { + var result = beforeRequest(req) + + // The `beforeRequest` hook may redefine response when returning something + if (typeof result !== 'undefined') { + return Promise.resolve(result) + } + } return this.fetch(req) } @@ -45,7 +52,14 @@ proto.query = function (query, variables, beforeRequest) { proto.fetch = function (req) { var self = this - self.trigger('request', [req]) + var results = self.trigger('request', [req]) + + // The 'request' hook may redefine response when returning something + for (var i = results.length; i--;) { + if (typeof results[i] !== 'undefined') { + return Promise.resolve(results[i]) + } + } return fetch(self.url, req).then(function (res) { self.trigger('response', [res]) @@ -76,18 +90,19 @@ proto.on = function (eventName, callback) { * Trigger an event. * @param {String} eventName - 'request', 'response', 'data' * @param {Array} args - * @returns Client instance + * @returns {Array} array of results received from each listener respectively */ proto.trigger = function (eventName, args) { var listeners = this.listeners + var results = [] for (var i = 0; i < listeners.length; i++) { if (listeners[i][0] === eventName) { - listeners[i][1].apply(this, args) + results.push(listeners[i][1].apply(this, args)) } } - return this + return results } module.exports = function (options) { diff --git a/test/specs.js b/test/specs.js index 456d632..f369134 100644 --- a/test/specs.js +++ b/test/specs.js @@ -96,8 +96,30 @@ describe('GraphQL client', () => { expect(req.method).to.equal('GET') done() }) - .query('{}', null, function (req) { + .query('{}', null, (req) => { req.method = 'GET' }) }) + + it('should redefine response through `beforeRequest` hook', () => { + return client + .query('{}', null, () => 'foo') + .then((r) => expect(r).to.equal('foo')) + }) + + it('should redefine response through `request` hook', () => { + return client + .on('request', () => 'foo') + .query('{}') + .then((r) => expect(r).to.equal('foo')) + }) + + it('should redefine response through latest `request` hook only', () => { + return client + .on('request', () => 'foo') + .on('request', () => 'bar') + .on('request', () => 'baz') + .query('{}') + .then((r) => expect(r).to.equal('baz')) + }) }) From f59d66e1a688d2683381af9a3b0deba182498e24 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 22:00:26 +0300 Subject: [PATCH 14/20] minor --- index.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 2563309..f02628e 100644 --- a/index.js +++ b/index.js @@ -21,10 +21,12 @@ var proto = Client.prototype * @returns {Promise} */ proto.query = function (query, variables, beforeRequest) { + var self = this + var headers = new Headers() headers.set('content-type', 'application/json') - var req = this.options.request || {} + var req = self.options.request || {} req.method || (req.method = 'POST') req.body || (req.body = JSON.stringify({ query: query, @@ -32,16 +34,21 @@ proto.query = function (query, variables, beforeRequest) { })) req.headers || (req.headers = headers) - if (beforeRequest) { - var result = beforeRequest(req) + var result = beforeRequest && beforeRequest(req) + + var results = self.trigger('request', [req]) + results.push(result) - // The `beforeRequest` hook may redefine response when returning something - if (typeof result !== 'undefined') { - return Promise.resolve(result) + // The 'request' or `beforeRequest` hooks may redefine response when + // returning something + for (var i = results.length; i--;) { + if (typeof results[i] !== 'undefined') { + self.trigger('data', [results[i]]) + return Promise.resolve(results[i]) } } - return this.fetch(req) + return self.fetch(req) } /** @@ -52,15 +59,6 @@ proto.query = function (query, variables, beforeRequest) { proto.fetch = function (req) { var self = this - var results = self.trigger('request', [req]) - - // The 'request' hook may redefine response when returning something - for (var i = results.length; i--;) { - if (typeof results[i] !== 'undefined') { - return Promise.resolve(results[i]) - } - } - return fetch(self.url, req).then(function (res) { self.trigger('response', [res]) return res.json() From cf8f681e17abf8ffd23990df971e9cd9aa734395 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 22:26:17 +0300 Subject: [PATCH 15/20] fixed `trigger` args --- index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index f02628e..88fa5dd 100644 --- a/index.js +++ b/index.js @@ -36,14 +36,14 @@ proto.query = function (query, variables, beforeRequest) { var result = beforeRequest && beforeRequest(req) - var results = self.trigger('request', [req]) + var results = self.trigger('request', req) results.push(result) // The 'request' or `beforeRequest` hooks may redefine response when // returning something for (var i = results.length; i--;) { if (typeof results[i] !== 'undefined') { - self.trigger('data', [results[i]]) + self.trigger('data', results[i]) return Promise.resolve(results[i]) } } @@ -60,10 +60,10 @@ proto.fetch = function (req) { var self = this return fetch(self.url, req).then(function (res) { - self.trigger('response', [res]) + self.trigger('response', res) return res.json() }).then(function (data) { - self.trigger('data', [data]) + self.trigger('data', data) return data }) } @@ -87,10 +87,11 @@ proto.on = function (eventName, callback) { /** * Trigger an event. * @param {String} eventName - 'request', 'response', 'data' - * @param {Array} args + * @param {mixed} ...args * @returns {Array} array of results received from each listener respectively */ -proto.trigger = function (eventName, args) { +proto.trigger = function (eventName) { + var args = Array.prototype.slice.call(arguments, 1) var listeners = this.listeners var results = [] From feff0243202fc5724aa49ffb96645f5e0728576a Mon Sep 17 00:00:00 2001 From: ilearnio Date: Wed, 27 Jul 2016 23:01:24 +0300 Subject: [PATCH 16/20] Using Request instead of plain req object --- index.js | 13 ++++++------- test/specs.js | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 88fa5dd..b5c4037 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -/* global fetch, Headers */ +/* global fetch, Request */ require('isomorphic-fetch') function Client (options) { @@ -23,16 +23,15 @@ var proto = Client.prototype proto.query = function (query, variables, beforeRequest) { var self = this - var headers = new Headers() - headers.set('content-type', 'application/json') - - var req = self.options.request || {} + var req = self.options.request || new Request(self.url) req.method || (req.method = 'POST') req.body || (req.body = JSON.stringify({ query: query, variables: variables })) - req.headers || (req.headers = headers) + if (!req.headers.get('content-type')) { + req.headers.set('content-type', 'application/json') + } var result = beforeRequest && beforeRequest(req) @@ -59,7 +58,7 @@ proto.query = function (query, variables, beforeRequest) { proto.fetch = function (req) { var self = this - return fetch(self.url, req).then(function (res) { + return fetch(req).then(function (res) { self.trigger('response', res) return res.json() }).then(function (data) { diff --git a/test/specs.js b/test/specs.js index f369134..f73f173 100644 --- a/test/specs.js +++ b/test/specs.js @@ -114,7 +114,7 @@ describe('GraphQL client', () => { .then((r) => expect(r).to.equal('foo')) }) - it('should redefine response through latest `request` hook only', () => { + it('should redefine response using result from latest `request` hook', () => { return client .on('request', () => 'foo') .on('request', () => 'bar') From 13519c3fb37e2b04305e327d13cfe0131e001033 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Thu, 28 Jul 2016 01:18:25 +0300 Subject: [PATCH 17/20] Fixed broken Request --- README.md | 8 +++++--- index.js | 32 +++++++++++++++++++------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fb9c061..f46c065 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,12 @@ var client = require('graphql-client')({ }) // Before request hook .on('request', (req) => { - // Do whatever you want with `req`, e.g. add JWT auth header - req.headers.set('Authentication', 'Bearer ' + token) + // Do whatever you want with `Request` instance, e.g. add JWT auth header + if (authenticated) { + req.headers.set('Authentication', 'Bearer ' + token) + } }) - // On response hook. Access `Response` instance before parsing response body + // On response hook. Access `Response` instance before parsing it's body .on('response', (res) => { ... }) diff --git a/index.js b/index.js index b5c4037..93c0d2d 100644 --- a/index.js +++ b/index.js @@ -2,12 +2,23 @@ require('isomorphic-fetch') function Client (options) { + var self = this + if (!options.url) throw new Error('Missing url parameter') - this.options = options - this.url = options.url + self.options = options + self.url = options.url + + // Request instance that is used for `fetch`ing + self.request = new Request(self.url, { + method: 'POST' + }) + self.request.headers.set('content-type', 'application/json') + // Ability to override default Request + if (options.request) self.request = options.request + // A stack of registered listeners - this.listeners = [] + self.listeners = [] } // to reduce file size @@ -23,19 +34,14 @@ var proto = Client.prototype proto.query = function (query, variables, beforeRequest) { var self = this - var req = self.options.request || new Request(self.url) - req.method || (req.method = 'POST') - req.body || (req.body = JSON.stringify({ + self.request.body = JSON.stringify({ query: query, variables: variables - })) - if (!req.headers.get('content-type')) { - req.headers.set('content-type', 'application/json') - } + }) - var result = beforeRequest && beforeRequest(req) + var result = beforeRequest && beforeRequest(self.request) - var results = self.trigger('request', req) + var results = self.trigger('request', self.request) results.push(result) // The 'request' or `beforeRequest` hooks may redefine response when @@ -47,7 +53,7 @@ proto.query = function (query, variables, beforeRequest) { } } - return self.fetch(req) + return self.fetch(self.request) } /** From a7c8941d76492b39daef68e4f234ebef01b1b2e6 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Thu, 28 Jul 2016 12:11:24 +0300 Subject: [PATCH 18/20] All hooks now are able to redefine response --- index.js | 48 ++++++++++++++++++++++++++++++++---------------- test/specs.js | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 93c0d2d..fccb7e7 100644 --- a/index.js +++ b/index.js @@ -39,21 +39,27 @@ proto.query = function (query, variables, beforeRequest) { variables: variables }) + // 'beforeRequest' is a top priority per-query hook, it should forcibly + // override response even from other hooks. var result = beforeRequest && beforeRequest(self.request) - var results = self.trigger('request', self.request) - results.push(result) + if (typeof result === 'undefined') { + result = self.emit('request', self.request) - // The 'request' or `beforeRequest` hooks may redefine response when - // returning something - for (var i = results.length; i--;) { - if (typeof results[i] !== 'undefined') { - self.trigger('data', results[i]) - return Promise.resolve(results[i]) + // No 'response' hook here, reserve it for real responses only. + + // 'data' hook is only triggered if there are any data + if (typeof result !== 'undefined') { + var data = self.emit('data', result, true) // `true` for fake data + if (typeof data !== 'undefined') result = data } } - return self.fetch(self.request) + if (typeof result !== 'undefined') { + result = Promise.resolve(result) + } + + return result || self.fetch(self.request) } /** @@ -65,10 +71,16 @@ proto.fetch = function (req) { var self = this return fetch(req).then(function (res) { - self.trigger('response', res) + // 'response' hook can redefine `res` + var _res = self.emit('response', res) + if (typeof _res !== 'undefined') res = _res + return res.json() }).then(function (data) { - self.trigger('data', data) + // 'data' hook can redefine `data` + var _data = self.emit('data', data) + if (typeof _data !== 'undefined') data = _data + return data }) } @@ -90,23 +102,27 @@ proto.on = function (eventName, callback) { } /** - * Trigger an event. + * Emit an event. * @param {String} eventName - 'request', 'response', 'data' * @param {mixed} ...args * @returns {Array} array of results received from each listener respectively */ -proto.trigger = function (eventName) { +proto.emit = function (eventName) { var args = Array.prototype.slice.call(arguments, 1) var listeners = this.listeners - var results = [] + var result + // Triggering listeners and gettings latest result for (var i = 0; i < listeners.length; i++) { if (listeners[i][0] === eventName) { - results.push(listeners[i][1].apply(this, args)) + var r = listeners[i][1].apply(this, args) + if (typeof r !== 'undefined') { + result = r + } } } - return results + return result } module.exports = function (options) { diff --git a/test/specs.js b/test/specs.js index f73f173..bb7630d 100644 --- a/test/specs.js +++ b/test/specs.js @@ -26,7 +26,7 @@ describe('GraphQL client', () => { }) }) - it('should register "request" and "response" listeners', (done) => { + it('should register "request" and "response" hooks', (done) => { let counter = 0 client .on('request', function (req) { @@ -91,7 +91,7 @@ describe('GraphQL client', () => { .query('{}') }) - it('should modify req using `beforeRequest` function', (done) => { + it('should modify req using "beforeRequest" function', (done) => { client.on('request', (req) => { expect(req.method).to.equal('GET') done() @@ -101,20 +101,20 @@ describe('GraphQL client', () => { }) }) - it('should redefine response through `beforeRequest` hook', () => { + it('should redefine response through "beforeRequest" hook', () => { return client .query('{}', null, () => 'foo') .then((r) => expect(r).to.equal('foo')) }) - it('should redefine response through `request` hook', () => { + it('should redefine response using "request" hook', () => { return client .on('request', () => 'foo') .query('{}') .then((r) => expect(r).to.equal('foo')) }) - it('should redefine response using result from latest `request` hook', () => { + it('should redefine response using result from latest "request" hook', () => { return client .on('request', () => 'foo') .on('request', () => 'bar') @@ -122,4 +122,30 @@ describe('GraphQL client', () => { .query('{}') .then((r) => expect(r).to.equal('baz')) }) + + it('should redefine `data` using "data" hook', () => { + return client + .on('data', (data) => data.data) + .query('{ user(id: "1") { id } }') + .then((r) => expect(r).to.eql({ user: { id: '1' } })) + }) + + it('should redefine using both "res" and "data" hooks', () => { + return client + .on('request', () => 'foo') + .on('data', (data) => (data += '-bar')) + .query('{}') + .then((r) => expect(r).to.eql('foo-bar')) + }) + + it('should redefine using "beforeRequest" and skipp other hooks', () => { + return client + // never triggers + .on('request', (req) => 'baz') + // never triggers + .on('data', (data) => 'bar') + // triggers + .query('{}', null, (req) => 'foo') + .then((r) => expect(r).to.eql('foo')) + }) }) From 1a6702e9e4763a817c56a87032e8373520ae3448 Mon Sep 17 00:00:00 2001 From: ilearnio Date: Thu, 28 Jul 2016 16:41:55 +0300 Subject: [PATCH 19/20] Fixed request inheritanse from options --- index.js | 10 ++++------ test/specs.js | 17 ++++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index fccb7e7..1719726 100644 --- a/index.js +++ b/index.js @@ -10,12 +10,10 @@ function Client (options) { self.url = options.url // Request instance that is used for `fetch`ing - self.request = new Request(self.url, { - method: 'POST' - }) - self.request.headers.set('content-type', 'application/json') - // Ability to override default Request - if (options.request) self.request = options.request + self.request = options.request instanceof Request + ? options.request + : new Request(self.url, options.request || { method: 'POST' }) + self.request.headers.append('content-type', 'application/json') // A stack of registered listeners self.listeners = [] diff --git a/test/specs.js b/test/specs.js index bb7630d..4b85af7 100644 --- a/test/specs.js +++ b/test/specs.js @@ -1,4 +1,5 @@ /* eslint-env mocha */ +/* global Request */ const { expect } = require('chai') const { Client } = require('..') const server = require('./lib/server') @@ -74,18 +75,16 @@ describe('GraphQL client', () => { .query('{}') }) - it('should modify req before querying', (done) => { - let client = new Client({ - url, - request: { - method: 'GET', - credentials: 'include' - } - }) + it('should redefine `Request` instance before querying', (done) => { + const request = new Request(url) + request.headers.set('content-length', 3) + + const client = new Client({ url, request }) client.on('request', (req) => { expect(req.method).to.equal('GET') - expect(req.credentials).to.equal('include') + expect(req.headers.get('content-type')).to.be.falsy + expect(req.headers.get('content-length')).to.equal(3) done() }) .query('{}') From a6238347dab7c6526d90a2c8787378f503fd264b Mon Sep 17 00:00:00 2001 From: ilearnio Date: Thu, 28 Jul 2016 18:43:53 +0300 Subject: [PATCH 20/20] Switched back to plain req object (instead of Request) --- index.js | 25 ++++++++++++------------- test/specs.js | 15 +++++++++++---- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 1719726..e8f582b 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -/* global fetch, Request */ +/* global fetch, Headers */ require('isomorphic-fetch') function Client (options) { @@ -9,12 +9,6 @@ function Client (options) { self.options = options self.url = options.url - // Request instance that is used for `fetch`ing - self.request = options.request instanceof Request - ? options.request - : new Request(self.url, options.request || { method: 'POST' }) - self.request.headers.append('content-type', 'application/json') - // A stack of registered listeners self.listeners = [] } @@ -32,17 +26,23 @@ var proto = Client.prototype proto.query = function (query, variables, beforeRequest) { var self = this - self.request.body = JSON.stringify({ + var req = self.options.request || {} + req.method || (req.method = 'POST') + if (!req.headers) { + req.headers = new Headers() + req.headers.set('content-type', 'application/json') + } + req.body = JSON.stringify({ query: query, variables: variables }) // 'beforeRequest' is a top priority per-query hook, it should forcibly // override response even from other hooks. - var result = beforeRequest && beforeRequest(self.request) + var result = beforeRequest && beforeRequest(req) if (typeof result === 'undefined') { - result = self.emit('request', self.request) + result = self.emit('request', req) // No 'response' hook here, reserve it for real responses only. @@ -56,8 +56,7 @@ proto.query = function (query, variables, beforeRequest) { if (typeof result !== 'undefined') { result = Promise.resolve(result) } - - return result || self.fetch(self.request) + return result || self.fetch(req) } /** @@ -68,7 +67,7 @@ proto.query = function (query, variables, beforeRequest) { proto.fetch = function (req) { var self = this - return fetch(req).then(function (res) { + return fetch(self.url, req).then(function (res) { // 'response' hook can redefine `res` var _res = self.emit('response', res) if (typeof _res !== 'undefined') res = _res diff --git a/test/specs.js b/test/specs.js index 4b85af7..4b40c3e 100644 --- a/test/specs.js +++ b/test/specs.js @@ -1,5 +1,5 @@ /* eslint-env mocha */ -/* global Request */ +/* global Headers */ const { expect } = require('chai') const { Client } = require('..') const server = require('./lib/server') @@ -75,14 +75,21 @@ describe('GraphQL client', () => { .query('{}') }) - it('should redefine `Request` instance before querying', (done) => { - const request = new Request(url) - request.headers.set('content-length', 3) + it('should modify request before querying', (done) => { + const headers = new Headers() + headers.set('content-length', 3) + + const request = { + method: 'GET', + credentials: 'include', + headers + } const client = new Client({ url, request }) client.on('request', (req) => { expect(req.method).to.equal('GET') + expect(req.credentials).to.equal('include') expect(req.headers.get('content-type')).to.be.falsy expect(req.headers.get('content-length')).to.equal(3) done()