Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: defer/stream plugin #1997

Merged
merged 8 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
},
]
`)
})
})