Skip to content

Commit 063dc0b

Browse files
ardatandotansimha
andauthored
Block tracking & Auto Pagination transform (graphprotocol#37)
Co-authored-by: Dotan Simha <[email protected]>
1 parent 52a3f68 commit 063dc0b

19 files changed

+2460
-58
lines changed

.changeset/orange-toys-hope copy.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@graphprotocol/client-auto-pagination': patch
3+
---
4+
5+
#### Auto Pagination Transform
6+
7+
`graph-client` implements automatic pagination using `first:` and `after:` filters of `graph-node`.
8+
9+
At the moment, `graph-node` allow fetching only 1000 records per query. This transfomer allow you to run queries with any limit, and the breaks it automatically to multiple concurrent requests, then merges the responses into a single response.
10+
11+
This feature is implemented in `@graphprotocol/client-auto-pagination` and installed automatically with the `graph-client` CLI package.
12+
13+
### Usage Example
14+
15+
```yaml
16+
# .graphclientrc.yml
17+
sources:
18+
- name: uniswap
19+
handler:
20+
graphql:
21+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
22+
transforms:
23+
- autoPagination:
24+
validateSchema: true # Validates that the schema source actually contains the required input filters.
25+
limitOfRecords: 1000 # Default is 1000, you can change if you indexer has different configuration in GRAPH_GRAPHQL_MAX_FIRST var.
26+
```

.changeset/orange-toys-hope.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@graphprotocol/client-block-tracking': patch
3+
---
4+
5+
#### Block Tracking Transform
6+
7+
`graph-client` implements automatic block tracking using `number_gte` filter of `graph-node`. This automates the process [of fetching and tracking the block number of entites](https://thegraph.com/docs/en/developer/distributed-systems/#polling-for-updated-data).
8+
9+
This feature is implemented in `@graphprotocol/client-block-tracking` and installed automatically with the `graph-client` CLI package.
10+
11+
### Usage Example
12+
13+
```yaml
14+
# .graphclientrc.yml
15+
sources:
16+
- name: uniswap
17+
handler:
18+
graphql:
19+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
20+
transforms: # The following section will make sure to automatically fetch the block information, and then use it for tracking in future queries.
21+
- blockTracking:
22+
validateSchema: true # Validates that the schema source actually contains _meta and input block filters.
23+
```

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
"files.exclude": {
1414
"**/.git": true,
1515
"**/.DS_Store": true,
16-
"**/node_modules": true
16+
"**/node_modules": false
1717
}
1818
}

README.md

+92-12
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ This library is intended to simplify the network aspect of data consumption for
1414

1515
> The tools provided in this repo can be used as standalone, but you can also use it with any existing GraphQL Client!
1616
17-
| Status | Feature | Notes |
18-
| ------ | -------------------------------------- | ------------------------------------------------------- |
19-
|| Multiple indexers | based on fetch strategies |
20-
|| Fetch Strategies | timeout, retry, fallback, race |
21-
|| Build time validations & optimizations | |
22-
|| Client-Side Composition | with improved execution planner (based on GraphQL-Mesh) |
23-
|| Raw Execution (standalone mode) | without a wrapping GraphQL client |
24-
|| Local (client-side) Mutations | |
25-
|| Integration with `@apollo/client` | |
26-
|| Integration with `urql` | |
27-
|| TypeScript support | with built-in GraphQL Codegen and `TypedDocumentNode` |
17+
| Status | Feature | Notes |
18+
| ------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
19+
|| Multiple indexers | based on fetch strategies |
20+
|| Fetch Strategies | timeout, retry, fallback, race |
21+
|| Build time validations & optimizations | |
22+
|| Client-Side Composition | with improved execution planner (based on GraphQL-Mesh) |
23+
|| Raw Execution (standalone mode) | without a wrapping GraphQL client |
24+
|| Local (client-side) Mutations | |
25+
|| [Automatic Block Tracking](./packages/block-tracking/README.md) | tracking block numbers [as described here](https://thegraph.com/docs/en/developer/distributed-systems/#polling-for-updated-data) |
26+
|| [Automatic Pagination](./packages/auto-pagination/README.md) | doing multiple requests in a single call to fetch more than the indexer limit |
27+
|| Integration with `@apollo/client` | |
28+
|| Integration with `urql` | |
29+
|| TypeScript support | with built-in GraphQL Codegen and `TypedDocumentNode` |
2830

2931
> You can find an [extended architecture design here](./docs/architecture.md)
3032
@@ -170,7 +172,7 @@ client.execute(myQuery, myVariables, {
170172

171173
> You can find the [complete documentation for the `graphql` handler here](https://www.graphql-mesh.com/docs/handlers/graphql#config-api-reference).
172174
173-
#### Environment Variables Inteporlation
175+
#### Environment Variables Interpolation
174176

175177
If you wish to use environment variables in your The Graph Client configuration file, you can use interpolation with `env` helper:
176178

@@ -282,6 +284,84 @@ sources:
282284

283285
</details>
284286

287+
#### Block Tracking
288+
289+
The Graph Client can track block numbers and do the following queries by following [this pattern](https://thegraph.com/docs/en/developer/distributed-systems/#polling-for-updated-data) with `blockTracking` transform;
290+
291+
```yaml
292+
sources:
293+
- name: uniswapv2
294+
handler:
295+
graphql:
296+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
297+
transforms:
298+
- blockTracking:
299+
# You might want to disable schema validation for faster startup
300+
validateSchema: true
301+
# Ignore the fields that you don't want to be tracked
302+
ignoreFieldNames: [users, prices]
303+
# Exclude the operation with the following names
304+
ignoreOperationNames: [NotFollowed]
305+
```
306+
307+
[You can try a working example here](./examples/transforms)
308+
309+
#### Automatic Pagination
310+
311+
With most subgraphs, the number of records you can fetch is limited. In this case, you have to send multiple requests with pagination.
312+
313+
```graphql
314+
query {
315+
# Will throw an error if the limit is 1000
316+
users(first: 2000) {
317+
id
318+
name
319+
}
320+
}
321+
```
322+
323+
So you have to send the following operations one after the other:
324+
325+
```graphql
326+
query {
327+
# Will throw an error if the limit is 1000
328+
users(first: 1000) {
329+
id
330+
name
331+
}
332+
}
333+
```
334+
335+
Then after the first response;
336+
337+
```graphql
338+
query {
339+
# Will throw an error if the limit is 1000
340+
users(first: 1000, skip: 1000) {
341+
id
342+
name
343+
}
344+
}
345+
```
346+
347+
After the second response, you have to merge the results manually. But instead The Graph Client allows you to do the first one and automatically does those multiple requests for you under the hood.
348+
349+
All you have to do is;
350+
351+
```yaml
352+
sources:
353+
- name: uniswapv2
354+
handler:
355+
graphql:
356+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
357+
transforms:
358+
- autoPagination:
359+
# You might want to disable schema validation for faster startup
360+
validateSchema: true
361+
```
362+
363+
[You can try a working example here](./examples/transforms)
364+
285365
#### Client-side Composition
286366

287367
The Graph Client has built-in support for client-side GraphQL Composition (powered by [GraphQL-Tools Schema-Stitching](https://www.graphql-tools.com/docs/schema-stitching/stitch-combining-schemas)).

examples/transforms/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.graphclient
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sources:
2+
- name: uniswap
3+
handler:
4+
graphql:
5+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
6+
transforms:
7+
# Enable Automatic Block Tracking
8+
- autoPagination:
9+
validateSchema: true
10+
- blockTracking:
11+
validateSchema: true

examples/transforms/package.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "transforms-example",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"start": "graphclient serve-dev"
7+
},
8+
"dependencies": {
9+
"@graphprotocol/client-cli": "0.0.3",
10+
"graphql": "16.3.0"
11+
}
12+
}

jest.config.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
const { resolve } = require('path')
2+
const { pathsToModuleNameMapper } = require('ts-jest')
23
const CI = !!process.env.CI
34

45
const ROOT_DIR = __dirname
56
const TSCONFIG = resolve(ROOT_DIR, 'tsconfig.json')
67
const tsconfig = require(TSCONFIG)
78

9+
process.env.LC_ALL = 'en_US'
10+
811
module.exports = {
912
testEnvironment: 'node',
1013
rootDir: ROOT_DIR,
1114
restoreMocks: true,
1215
reporters: ['default'],
1316
modulePathIgnorePatterns: ['dist'],
14-
moduleNameMapper: {},
17+
moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: `${ROOT_DIR}/` }),
1518
collectCoverage: true,
1619
cacheDirectory: resolve(ROOT_DIR, `${CI ? '' : 'node_modules/'}.cache/jest`),
17-
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
20+
extensionsToTreatAsEsm: ['.ts'],
21+
coverageThreshold: {
22+
global: {
23+
branches: 80,
24+
functions: 95,
25+
lines: 90,
26+
statements: 90,
27+
},
28+
},
1829
}

package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"prebuild": "rimraf packages/*/dist",
99
"check": "yarn workspaces run check",
1010
"build": "tsc --project tsconfig.build.json && bob build",
11-
"test": "jest --passWithNoTests",
11+
"test": "jest --passWithNoTests --detectLeaks --detectOpenHandles",
1212
"release": "yarn build && changeset publish",
1313
"release:canary": "(node scripts/canary-release.js && yarn build && yarn changeset publish --tag canary) || echo Skipping Canary...",
1414
"fix-bin": "node scripts/fix-bin.js",
@@ -26,7 +26,8 @@
2626
"tools"
2727
],
2828
"contributors": [
29-
"Dotan Simha <[email protected]>"
29+
"Dotan Simha <[email protected]>",
30+
"Arda Tanrikulu <[email protected]>"
3031
],
3132
"license": "MIT",
3233
"bugs": {
@@ -58,7 +59,9 @@
5859
"prettier": "2.6.2",
5960
"pretty-quick": "3.1.3",
6061
"rimraf": "3.0.2",
61-
"typescript": "4.6.3"
62+
"typescript": "4.6.3",
63+
"weak-napi": "2.0.2",
64+
"ts-jest": "27.1.4"
6265
},
6366
"resolutions": {
6467
"@changesets/apply-release-plan": "6.0.0",

packages/auto-pagination/README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## Automatic Unlimited Pagination
2+
3+
`graph-client` implements automatic pagination using `first:` and `after:` filters of `graph-node`.
4+
5+
At the moment, `graph-node` allow fetching only 1000 records per query. This transfomer allow you to run queries with any limit, and the breaks it automatically to multiple concurrent requests, then merges the responses into a single response.
6+
7+
This feature is implemented in `@graphprotocol/client-auto-pagination` and installed automatically with the `graph-client` CLI package.
8+
9+
### Usage Example
10+
11+
```yaml
12+
# .graphclientrc.yml
13+
sources:
14+
- name: uniswap
15+
handler:
16+
graphql:
17+
endpoint: https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2
18+
transforms:
19+
- autoPagination:
20+
validateSchema: true # Validates that the schema source actually contains the required input filters.
21+
limitOfRecords: 1000 # Default is 1000, you can change if you indexer has different configuration in GRAPH_GRAPHQL_MAX_FIRST var.
22+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { makeExecutableSchema } from '@graphql-tools/schema'
2+
import { wrapSchema } from '@graphql-tools/wrap'
3+
import { execute, graphql, parse } from 'graphql'
4+
import AutoPaginationTransform from '../src'
5+
6+
describe('Auto Pagination', () => {
7+
const users = new Array(20).fill({}).map((_, i) => ({ id: (i + 1).toString(), name: `User ${i + 1}` }))
8+
const LIMIT = 3
9+
const schema = makeExecutableSchema({
10+
typeDefs: /* GraphQL */ `
11+
type Query {
12+
users(first: Int = ${LIMIT}, skip: Int = 0): [User!]!
13+
}
14+
type User {
15+
id: ID!
16+
name: String!
17+
}
18+
`,
19+
resolvers: {
20+
Query: {
21+
users: (_, { first = LIMIT, skip = 0 }) => {
22+
if (first > LIMIT) {
23+
throw new Error(`You cannot request more than ${LIMIT} users; you requested ${first}`)
24+
}
25+
return users.slice(skip, skip + first)
26+
},
27+
},
28+
},
29+
})
30+
const wrappedSchema = wrapSchema({
31+
schema,
32+
transforms: [
33+
new AutoPaginationTransform({
34+
config: {
35+
limitOfRecords: LIMIT,
36+
},
37+
}),
38+
],
39+
})
40+
it('should give correct numbers of results if first arg are higher than given limit', async () => {
41+
const query = /* GraphQL */ `
42+
query {
43+
users(first: 10) {
44+
id
45+
name
46+
}
47+
}
48+
`
49+
const result = await execute({
50+
schema: wrappedSchema,
51+
document: parse(query),
52+
})
53+
expect(result).toEqual({
54+
data: {
55+
users: users.slice(0, 10),
56+
},
57+
})
58+
})
59+
it('should respect skip argument', async () => {
60+
const query = /* GraphQL */ `
61+
query {
62+
users(first: 10, skip: 1) {
63+
id
64+
name
65+
}
66+
}
67+
`
68+
const result = await execute({
69+
schema: wrappedSchema,
70+
document: parse(query),
71+
})
72+
expect(result).toEqual({
73+
data: {
74+
users: users.slice(1, 10),
75+
},
76+
})
77+
})
78+
})

0 commit comments

Comments
 (0)