Skip to content

Commit

Permalink
Batching RFC support (#1662)
Browse files Browse the repository at this point in the history
* Batching support

* Fix graphql-ws support

* Fix graphql-http compat

* Flag for batched queries

* More

* Changeset

* Go

* Go

* Expose MaybeArray utility and use it

* Go
  • Loading branch information
ardatan committed Sep 6, 2022
1 parent 95600e9 commit 098e139
Show file tree
Hide file tree
Showing 20 changed files with 657 additions and 224 deletions.
8 changes: 8 additions & 0 deletions .changeset/nine-brooms-cough.md
@@ -0,0 +1,8 @@
---
'graphql-yoga': minor
---

- Batching RFC support with `batchingLimit` option to enable batching with an exact limit of requests per batch.
- New `onParams` hook that takes a single `GraphQLParams` object
- Changes in `onRequestParse` and `onRequestParseDone` hook
- - Now `onRequestParseDone` receives the exact object that is passed by the request parser so it can be `GraphQLParams` or an array of `GraphQLParams` so use `onParams` if you need to manipulate batched execution params individually.
1 change: 1 addition & 0 deletions examples/graphql-ws/src/app.ts
Expand Up @@ -58,6 +58,7 @@ export function buildApp() {
...ctx,
req: ctx.extra.request,
socket: ctx.extra.socket,
params: msg.payload,
})

const args = {
Expand Down
299 changes: 299 additions & 0 deletions packages/graphql-yoga/__tests__/batching.spec.ts
@@ -0,0 +1,299 @@
import { createSchema } from '../src/schema'
import { createYoga } from '../src/server'

describe('Batching', () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String
bye: String
}
`,
resolvers: {
Query: {
hello: () => 'hello',
bye: () => 'bye',
},
},
})
const yoga = createYoga({
schema,
batching: true,
})
it('should support batching for JSON requests', async () => {
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([{ query: query1 }, { query: query2 }]),
})
const result = await response.json()
expect(result).toEqual([
{ data: { hello: 'hello' } },
{ data: { bye: 'bye' } },
])
})
it('should support batching for multipart requests', async () => {
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const formData = new yoga.fetchAPI.FormData()
formData.append(
'operations',
JSON.stringify([{ query: query1 }, { query: query2 }]),
)
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'multipart/mixed; boundary=boundary',
},
body: formData,
})
const result = await response.json()
expect(result).toEqual([
{ data: { hello: 'hello' } },
{ data: { bye: 'bye' } },
])
})
it('should throw if the default limit is exceeded', async () => {
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
]),
})
expect(response.status).toBe(413)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is limited to 10 operations per request.',
},
],
})
})
it('should not support batching by default', async () => {
const noBatchingYoga = createYoga({
schema,
})
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await noBatchingYoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([{ query: query1 }, { query: query2 }]),
})
expect(response.status).toBe(400)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is not supported.',
},
],
})
})
it('should respect `batching.limit` option', async () => {
const yoga = createYoga({
schema,
batching: {
limit: 2,
},
})
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`

const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([
{ query: query1 },
{ query: query2 },
{ query: query1 },
]),
})

expect(response.status).toBe(413)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is limited to 2 operations per request.',
},
],
})
})
it('should not allow batching if `batching: false`', async () => {
const yoga = createYoga({
schema,
batching: false,
})
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([{ query: query1 }, { query: query2 }]),
})
expect(response.status).toBe(400)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is not supported.',
},
],
})
})
it('should not allow batching if `batching.limit` is 0', async () => {
const yoga = createYoga({
schema,
batching: {
limit: 0,
},
})
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([{ query: query1 }, { query: query2 }]),
})
expect(response.status).toBe(400)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is not supported.',
},
],
})
})
it('should set `batching.limit` to 10 by default', async () => {
const yoga = createYoga({
schema,
batching: {},
})
const query1 = /* GraphQL */ `
query {
hello
}
`
const query2 = /* GraphQL */ `
query {
bye
}
`
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify([
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
{ query: query1 },
{ query: query2 },
]),
})
expect(response.status).toBe(413)
const result = await response.json()
expect(result).toEqual({
errors: [
{
message: 'Batching is limited to 10 operations per request.',
},
],
})
})
})
7 changes: 1 addition & 6 deletions packages/graphql-yoga/src/plugins/requestParser/POSTJson.ts
Expand Up @@ -26,12 +26,7 @@ export async function parsePOSTJsonRequest(
})
}

return {
operationName: requestBody.operationName,
query: requestBody.query,
variables: requestBody.variables,
extensions: requestBody.extensions,
}
return requestBody
} catch (err) {
throw createGraphQLError('POST body sent invalid JSON.', {
extensions: {
Expand Down
Expand Up @@ -46,10 +46,5 @@ export async function parsePOSTMultipartRequest(
}
}

return {
operationName: operations.operationName,
query: operations.query,
variables: operations.variables,
extensions: operations.extensions,
}
return operations
}
Expand Up @@ -97,12 +97,8 @@ export function isValidGraphQLParams(params: unknown): params is GraphQLParams {

export function useCheckGraphQLQueryParams(): Plugin {
return {
onRequestParse() {
return {
onRequestParseDone({ params }) {
checkGraphQLQueryParams(params)
},
}
onParams({ params }) {
checkGraphQLQueryParams(params)
},
}
}
Expand Down
@@ -0,0 +1,36 @@
import { GraphQLError } from 'graphql'
import { Plugin } from '../types'

export function useLimitBatching(limit?: number): Plugin {
return {
onRequestParse() {
return {
onRequestParseDone({ requestParserResult }) {
if (Array.isArray(requestParserResult)) {
if (!limit) {
throw new GraphQLError(`Batching is not supported.`, {
extensions: {
http: {
status: 400,
},
},
})
}
if (requestParserResult.length > limit) {
throw new GraphQLError(
`Batching is limited to ${limit} operations per request.`,
{
extensions: {
http: {
status: 413,
},
},
},
)
}
}
},
}
},
}
}

0 comments on commit 098e139

Please sign in to comment.