Skip to content

Commit

Permalink
feat: defer/stream plugin (#1997)
Browse files Browse the repository at this point in the history
* feat: defer/stream plugin

* changeset

* adjust deps

* code review

* fix test

* customize validate to avoid running validation rule twice

* Go

* run prettier

Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
saihaj and ardatan committed Nov 2, 2022
1 parent cedde92 commit 8773a27
Show file tree
Hide file tree
Showing 22 changed files with 329 additions and 103 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-lamps-know.md
@@ -0,0 +1,5 @@
---
'graphql-yoga': patch
---

introduce a new plugin for defer and stream instead of making it default in yoga
5 changes: 5 additions & 0 deletions .changeset/eight-eyes-nail.md
@@ -0,0 +1,5 @@
---
'@graphql-yoga/plugin-defer-stream': patch
---

plugin to enable defer stream in yoga
3 changes: 2 additions & 1 deletion examples/defer-stream/package.json
Expand Up @@ -9,7 +9,8 @@
"dependencies": {
"graphql-yoga": "3.0.0-next.9",
"@graphql-yoga/render-graphiql": "3.0.0-next.9",
"graphql": "16.6.0"
"graphql": "16.6.0",
"@graphql-yoga/plugin-defer-stream": "0.0.0"
},
"devDependencies": {
"ts-node": "10.8.1",
Expand Down
2 changes: 2 additions & 0 deletions examples/defer-stream/src/yoga.ts
@@ -1,5 +1,6 @@
import { createSchema, createYoga } from 'graphql-yoga'
import { renderGraphiQL } from '@graphql-yoga/render-graphiql'
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'

const wait = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time))
Expand Down Expand Up @@ -78,6 +79,7 @@ export const yoga = createYoga({
typeDefs,
resolvers,
}),
plugins: [useDeferStream()],
renderGraphiQL,
graphiql: {
defaultQuery: /* GraphQL */ `
Expand Down
10 changes: 3 additions & 7 deletions packages/graphql-yoga/__integration-tests__/browser.spec.ts
Expand Up @@ -17,8 +17,7 @@ import {
import { GraphQLBigInt } from 'graphql-scalars'
import 'json-bigint-patch'
import { AddressInfo } from 'net'
import { GraphQLStreamDirective } from '../src/directives/stream'
import { GraphQLDeferDirective } from '../src/directives/defer'
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'

export function createTestSchema() {
let liveQueryCounter = 0
Expand Down Expand Up @@ -126,11 +125,7 @@ export function createTestSchema() {
},
}),
}),
directives: [
GraphQLLiveDirective,
GraphQLStreamDirective,
GraphQLDeferDirective,
],
directives: [GraphQLLiveDirective],
})
}

Expand All @@ -147,6 +142,7 @@ describe('browser', () => {
useLiveQuery({
liveQueryStore,
}),
useDeferStream(),
],
renderGraphiQL,
})
Expand Down
28 changes: 0 additions & 28 deletions packages/graphql-yoga/src/directives/defer.ts

This file was deleted.

31 changes: 0 additions & 31 deletions packages/graphql-yoga/src/directives/stream.ts

This file was deleted.

14 changes: 0 additions & 14 deletions packages/graphql-yoga/src/schema.ts
Expand Up @@ -10,19 +10,5 @@ export function createSchema<TContext = {}>(
): GraphQLSchemaWithContext<TContext> {
return makeExecutableSchema<TContext & YogaInitialContext>({
...opts,
typeDefs: [
/* GraphQL */ `
directive @defer(
if: Boolean
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @stream(
if: Boolean
label: String
initialCount: Int = 0
) on FIELD
`,
opts.typeDefs,
],
})
}
15 changes: 1 addition & 14 deletions packages/graphql-yoga/src/server.ts
Expand Up @@ -80,10 +80,6 @@ import { useUnhandledRoute } from './plugins/useUnhandledRoute.js'
import { yogaDefaultFormatError } from './utils/yoga-default-format-error.js'
import { useSchema, YogaSchemaDefinition } from './plugins/useSchema.js'
import { useLimitBatching } from './plugins/requestValidation/useLimitBatching.js'
import { OverlappingFieldsCanBeMergedRule } from './validations/overlapping-fields-can-be-merged.js'
import { DeferStreamDirectiveOnRootFieldRule } from './validations/defer-stream-directive-on-root-field.js'
import { DeferStreamDirectiveLabelRule } from './validations/defer-stream-directive-label.js'
import { StreamDirectiveOnListFieldRule } from './validations/stream-directive-on-list-field.js'

/**
* Configuration options for the server
Expand Down Expand Up @@ -282,16 +278,7 @@ export class YogaServer<
validate,
execute: normalizedExecutor,
subscribe: normalizedExecutor,
specifiedRules: [
...specifiedRules.filter(
// We do not want to use the default one cause it does not account for `@defer` and `@stream`
({ name }) => !['OverlappingFieldsCanBeMergedRule'].includes(name),
),
OverlappingFieldsCanBeMergedRule,
DeferStreamDirectiveOnRootFieldRule,
DeferStreamDirectiveLabelRule,
StreamDirectiveOnListFieldRule,
],
specifiedRules,
}),
// Use the schema provided by the user
!!options?.schema && useSchema(options.schema),
Expand Down
174 changes: 174 additions & 0 deletions packages/plugins/defer-stream/__tests__/defer-stream.spec.ts
@@ -0,0 +1,174 @@
import { createYoga } from 'graphql-yoga'
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'
import {
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
} from 'graphql'

const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'hello',
},
goodbye: {
type: GraphQLString,
resolve: () =>
new Promise((resolve) => setTimeout(() => resolve('goodbye'), 1000)),
},
stream: {
type: new GraphQLList(GraphQLString),
async *resolve() {
yield 'A'
await new Promise((resolve) => setTimeout(resolve, 1000))
yield 'B'
await new Promise((resolve) => setTimeout(resolve, 1000))
yield 'C'
},
},
},
}),
})

describe('Defer/Stream', () => {
it('should error on defer directive usage when plugin is not used', async () => {
const yoga = createYoga({
schema,
})
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ query: '{ ... @defer { goodbye } }' }),
})

const body = await response.json()
expect(body.errors).toBeDefined()
expect(body.errors[0].message).toMatchInlineSnapshot(
`"Unknown directive "@defer"."`,
)
})

it('should error on stream directive usage when plugin is not used', async () => {
const yoga = createYoga({
schema,
})
const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ query: '{ stream @stream }' }),
})

const body = await response.json()
expect(body.errors).toBeDefined()
expect(body.errors[0].message).toMatchInlineSnapshot(
`"Unknown directive "@stream"."`,
)
})

it('should execute on defer directive', async () => {
const yoga = createYoga({
schema,
plugins: [useDeferStream()],
})

const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({ query: '{ ... @defer { goodbye } }' }),
})
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toBe('text/event-stream')
const chunks: string[] = []
for await (const chunk of response.body!) {
chunks.push(chunk.toString())
}

expect(chunks.length).toBe(2)
expect(JSON.parse(chunks[0].replace('data:', ''))).toMatchInlineSnapshot(`
{
"data": {},
"hasNext": true,
}
`)
expect(JSON.parse(chunks[1].replace('data:', ''))).toMatchInlineSnapshot(`
{
"hasNext": false,
"incremental": [
{
"data": {
"goodbye": "goodbye",
},
"path": [],
},
],
}
`)
})

it('should execute on stream directive', async () => {
const yoga = createYoga({
schema,
plugins: [useDeferStream()],
})

const response = await yoga.fetch('http://yoga/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({ query: '{ stream @stream(initialCount: 2) }' }),
})

expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toBe('text/event-stream')
const chunks: string[] = []
for await (const chunk of response.body!) {
chunks.push(chunk.toString())
}

expect(chunks.length).toBe(3)
expect(chunks.map((c) => JSON.parse(c.replace('data:', ''))))
.toMatchInlineSnapshot(`
[
{
"data": {
"stream": [
"A",
"B",
],
},
"hasNext": true,
},
{
"hasNext": true,
"incremental": [
{
"items": [
"C",
],
"path": [
"stream",
2,
],
},
],
},
{
"hasNext": false,
},
]
`)
})
})

0 comments on commit 8773a27

Please sign in to comment.