Skip to content

feat: @defer support #898

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

Open
wants to merge 20 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cee50b7
added @defer support for requests with multipart/mixed; deferSpec=202…
Oct 24, 2022
4ad38b4
reverted mercurius version bump, moved @defer tests to their own file
Oct 26, 2022
08ee6d2
remove 'Mercurius' from the error message about wrong accept header, …
Oct 26, 2022
35fdb02
bump graphql to 17.0.0-alpha.2
Oct 26, 2022
d41fff6
moved missing multipart accept header error into errors.js
Oct 26, 2022
274c5bc
added opts.defer: boolean to enable @defer directive
Oct 26, 2022
3e3ca77
use @fastify/accepts instead of Negotiator package
Oct 26, 2022
2b3c25a
add @defer test with undici.fetch
Oct 26, 2022
369457e
Add space between merged SDLs to fix merging errors (#899)
Igloczek Nov 2, 2022
780c668
Explicitly say in the docs that JIT is disabled by default (#901)
igrlk Nov 2, 2022
b4d70fc
feat: add types for object in graphiql configuration (#907)
codeflyer Nov 2, 2022
e27e5b0
Prevent parsing schema exceptions when importing directives (#900)
Igloczek Nov 2, 2022
6933420
Bumped v11.3.0
mcollina Nov 2, 2022
d1d8485
Removed canUseIncrementalExecution check
Nov 2, 2022
2d599a7
Merge branch 'mercurius-js:master' into defer-directive-support
igrlk Nov 2, 2022
ef73394
Merge branch 'defer-directive-support' of github.com:igrlk/mercurius …
Nov 2, 2022
640300f
Throw an error if JIT is used together with defer, update the docs
Nov 2, 2022
8b7e9b2
Throw an error if JIT is used together with defer, update the docs
Nov 2, 2022
7a84f17
Merge branch 'defer-directive-support' of github.com:igrlk/mercurius …
Nov 2, 2022
093d0df
Merge branch 'next' into defer-directive-support
Nov 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 96 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const {
extendSchema,
validate,
validateSchema,
specifiedRules,
execute
specifiedRules
} = require('graphql')
const { buildExecutionContext } = require('graphql/execution/execute')
const { Readable } = require('stream')
const queryDepth = require('./lib/queryDepth')
const buildFederationSchema = require('./lib/federation')
const { initGateway } = require('./lib/gateway')
Expand All @@ -37,7 +37,8 @@ const {
MER_ERR_GQL_VALIDATION,
MER_ERR_INVALID_OPTS,
MER_ERR_METHOD_NOT_ALLOWED,
MER_ERR_INVALID_METHOD
MER_ERR_INVALID_METHOD,
MER_ERR_INVALID_MULTIPART_ACCEPT_HEADER
} = require('./lib/errors')
const { Hooks, assignLifeCycleHooksToContext } = require('./lib/hooks')
const { kLoaders, kFactory, kSubscriptionFactory, kHooks } = require('./lib/symbols')
Expand All @@ -47,6 +48,7 @@ const {
preExecutionHandler,
onResolutionHandler
} = require('./lib/handlers')
const { executeGraphql, MEDIA_TYPES } = require('./lib/util')

// Required for module bundlers
// istanbul ignore next
Expand Down Expand Up @@ -187,6 +189,20 @@ const plugin = fp(async function (app, opts) {
})
}

if (opts.defer) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you throw if jit is enabled with defer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an error throwing when both jit and defer are enabled & updated the docs to have defer option there and in typescript definitions

app.register(require('@fastify/accepts'))

schema = extendSchema(
schema,
parse(`
directive @defer(
if: Boolean! = true
label: String
) on FRAGMENT_SPREAD | INLINE_FRAGMENT
`)
)
}

fastifyGraphQl.schema = schema

app.addHook('onReady', async function () {
Expand Down Expand Up @@ -546,7 +562,7 @@ const plugin = fp(async function (app, opts) {
return maybeFormatErrors(execution, context)
}

const execution = await execute({
const execution = await executeGraphql({
schema: modifiedSchema || fastifyGraphQl.schema,
document: modifiedDocument || document,
rootValue: root,
Expand All @@ -555,9 +571,85 @@ const plugin = fp(async function (app, opts) {
operationName
})

/* istanbul ignore next */
if (execution.initialResult) {
const accept = reply.request.accepts() // Accepts object

if (
!(
accept.negotiator.mediaType([
// mediaType() will return the first one that matches, so if the client
// doesn't include the deferSpec parameter it will match this one here,
// which isn't good enough.
MEDIA_TYPES.MULTIPART_MIXED_NO_DEFER_SPEC,
MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL
]) === MEDIA_TYPES.MULTIPART_MIXED_EXPERIMENTAL
)
) {
// The client ran an operation that would yield multiple parts, but didn't
// specify `accept: multipart/mixed`. We return an error.
throw new MER_ERR_INVALID_MULTIPART_ACCEPT_HEADER()
}

reply.header('content-type', 'multipart/mixed; boundary="-"; deferSpec=20220824')

return Readable.from(
writeMultipartBody(
execution.initialResult,
execution.subsequentResults
)
)
}

return maybeFormatErrors(execution, context)
}

/* istanbul ignore next */
async function * writeMultipartBody (initialResult, subsequentResults) {
yield `\r\n---\r\ncontent-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify(
orderInitialIncrementalExecutionResultFields(initialResult)
)}\r\n---${initialResult.hasNext ? '' : '--'}\r\n`

for await (const result of subsequentResults) {
yield `content-type: application/json; charset=utf-8\r\n\r\n${JSON.stringify(
orderSubsequentIncrementalExecutionResultFields(result)
)}\r\n---${result.hasNext ? '' : '--'}\r\n`
}
}

/* istanbul ignore next */
function orderInitialIncrementalExecutionResultFields (result) {
return {
hasNext: result.hasNext,
errors: result.errors,
data: result.data,
incremental: orderIncrementalResultFields(result.incremental),
extensions: result.extensions
}
}

/* istanbul ignore next */
function orderSubsequentIncrementalExecutionResultFields (result) {
return {
hasNext: result.hasNext,
incremental: orderIncrementalResultFields(result.incremental),
extensions: result.extensions
}
}

/* istanbul ignore next */
function orderIncrementalResultFields (incremental) {
return incremental?.map((i) => ({
hasNext: i.hasNext,
errors: i.errors,
path: i.path,
label: i.label,
data: i.data,
items: i.items,
extensions: i.extensions
}))
}

async function maybeFormatErrors (execution, context) {
execution = addErrorsToExecutionResult(execution, context.errors)

Expand Down
18 changes: 12 additions & 6 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@ function toGraphQLError (err) {

const gqlError = new GraphQLError(
err.message,
err.nodes,
err.source,
err.positions,
err.path,
err,
err.extensions
{
nodes: err.nodes,
source: err.source,
positions: err.positions,
path: err.path,
originalError: err,
extensions: err.extensions
}
)

gqlError.locations = err.locations
Expand Down Expand Up @@ -137,6 +139,10 @@ const errors = {
'Method not allowed',
405
),
MER_ERR_INVALID_MULTIPART_ACCEPT_HEADER: createError(
'MER_ERR_INVALID_MULTIPART_ACCEPT_HEADER',
'Server received an operation that uses incremental delivery (@defer or @stream), but the client does not accept multipart/mixed HTTP responses. To enable incremental delivery support, add the HTTP header "Accept: multipart/mixed; deferSpec=20220824".'
),
/**
* General graphql errors
*/
Expand Down
24 changes: 23 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict'

const { execute } = require('graphql')
const { experimentalExecuteIncrementally } = require('graphql/execution')

function hasDirective (directiveName, node) {
if (!node.directives || node.directives.length < 1) {
return false
Expand All @@ -23,7 +26,26 @@ function hasExtensionDirective (node) {
}
}

const canUseIncrementalExecution = !!require('graphql/execution').experimentalExecuteIncrementally
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed, this is always true now.


// istanbul ignore next
function executeGraphql (args) {
if (canUseIncrementalExecution) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should happen only if defer is enabled.

return experimentalExecuteIncrementally(args)
}

return execute(args)
}

const MEDIA_TYPES = {
MULTIPART_MIXED_NO_DEFER_SPEC: 'multipart/mixed',
MULTIPART_MIXED_EXPERIMENTAL: 'multipart/mixed; deferSpec=20220824'
}

module.exports = {
hasDirective,
hasExtensionDirective
hasExtensionDirective,
canUseIncrementalExecution,
executeGraphql,
MEDIA_TYPES
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"homepage": "https://mercurius.dev",
"peerDependencies": {
"graphql": "^16.0.0"
"graphql": "^17.0.0-alpha.2"
},
"devDependencies": {
"@graphql-tools/merge": "^8.0.0",
Expand Down Expand Up @@ -56,12 +56,13 @@
"wait-on": "^6.0.0"
},
"dependencies": {
"@fastify/accepts": "^4.0.1",
"@fastify/error": "^3.0.0",
"@fastify/static": "^6.0.0",
"@fastify/websocket": "^7.0.0",
"events.on": "^1.0.1",
"fastify-plugin": "^4.2.0",
"graphql": "^16.0.0",
"graphql": "^17.0.0-alpha.2",
"graphql-jit": "^0.7.3",
"mqemitter": "^5.0.0",
"p-map": "^4.0.0",
Expand Down
Loading