Skip to content

Commit

Permalink
chore: finish port
Browse files Browse the repository at this point in the history
  • Loading branch information
bcomnes committed Jun 8, 2020
1 parent eb6210a commit d0524e0
Show file tree
Hide file tree
Showing 12 changed files with 599 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@12core/eslint-config-12core"
}
21 changes: 21 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: tests

on: [push]

jobs:
test:
runs-on: ${{ matrix.os }}

strategy:
matrix:
os: [ubuntu-latest]
node: [12]

steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm i
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.nyc_output
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Fork of the Minimal GraphQL client graphql-request.

- Even simpler than graphql-request! Needlessly duplicated code removed.
- Same Promise-based API (works with `async` / `await`).
- No Typescript. Type annotations via JSDoc.
- No Typescript.
- Actually Isomorphic (works with Node / browsers). Ships a real ESM module, instead of the fake one TS generates.

## Why?
Expand Down Expand Up @@ -311,10 +311,10 @@ main().catch((error) => console.error(error))
- work with Node.js "type": "module".
- further reducing library size (remove unnecessarily duplicated code)
- removing the project overhead of Typescript syntax, Typescript tooling, and Typescript bugs.
- Provide Type annotations via JSDoc.
- Clarify undocumented methods and edge-cases.

Breaking changes include:

- No fake 'default' export. If you use this, switch to importing named exports.
- Imports node-fetch. This might break react native, not sure.

84 changes: 84 additions & 0 deletions cjs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';
const fetch = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('node-fetch'))
const { ClientError } = require('./types.js')

class GraphQLClient {
constructor (url, options = {}) {
this.url = url
this.options = options
}

async rawRequest (query, variables) {
const { headers, ...others } = this.options

const body = JSON.stringify({
query,
variables: variables
})

const response = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body,
...others
})

const result = await getResult(response)

if (response.ok && !result.errors && result.data) {
const { headers, status } = response
return { ...result, headers, status }
} else {
const errorResult = typeof result === 'string' ? { error: result } : result
throw new ClientError(
{ ...errorResult, status: response.status, headers: response.headers },
{ query, variables }
)
}
}

async request (query, variables) {
const { data } = await this.rawRequest(query, variables)
return data
}

setHeaders (headers) {
this.options.headers = headers

return this
}

setHeader (key, value) {
const { headers } = this.options

if (headers) {
// todo what if headers is in nested array form... ?
headers[key] = value
} else {
this.options.headers = { [key]: value }
}
return this
}
}
exports.GraphQLClient = GraphQLClient

function rawRequest (url, query, variables) {
const client = new GraphQLClient(url)
return client.rawRequest(query, variables)
}
exports.rawRequest = rawRequest

function request (url, query, variables) {
const client = new GraphQLClient(url)
return client.request(query, variables)
}
exports.request = request

function getResult (response) {
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.startsWith('application/json')) {
return response.json()
} else {
return response.text()
}
}
171 changes: 171 additions & 0 deletions cjs/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
'use strict';
const tap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('tap'))
const body = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('body-parser'))
const express = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('express'))
const { createServer } = require('http')

const { GraphQLClient, request, rawRequest } = require('./index.js')

tap.afterEach((done, t) => {
// https://stackoverflow.com/questions/10378690/remove-route-mappings-in-nodejs-express/28369539#28369539
ctx.server._router.stack.forEach((item, i) => {
if (item.name === 'mock') ctx.server._router.stack.splice(i, 1)
})

done()
})

const ctx = {}
tap.test('set up test server', t => {
ctx.server = express()
ctx.server.use(body.json())
ctx.nodeServer = createServer()
ctx.nodeServer.listen({ port: 3210 })
ctx.url = 'http://localhost:3210'
ctx.nodeServer.on('request', ctx.server)
ctx.nodeServer.once('listening', t.end)
ctx.mock = (spec) => {
const requests = []
ctx.server.use('*', function mock (req, res) {
requests.push({
method: req.method,
headers: req.headers,
body: req.body
})
if (spec.headers) {
Object.entries(spec.headers).forEach(([name, value]) => {
res.setHeader(name, value)
})
}
res.send(spec.body ?? {})
})
return { ...spec, requests }
}
})

tap.test('export contract', async t => {
t.ok(GraphQLClient)
t.ok(request)
t.ok(rawRequest)

const client = new GraphQLClient('https://example.com/graphql')

t.ok(client)
})

tap.test('minimal query', async t => {
console.log()
const data = ctx.mock({
body: {
data: {
viewer: {
id: 'some-id'
}
}
}
}).body.data

const results = await request(ctx.url, '{ viewer { id } }')
t.deepEqual(results, data)
})

tap.test('minimal raw query', async t => {
const { extensions, data } = ctx.mock({
body: {
data: {
viewer: {
id: 'some-id'
}
},
extensions: {
version: '1'
}
}
}).body
const { headers, ...result } = await rawRequest(ctx.url, '{ viewer { id } }')

t.deepEqual(result, { data, extensions, status: 200 })
})

tap.test('minimal raw query with response headers', async t => {
const {
headers: reqHeaders,
body: { data, extensions }
} = ctx.mock({
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'test-custom-header'
},
body: {
data: {
viewer: {
id: 'some-id'
}
},
extensions: {
version: '1'
}
}
})
const { headers, ...result } = await rawRequest(ctx.url, '{ viewer { id } }')

t.deepEqual(result, { data, extensions, status: 200 })
t.deepEqual(headers.get('X-Custom-Header'), reqHeaders['X-Custom-Header'])
})

tap.test('content-type with charset', async t => {
const { data } = ctx.mock({
// headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: {
data: {
viewer: {
id: 'some-id'
}
}
}
}).body
const results = await request(ctx.url, '{ viewer { id } }')
t.deepEqual(results, data)
})

tap.test('basic error', async t => {
ctx.mock({
body: {
errors: {
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n',
locations: [
{
line: 1,
column: 1
}
]
}
}
})

const res = await request(ctx.url, 'x').catch((x) => x)

t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
})

tap.test('basic error with raw request', async t => {
ctx.mock({
body: {
errors: {
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n',
locations: [
{
line: 1,
column: 1
}
]
}
}
})
const res = await rawRequest(ctx.url, 'x').catch((x) => x)
t.deepEqual(res.message, 'GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}')
})

tap.test('shut down test server', t => {
ctx.nodeServer.close(t.end)
})
1 change: 1 addition & 0 deletions cjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"commonjs"}
30 changes: 30 additions & 0 deletions cjs/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';
class ClientError extends Error {
constructor (response, request) {
const message = `${extractMessage(response)}: ${JSON.stringify({
response,
request
})}`

super(message)

Object.setPrototypeOf(this, ClientError.prototype)

this.response = response
this.request = request

// this is needed as Safari doesn't support .captureStackTrace
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, ClientError)
}
}
}
exports.ClientError = ClientError

function extractMessage (response) {
try {
return response.errors[0].message
} catch (e) {
return `GraphQL Error (Code: ${response.status})`
}
}
Loading

0 comments on commit d0524e0

Please sign in to comment.