Skip to content

Commit a741e98

Browse files
committed
init http-link-dataloader
1 parent e13f3fa commit a741e98

File tree

8 files changed

+258
-154
lines changed

8 files changed

+258
-154
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2017 Graphcool
3+
Copyright (c) 2018 Graphcool
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,107 @@
1-
# batched-graphql-request [![Build Status](https://travis-ci.org/graphcool/batched-graphql-request.svg?branch=master)](https://travis-ci.org/graphcool/graphql-request) [![npm version](https://badge.fury.io/js/graphql-request.svg)](https://badge.fury.io/js/graphql-request) [![Greenkeeper badge](https://badges.greenkeeper.io/graphcool/graphql-request.svg)](https://greenkeeper.io/)
1+
# http-link-dataloader
22

3-
📚📡 Node-only, batched version of the graphql-request library
3+
A Apollo Link that batches requests both in Node and the Browser.
4+
You may ask what's the difference to [apollo-link-batch-http](https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-batch-http).
5+
Instead of having a time-frame/fixed cache size based batching approach like in `apollo-link-batch-http`, this library uses [dataloader](https://github.com/facebook/dataloader) for batching requests. It is a more generic approach just depending on the Node.JS event loop that batches all consecutive queries directly.
6+
The main use-case for this library is the usage from a [`graphql-yoga`](https://github.com/graphcool/graphql-yoga) server using [`prisma-binding`](https://github.com/graphcool/prisma-binding), but it can be used in any environment, even the browser as the latest `dataloader` version also runs in browser environments.
47

5-
## Features
8+
## Usage
69

7-
* Most **simple and lightweight** GraphQL client
8-
* Includes batching and caching based [`dataloader`](https://github.com/facebook/dataloader)
9-
* Promise-based API (works with `async` / `await`)
10-
* Typescript support (Flow coming soon)
10+
```ts
11+
import { BatchedHTTPLink } from 'http-link-dataloader'
1112

12-
## Idea
13-
The idea of this library is to provide query batching and caching for Node.js backends on a per-request basis.
14-
That means, per http request to your Node.js backend, you create a new instance of `BatchedGraphQLClient` which has its
15-
own cache and batching. Sharing a `BatchedGraphQLClient` instance across requests against your webserver is not recommended as that would result
16-
in Memory Leaks with the Cache growing infinitely. The batching and caching is based on [`dataloader`](https://github.com/facebook/dataloader)
13+
const link = new BatchedHTTPLink()
1714

18-
## Install
15+
const token = 'Auth Token'
1916

20-
```sh
21-
npm install batched-graphql-request
17+
const httpLink = new BatchedHttpLink({
18+
uri: `api endpoint`,
19+
headers: { Authorization: `Bearer ${token}` },
20+
})
2221
```
2322

24-
## Usage
25-
The basic usage is exactly the same as you're used to with [`graphql-request`](https://github.com/graphcool/graphql-request)
26-
```js
27-
import { BatchedGraphQLClient } from 'batched-graphql-request'
23+
## Caching behavior
24+
25+
Note that the dataloader cache aggressively caches everything! That means if you don't want to cache anymore, just create a new instance of `BatchedHTTPLink`.
26+
A good fit for this is every incoming HTTP request in a server environment - on each new HTTP request a new `BatchedHTTPLink` instance is created.
27+
28+
## Batching
29+
30+
This library uses array-based batching. Querying 2 queries like this creates the following payload:
31+
32+
```graphql
33+
query {
34+
Item(id: "1") {
35+
id
36+
name
37+
text
38+
}
39+
}
40+
```
2841

29-
const client = new BatchedGraphQLClient(endpoint, { headers: {} })
30-
client.request(query, variables).then(data => console.log(data))
42+
```graphql
43+
query {
44+
Item(id: "2") {
45+
id
46+
name
47+
text
48+
}
49+
}
3150
```
3251

33-
## Examples
52+
Instead of sending 2 separate http requests, it gets combined into one:
3453

35-
### Creating a new Client per request
36-
In this example, we proxy requests that come in the form of [batched array](https://blog.graph.cool/improving-performance-with-apollo-query-batching-66455ea9d8bc).
37-
Instead of sending each request individually to `my-endpoint`, all requests are again batched together and send grouped to
38-
the underlying endpoint, which increases the performance dramatically.
3954
```js
40-
import { BatchedGraphQLClient } from 'batched-graphql-request'
41-
import * as express from 'express'
42-
import * as bodyParser from 'body-parser'
43-
44-
const app = express()
45-
46-
/*
47-
This accepts POST requests to /graphql of this form:
48-
[
49-
{query: "...", variables: {}},
50-
{query: "...", variables: {}},
51-
{query: "...", variables: {}}
55+
;[
56+
{
57+
query: `query {
58+
Item(id: "1") {
59+
id
60+
name
61+
text
62+
}
63+
}`,
64+
},
65+
{
66+
query: `query {
67+
Item(id: "2") {
68+
id
69+
name
70+
text
71+
}
72+
}`,
73+
},
5274
]
53-
*/
54-
55-
app.use(
56-
'/graphql',
57-
bodyParser.json(),
58-
async (req, res) => {
59-
const client = new BatchedGraphQLClient('my-endpoint', {
60-
headers: {
61-
Authorization: 'Bearer my-jwt-token',
62-
},
63-
})
64-
65-
const requests = Array.isArray(req.body) ? req.body : [req.body]
66-
67-
const results = await Promise.all(requests.map(({query, variables}) => client.request(query, variables)))
68-
69-
res.json(results)
70-
}
71-
)
75+
```
7276

73-
app.listen(3000, () =>
74-
console.log('Server running.'),
75-
)
77+
**Note that the GraphQL Server needs to support the array-based batching!**
78+
(Prisma supports this out of the box)
79+
80+
## Even better batching
81+
82+
A batching that would even be faster is alias-based batching. Instead of creating the array described above, it would generate something like this:
83+
84+
```js
85+
{
86+
query: `
87+
query {
88+
item_1: Item(id: "1") {
89+
id
90+
name
91+
text
92+
}
93+
item_2: Item(id: "2") {
94+
id
95+
name
96+
text
97+
}
98+
}`
99+
},
76100
```
77101

78-
To learn more about the usage, please check out [graphql-request](https://github.com/graphcool/graphql-request)
102+
This requires a lot more logic and resolution logic for aliases, but would be a lot faster than the array based batching!
103+
Anyone intersted in working on this is more than welcome to do so!
104+
You can either create an issue or just reach out to us in slack and join our #contributors channel.
79105

80106
## Help & Community [![Slack Status](https://slack.graph.cool/badge.svg)](https://slack.graph.cool)
81107

package.json

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
{
2-
"name": "batched-graphql-request",
3-
"version": "0.4.1",
2+
"name": "http-link-dataloader",
3+
"version": "0.0.0",
44
"main": "dist/src/index.js",
55
"typings": "./dist/src/index.d.ts",
6-
"files": [
7-
"dist"
8-
],
6+
"files": ["dist"],
97
"repository": {
108
"type": "git",
11-
"url": "git+https://github.com/graphcool/batched-graphql-request.git"
9+
"url": "git+https://github.com/graphcool/http-link-dataloader.git"
1210
},
13-
"keywords": [
14-
"graphql",
15-
"request",
16-
"fetch",
17-
"graphql-client",
18-
"apollo"
19-
],
11+
"keywords": ["graphql", "request", "fetch", "graphql-client", "apollo"],
2012
"author": "Tim Suchanek <tim.suchanek@gmail.com>",
2113
"license": "MIT",
2214
"bugs": {
23-
"url": "https://github.com/graphcool/batched-graphql-request/issues"
15+
"url": "https://github.com/graphcool/http-link-dataloader/issues"
2416
},
25-
"homepage": "https://github.com/graphcool/batched-graphql-request",
17+
"homepage": "https://github.com/graphcool/http-link-dataloader",
2618
"scripts": {
2719
"prepublish": "npm run build",
2820
"build": "rm -rf dist && tsc -d",
@@ -39,6 +31,7 @@
3931
"typescript": "^2.7.1"
4032
},
4133
"dependencies": {
34+
"apollo-link": "^1.0.7",
4235
"cross-fetch": "1.1.1",
4336
"dataloader": "^1.4.0"
4437
}

src/BatchedGraphQLClient.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ClientError, Options, Variables } from './types'
2+
import 'cross-fetch/polyfill'
3+
import * as DataLoader from 'dataloader'
4+
5+
export { ClientError } from './types'
6+
7+
export class BatchedGraphQLClient {
8+
public uri: string
9+
public options: Options
10+
private dataloader: DataLoader<string, any>
11+
12+
constructor(uri: string, options?: Options) {
13+
this.uri = uri
14+
this.options = options || {}
15+
this.dataloader = new DataLoader(this.load)
16+
}
17+
18+
async request<T extends any>(
19+
query: string,
20+
variables?: Variables,
21+
operationName?: string,
22+
): Promise<T> {
23+
const body = JSON.stringify({
24+
query,
25+
variables: variables ? variables : undefined,
26+
operationName: operationName ? operationName : undefined,
27+
})
28+
return this.dataloader.load(body)
29+
}
30+
31+
load = async (keys: string[]): Promise<any> => {
32+
const requests = keys.map(k => JSON.parse(k))
33+
const body = JSON.stringify(requests)
34+
35+
const response = await fetch(this.uri, {
36+
method: 'POST',
37+
...this.options,
38+
headers: Object.assign(
39+
{ 'Content-Type': 'application/json' },
40+
this.options.headers,
41+
),
42+
body,
43+
})
44+
45+
const results = await getResults(response)!
46+
47+
const allResultsHaveData =
48+
results.filter(r => r.data).length === results.length
49+
50+
if (response.ok && !results.find(r => r.errors) && allResultsHaveData) {
51+
return results.map(r => r.data)
52+
} else {
53+
const errorIndex = results.findIndex(r => r.errors)
54+
const result = results[errorIndex]
55+
const { query, variables } = requests[errorIndex]
56+
const errorResult =
57+
typeof result === 'string' ? { error: result } : result
58+
throw new ClientError(
59+
{ ...errorResult, status: response.status },
60+
{ query, variables },
61+
)
62+
}
63+
}
64+
}
65+
66+
async function getResults(response: Response): Promise<any> {
67+
const contentType = response.headers.get('Content-Type')
68+
if (contentType && contentType.startsWith('application/json')) {
69+
return await response.json()
70+
} else {
71+
return await response.text()
72+
}
73+
}

src/BatchedHTTPLink.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ApolloLink, Observable, Operation } from 'apollo-link'
2+
import { print } from 'graphql'
3+
import { BatchedGraphQLClient } from './index'
4+
import { Options, HttpOptions } from './types'
5+
6+
export class BatchedHttpLink extends ApolloLink {
7+
constructor(options: HttpOptions) {
8+
super(BatchedHttpLink.createBatchedHttpRequest(options))
9+
}
10+
11+
private static createBatchedHttpRequest(options: HttpOptions) {
12+
const { uri, ...rest } = options
13+
const client = new BatchedGraphQLClient(uri, rest)
14+
15+
return (operation: Operation) =>
16+
new Observable(observer => {
17+
const {
18+
headers,
19+
uri: contextURI,
20+
}: Record<string, any> = operation.getContext()
21+
22+
const { operationName, variables, query } = operation
23+
24+
if (contextURI) {
25+
client.uri = contextURI
26+
}
27+
28+
if (headers) {
29+
client.options = {
30+
...client.options,
31+
headers: {
32+
...client.options.headers,
33+
...headers,
34+
},
35+
}
36+
}
37+
38+
client
39+
.request(print(query), variables, operationName)
40+
.then(response => {
41+
operation.setContext({ response })
42+
observer.next({ data: response })
43+
observer.complete()
44+
return response
45+
})
46+
.catch(err => {
47+
if (err.name === 'AbortError') {
48+
return
49+
}
50+
51+
observer.error(err)
52+
})
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)