Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
MOCHA_TARGET=test/specs.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove Makefile
use npm scripts
https://www.keithcirkel.co.uk/how-to-use-npm-as-a-build-tool/


test:
make testonly && make lint

testonly:
mocha $(MOCHA_TARGET)

testonly-watch:
mocha -w $(MOCHA_TARGET)

lint:
standard .

.PHONY: test testonly testonly-watch lint
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,23 @@ 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
.on('request', (req) => {
// Do whatever you want with `req`, e.g. add JWT auth header
req.headers.set('Authentication', 'Bearer ' + token)
})
// 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) {
Expand Down
145 changes: 98 additions & 47 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,112 @@
function highlightQuery (query, errors) {
var locations = errors.map(function (e) { return e.locations })
.reduce(function (a, b) {
return a.concat(b)
}, [])
/* global fetch, Headers */
require('isomorphic-fetch')

var queryHighlight = ''
function Client (options) {
if (!options.url) throw new Error('Missing url parameter')

query.split('\n').forEach(function (row, index) {
var line = index + 1
var lineErrors = locations.filter(function (loc) { return loc.line === line })
this.options = options
this.url = options.url
// A stack of registered listeners
this.listeners = []
}

// to reduce file size
var proto = Client.prototype

/**
* Send a query and get a Promise
* @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')

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)

if (beforeRequest) {
var result = beforeRequest(req)

queryHighlight += row + '\n'
// The `beforeRequest` hook may redefine response when returning something
if (typeof result !== 'undefined') {
return Promise.resolve(result)
}
}

if (lineErrors.length) {
var errorHighlight = []
return this.fetch(req)
}

lineErrors.forEach(function (line) {
for (var i = 0; i < 8; i++) {
errorHighlight[line.column + i] = '~'
}
})
/**
* For making requests
* @param {Object} req
* @returns Promise
*/
proto.fetch = function (req) {
var self = this

for (var i = 0; i < errorHighlight.length; i++) {
queryHighlight += errorHighlight[i] || ' '
}
queryHighlight += '\n'
var results = self.trigger('request', [req])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass args as args. and handle it in the trigger function instead so we use the same syntax as an ordinary event emitter

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that in jQuery they pass an array of arguments instead of passing args as args. But maybe you are right, let's do this the Node way. I fixed this


// The 'request' hook may redefine response when returning something
for (var i = results.length; i--;) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure i like this part very much, it feels odd that a return from a trigger should change any execution. I can accept that you can mutate the req object but the return value should not be handled. What are you aiming for with this ?

Copy link
Author

@ilearnio ilearnio Jul 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I happened to need such a feature in a previous graphql client (called "lokka") that I used to use. I wanted to setup caching of non-mutating requests, but I already do so in my application level. So I personally don't need this feature at the moment, just felt it needs to be there, might come in use some day. Yet the module becomes full-featured, and now can be even extended by external plugins/modules (using hooks). I updated this code a bit

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()
}).then(function (data) {
self.trigger('data', [data])
return data
})
}

/**
* Register a listener.
* @param {String} eventName - 'request', 'response', 'data'
* @param {Function} callback
* @returns Client instance
*/
proto.on = function (eventName, callback) {
var allowedNames = ['request', 'response', 'data']

if (~allowedNames.indexOf(eventName)) {
this.listeners.push([ eventName, callback ])
}

return queryHighlight
return this
}

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')

return fetch(params.url, {
method: 'POST',
body: JSON.stringify({
query: query,
variables: variables
}),
headers: headers,
credentials: params.credentials
}).then(function (res) {
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))
}
return data
})
/**
* Trigger an event.
* @param {String} eventName - 'request', 'response', 'data'
* @param {Array} args
* @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) {
results.push(listeners[i][1].apply(this, args))
}
}

return results
}

module.exports = function (options) {
return new Client(options)
}

module.exports.Client = Client
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "make test"
},
"files": [
"index.js",
"README.md"
],
"repository": {
"type": "git",
"url": "https://github.com/nordsimon/graphql-client"
Expand All @@ -15,5 +19,9 @@
"license": "ISC",
"dependencies": {
"isomorphic-fetch": "^2.2.1"
},
"devDependencies": {
"chai": "^3.5.0",
"graphql": "^0.6.2"
}
}
36 changes: 36 additions & 0 deletions test/lib/schema.js
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions test/lib/server.js
Original file line number Diff line number Diff line change
@@ -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))
})
})
}
})
Loading