From cee50b775f00dd436e16d6e943804dfb4e454fd6 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Mon, 24 Oct 2022 09:10:06 +0100 Subject: [PATCH 01/16] added @defer support for requests with multipart/mixed; deferSpec=20220824 accept header --- index.js | 94 ++++++++++++++++++++++++++++- lib/errors.js | 14 +++-- lib/util.js | 24 +++++++- package.json | 3 +- test/directives.js | 145 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 790a8350..08108b0d 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict' +const Negotiator = require('negotiator') const fp = require('fastify-plugin') let LRU = require('tiny-lru') const routes = require('./lib/routes') @@ -18,10 +19,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') @@ -47,6 +48,7 @@ const { preExecutionHandler, onResolutionHandler } = require('./lib/handlers') +const { executeGraphql, MEDIA_TYPES } = require('./lib/util') // Required for module bundlers // istanbul ignore next @@ -546,7 +548,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, @@ -555,9 +557,95 @@ const plugin = fp(async function (app, opts) { operationName }) + /* istanbul ignore next */ + if (execution.initialResult) { + const acceptHeader = reply.request.raw.headers.accept + + if ( + !( + acceptHeader && + new Negotiator({ + headers: { accept: acceptHeader } + }).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 Error( + 'Mercurius 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'.", + // Use 406 Not Accepted TODO + { extensions: { http: { status: 406 } } } + ) + } + + 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) diff --git a/lib/errors.js b/lib/errors.js index b6f7a31b..1cfb5c79 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -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 diff --git a/lib/util.js b/lib/util.js index 8a836203..de348228 100644 --- a/lib/util.js +++ b/lib/util.js @@ -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 @@ -23,7 +26,26 @@ function hasExtensionDirective (node) { } } +const canUseIncrementalExecution = !!require('graphql/execution').experimentalExecuteIncrementally + +// istanbul ignore next +function executeGraphql (args) { + if (canUseIncrementalExecution) { + 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 } diff --git a/package.json b/package.json index 9611f408..bbd1857d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mercurius", - "version": "11.1.0", + "version": "11.2.0", "description": "Fastify GraphQL adapter with gateway and subscription support", "main": "index.js", "types": "index.d.ts", @@ -64,6 +64,7 @@ "graphql": "^16.0.0", "graphql-jit": "^0.7.3", "mqemitter": "^5.0.0", + "negotiator": "^0.6.3", "p-map": "^4.0.0", "readable-stream": "^4.0.0", "safe-stable-stringify": "^2.3.0", diff --git a/test/directives.js b/test/directives.js index 245d9b7f..c23bd923 100644 --- a/test/directives.js +++ b/test/directives.js @@ -14,6 +14,7 @@ const { getResolversFromSchema } = require('@graphql-tools/utils') const buildFederationSchema = require('../lib/federation') +const { canUseIncrementalExecution } = require('../lib/util') class ValidationError extends Error { constructor (message, extensions) { @@ -962,3 +963,147 @@ type User { } }) }) + +if (canUseIncrementalExecution) { + const schema = ` + directive @defer( + if: Boolean! = true + label: String + ) on FRAGMENT_SPREAD | INLINE_FRAGMENT + + type Query { + allProducts: [Product!]! + } + + type Product { + delivery: DeliveryEstimates! + sku: String! + id: ID! + } + + type DeliveryEstimates { + estimatedDelivery: String! + fastestDelivery: String! + }` + + const allProducts = new Array(1).fill(0).map((_, index) => ({ + id: `${index}`, + sku: 'sku' + })) + + const resolvers = { + Query: { + allProducts: () => { + return allProducts + } + }, + Product: { + delivery: () => { + return { + estimatedDelivery: '25.01.2000', + fastestDelivery: '25.01.2000' + } + } + } + } + + const query = ` + query deferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + ` + + test('errors with @defer when used with wrong "accept" header', async t => { + const wrongAcceptValues = [ + '', + 'application/json', + 'multipart/mixed', + 'multipart/mixed; deferSpec=12345' + ] + + for (const accept of wrongAcceptValues) { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) + + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json', + accept + }, + body: JSON.stringify({ query }) + }) + + t.match(res, { + statusCode: 200, + body: JSON.stringify({ + data: null, + errors: [{ + message: "Mercurius 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'." + }] + }) + }) + + await app.close() + } + + t.end() + }) + + test('works with @defer when used with correct "accept" header', async t => { + const correctAcceptValues = [ + 'multipart/mixed; deferSpec=20220824', + 'multipart/mixed; deferSpec=20220824, application/json', + 'application/json, multipart/mixed; deferSpec=20220824' + ] + + for (const accept of correctAcceptValues) { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) + + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json', + accept + }, + body: JSON.stringify({ query }) + }) + + t.match(res, { + statusCode: 200, + headers: { + 'content-type': 'multipart/mixed; boundary="-"; deferSpec=20220824' + }, + body: `\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"allProducts":[{"delivery":{},"sku":"sku","id":"0"}]}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"incremental":[{"path":["allProducts",0,"delivery"],"data":{"estimatedDelivery":"25.01.2000","fastestDelivery":"25.01.2000"}}]}\r +-----\r +` + }) + + await app.close() + } + + t.end() + }) +} From 4ad38b468bdc1d632190bf96c35fb2be493165f4 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 10:02:59 +0100 Subject: [PATCH 02/16] reverted mercurius version bump, moved @defer tests to their own file --- package.json | 2 +- test/defer.js | 146 +++++++++++++++++++++++++++++++++++++++++++++ test/directives.js | 145 -------------------------------------------- 3 files changed, 147 insertions(+), 146 deletions(-) create mode 100644 test/defer.js diff --git a/package.json b/package.json index bbd1857d..36f17bba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mercurius", - "version": "11.2.0", + "version": "11.1.0", "description": "Fastify GraphQL adapter with gateway and subscription support", "main": "index.js", "types": "index.d.ts", diff --git a/test/defer.js b/test/defer.js new file mode 100644 index 00000000..9e2e3e46 --- /dev/null +++ b/test/defer.js @@ -0,0 +1,146 @@ +const { test } = require('tap') +const Fastify = require('fastify') +const mercurius = require('../index') +const { canUseIncrementalExecution } = require('../lib/util') + +if (canUseIncrementalExecution) { + const schema = ` + directive @defer( + if: Boolean! = true + label: String + ) on FRAGMENT_SPREAD | INLINE_FRAGMENT + + type Query { + allProducts: [Product!]! + } + + type Product { + delivery: DeliveryEstimates! + sku: String! + id: ID! + } + + type DeliveryEstimates { + estimatedDelivery: String! + fastestDelivery: String! + }` + + const allProducts = new Array(1).fill(0).map((_, index) => ({ + id: `${index}`, + sku: 'sku' + })) + + const resolvers = { + Query: { + allProducts: () => { + return allProducts + } + }, + Product: { + delivery: () => { + return { + estimatedDelivery: '25.01.2000', + fastestDelivery: '25.01.2000' + } + } + } + } + + const query = ` + query deferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + ` + + const wrongAcceptValues = [ + '', + 'application/json', + 'multipart/mixed', + 'multipart/mixed; deferSpec=12345' + ] + + for (const accept of wrongAcceptValues) { + test('errors with @defer when used with wrong "accept" header', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) + + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json', + accept + }, + body: JSON.stringify({ query }) + }) + + t.match(res, { + statusCode: 200, + body: JSON.stringify({ + data: null, + errors: [{ + message: "Mercurius 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'." + }] + }) + }) + + await app.close() + t.end() + }) + } + + const correctAcceptValues = [ + 'multipart/mixed; deferSpec=20220824', + 'multipart/mixed; deferSpec=20220824, application/json', + 'application/json, multipart/mixed; deferSpec=20220824' + ] + + for (const accept of correctAcceptValues) { + test('works with @defer when used with correct "accept" header', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) + + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json', + accept + }, + body: JSON.stringify({ query }) + }) + + t.match(res, { + statusCode: 200, + headers: { + 'content-type': 'multipart/mixed; boundary="-"; deferSpec=20220824' + }, + body: `\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"allProducts":[{"delivery":{},"sku":"sku","id":"0"}]}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"incremental":[{"path":["allProducts",0,"delivery"],"data":{"estimatedDelivery":"25.01.2000","fastestDelivery":"25.01.2000"}}]}\r +-----\r +` + }) + + await app.close() + t.end() + }) + } +} diff --git a/test/directives.js b/test/directives.js index c23bd923..245d9b7f 100644 --- a/test/directives.js +++ b/test/directives.js @@ -14,7 +14,6 @@ const { getResolversFromSchema } = require('@graphql-tools/utils') const buildFederationSchema = require('../lib/federation') -const { canUseIncrementalExecution } = require('../lib/util') class ValidationError extends Error { constructor (message, extensions) { @@ -963,147 +962,3 @@ type User { } }) }) - -if (canUseIncrementalExecution) { - const schema = ` - directive @defer( - if: Boolean! = true - label: String - ) on FRAGMENT_SPREAD | INLINE_FRAGMENT - - type Query { - allProducts: [Product!]! - } - - type Product { - delivery: DeliveryEstimates! - sku: String! - id: ID! - } - - type DeliveryEstimates { - estimatedDelivery: String! - fastestDelivery: String! - }` - - const allProducts = new Array(1).fill(0).map((_, index) => ({ - id: `${index}`, - sku: 'sku' - })) - - const resolvers = { - Query: { - allProducts: () => { - return allProducts - } - }, - Product: { - delivery: () => { - return { - estimatedDelivery: '25.01.2000', - fastestDelivery: '25.01.2000' - } - } - } - } - - const query = ` - query deferVariation { - allProducts { - delivery { - ...MyFragment @defer - } - sku - id - } - } - - fragment MyFragment on DeliveryEstimates { - estimatedDelivery - fastestDelivery - } - ` - - test('errors with @defer when used with wrong "accept" header', async t => { - const wrongAcceptValues = [ - '', - 'application/json', - 'multipart/mixed', - 'multipart/mixed; deferSpec=12345' - ] - - for (const accept of wrongAcceptValues) { - const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true }) - - const res = await app.inject({ - method: 'POST', - url: '/graphql', - headers: { - 'content-type': 'application/json', - accept - }, - body: JSON.stringify({ query }) - }) - - t.match(res, { - statusCode: 200, - body: JSON.stringify({ - data: null, - errors: [{ - message: "Mercurius 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'." - }] - }) - }) - - await app.close() - } - - t.end() - }) - - test('works with @defer when used with correct "accept" header', async t => { - const correctAcceptValues = [ - 'multipart/mixed; deferSpec=20220824', - 'multipart/mixed; deferSpec=20220824, application/json', - 'application/json, multipart/mixed; deferSpec=20220824' - ] - - for (const accept of correctAcceptValues) { - const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true }) - - const res = await app.inject({ - method: 'POST', - url: '/graphql', - headers: { - 'content-type': 'application/json', - accept - }, - body: JSON.stringify({ query }) - }) - - t.match(res, { - statusCode: 200, - headers: { - 'content-type': 'multipart/mixed; boundary="-"; deferSpec=20220824' - }, - body: `\r ----\r -content-type: application/json; charset=utf-8\r -\r -{"hasNext":true,"data":{"allProducts":[{"delivery":{},"sku":"sku","id":"0"}]}}\r ----\r -content-type: application/json; charset=utf-8\r -\r -{"hasNext":false,"incremental":[{"path":["allProducts",0,"delivery"],"data":{"estimatedDelivery":"25.01.2000","fastestDelivery":"25.01.2000"}}]}\r ------\r -` - }) - - await app.close() - } - - t.end() - }) -} From 08ee6d2ff7526d3ba27a3a1319cd4fefbf9a4904 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 10:17:17 +0100 Subject: [PATCH 03/16] remove 'Mercurius' from the error message about wrong accept header, removed unnecessary status code in the error --- index.js | 6 ++---- test/defer.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 08108b0d..fd3fee97 100644 --- a/index.js +++ b/index.js @@ -578,12 +578,10 @@ const plugin = fp(async function (app, opts) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. throw new Error( - 'Mercurius server received an operation that uses incremental delivery ' + + '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'.", - // Use 406 Not Accepted TODO - { extensions: { http: { status: 406 } } } + "header 'Accept: multipart/mixed; deferSpec=20220824'." ) } diff --git a/test/defer.js b/test/defer.js index 9e2e3e46..89b0de31 100644 --- a/test/defer.js +++ b/test/defer.js @@ -90,7 +90,7 @@ if (canUseIncrementalExecution) { body: JSON.stringify({ data: null, errors: [{ - message: "Mercurius 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'." + message: "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'." }] }) }) From 35fdb02853559da512278b23c331e3e2ad8818ec Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 19:31:47 +0100 Subject: [PATCH 04/16] bump graphql to 17.0.0-alpha.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 36f17bba..6f66a1df 100644 --- a/package.json +++ b/package.json @@ -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", @@ -61,7 +61,7 @@ "@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", "negotiator": "^0.6.3", From d41fff63bcc083fd008b63d03e20ddddae007c19 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 19:32:24 +0100 Subject: [PATCH 05/16] moved missing multipart accept header error into errors.js --- index.js | 10 +++------- lib/errors.js | 4 ++++ test/defer.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index fd3fee97..ca6337a1 100644 --- a/index.js +++ b/index.js @@ -38,7 +38,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') @@ -577,12 +578,7 @@ const plugin = fp(async function (app, opts) { ) { // The client ran an operation that would yield multiple parts, but didn't // specify `accept: multipart/mixed`. We return an error. - throw new Error( - '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'." - ) + throw new MER_ERR_INVALID_MULTIPART_ACCEPT_HEADER() } reply.header('content-type', 'multipart/mixed; boundary="-"; deferSpec=20220824') diff --git a/lib/errors.js b/lib/errors.js index 1cfb5c79..e55242ee 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -139,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 */ diff --git a/test/defer.js b/test/defer.js index 89b0de31..eb2a8a65 100644 --- a/test/defer.js +++ b/test/defer.js @@ -86,11 +86,11 @@ if (canUseIncrementalExecution) { }) t.match(res, { - statusCode: 200, + statusCode: 500, body: JSON.stringify({ data: null, errors: [{ - message: "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'." + message: '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".' }] }) }) From 274c5bc6d35f7dd1a1e0104b163d89b8ab782def Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 19:50:51 +0100 Subject: [PATCH 06/16] added opts.defer: boolean to enable @defer directive --- index.js | 12 ++++++++++++ test/defer.js | 36 +++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index ca6337a1..6c96c425 100644 --- a/index.js +++ b/index.js @@ -190,6 +190,18 @@ const plugin = fp(async function (app, opts) { }) } + if (opts.defer) { + schema = extendSchema( + schema, + parse(` + directive @defer( + if: Boolean! = true + label: String + ) on FRAGMENT_SPREAD | INLINE_FRAGMENT + `) + ) + } + fastifyGraphQl.schema = schema app.addHook('onReady', async function () { diff --git a/test/defer.js b/test/defer.js index eb2a8a65..8bf05fd8 100644 --- a/test/defer.js +++ b/test/defer.js @@ -5,11 +5,6 @@ const { canUseIncrementalExecution } = require('../lib/util') if (canUseIncrementalExecution) { const schema = ` - directive @defer( - if: Boolean! = true - label: String - ) on FRAGMENT_SPREAD | INLINE_FRAGMENT - type Query { allProducts: [Product!]! } @@ -70,10 +65,37 @@ if (canUseIncrementalExecution) { 'multipart/mixed; deferSpec=12345' ] + test('errors with @defer when opts.defer is not true', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) + + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ query }) + }) + + t.match(res, { + statusCode: 400, + body: JSON.stringify({ + data: null, + errors: [{ + message: 'Unknown directive "@defer".', locations: [{ line: 5, column: 25 }] + }] + }) + }) + + await app.close() + t.end() + }) + for (const accept of wrongAcceptValues) { test('errors with @defer when used with wrong "accept" header', async t => { const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true }) + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) const res = await app.inject({ method: 'POST', @@ -109,7 +131,7 @@ if (canUseIncrementalExecution) { for (const accept of correctAcceptValues) { test('works with @defer when used with correct "accept" header', async t => { const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true }) + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) const res = await app.inject({ method: 'POST', From 3e3ca7772e6e1e38a70f74f9cf2b63aabc0d1462 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 20:02:57 +0100 Subject: [PATCH 07/16] use @fastify/accepts instead of Negotiator package --- index.js | 10 ++++------ package.json | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 6c96c425..99b2a19b 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict' -const Negotiator = require('negotiator') const fp = require('fastify-plugin') let LRU = require('tiny-lru') const routes = require('./lib/routes') @@ -191,6 +190,8 @@ const plugin = fp(async function (app, opts) { } if (opts.defer) { + app.register(require('@fastify/accepts')) + schema = extendSchema( schema, parse(` @@ -572,14 +573,11 @@ const plugin = fp(async function (app, opts) { /* istanbul ignore next */ if (execution.initialResult) { - const acceptHeader = reply.request.raw.headers.accept + const accept = reply.request.accepts() // Accepts object if ( !( - acceptHeader && - new Negotiator({ - headers: { accept: acceptHeader } - }).mediaType([ + 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. diff --git a/package.json b/package.json index 6f66a1df..6d94e373 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "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", @@ -64,7 +65,6 @@ "graphql": "^17.0.0-alpha.2", "graphql-jit": "^0.7.3", "mqemitter": "^5.0.0", - "negotiator": "^0.6.3", "p-map": "^4.0.0", "readable-stream": "^4.0.0", "safe-stable-stringify": "^2.3.0", From 2b3c25a400199df25aff25ca19037a6f4922913a Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 26 Oct 2022 21:07:42 +0100 Subject: [PATCH 08/16] add @defer test with undici.fetch --- test/defer.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/test/defer.js b/test/defer.js index 8bf05fd8..2d70ef81 100644 --- a/test/defer.js +++ b/test/defer.js @@ -1,5 +1,6 @@ const { test } = require('tap') const Fastify = require('fastify') +const { fetch } = require('undici') const mercurius = require('../index') const { canUseIncrementalExecution } = require('../lib/util') @@ -165,4 +166,46 @@ content-type: application/json; charset=utf-8\r t.end() }) } + + test('returns stream when using undici.fetch with @defer', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) + const url = await app.listen({ port: 0 }) + + const res = await fetch(`${url}/graphql`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; deferSpec=20220824' + }, + body: JSON.stringify({ query }) + }) + + const reader = res.body.getReader() + const { value } = await reader.read() + const result = new TextDecoder('utf-8').decode(value) + + t.same(result, `\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":true,"data":{"allProducts":[{"delivery":{},"sku":"sku","id":"0"}]}}\r +---\r +content-type: application/json; charset=utf-8\r +\r +{"hasNext":false,"incremental":[{"path":["allProducts",0,"delivery"],"data":{"estimatedDelivery":"25.01.2000","fastestDelivery":"25.01.2000"}}]}\r +-----\r +`) + + t.same(res.status, 200) + t.same(res.headers.get('content-type'), 'multipart/mixed; boundary="-"; deferSpec=20220824') + + t.teardown(async () => { + await reader.releaseLock() + app.close() + process.exit() + }) + + t.end() + }) } From 369457ea9b1822d6f1f65234c48bf3a6cd6bd61c Mon Sep 17 00:00:00 2001 From: Bartek Igielski Date: Wed, 2 Nov 2022 12:11:37 +0100 Subject: [PATCH 09/16] Add space between merged SDLs to fix merging errors (#899) * Add space between merged SDLs to fix merging errors * Add unit tests for schema merging --- lib/gateway/build-gateway.js | 6 ++-- test/gateway/schema.js | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/gateway/build-gateway.js b/lib/gateway/build-gateway.js index bdac04c7..758792f3 100644 --- a/lib/gateway/build-gateway.js +++ b/lib/gateway/build-gateway.js @@ -313,7 +313,7 @@ async function buildGateway (gatewayOpts, app) { throw new MER_ERR_GQL_GATEWAY_INIT('No valid service SDLs were provided') } - const schema = buildFederatedSchema(serviceSDLs.join(''), true) + const schema = buildFederatedSchema(serviceSDLs.join(' '), true) const typeToServiceMap = {} const typeFieldsToService = {} @@ -419,7 +419,7 @@ async function buildGateway (gatewayOpts, app) { async refresh (isRetry) { const failedMandatoryServices = [] if (this._serviceSDLs === undefined) { - this._serviceSDLs = serviceSDLs.join('') + this._serviceSDLs = serviceSDLs.join(' ') } const $refreshResult = await Promise.allSettled( @@ -458,7 +458,7 @@ async function buildGateway (gatewayOpts, app) { const _serviceSDLs = Object.values(serviceMap) .map((service) => service.schemaDefinition) - .join('') + .join(' ') if (this._serviceSDLs === _serviceSDLs) { return null diff --git a/test/gateway/schema.js b/test/gateway/schema.js index 13bdecd7..e2fce8fe 100644 --- a/test/gateway/schema.js +++ b/test/gateway/schema.js @@ -302,6 +302,65 @@ test('It builds the gateway schema correctly', async (t) => { }) }) +test('Should merge schemas correctly', async (t) => { + const [helloService, helloServicePort] = await createService( + t, + 'extend type Query { hello: String } directive @customDirective on FIELD_DEFINITION', + { Query: { hello: () => 'World' } } + ) + + const [worldService, worldServicePort] = await createService( + t, + 'extend type Query { world: String }', + { Query: { world: () => 'Hello' } } + ) + + const gateway = Fastify() + + t.teardown(async () => { + await gateway.close() + await helloService.close() + await worldService.close() + }) + + gateway.register(GQL, { + gateway: { + services: [{ + name: 'hello', + url: `http://localhost:${helloServicePort}/graphql` + }, { + name: 'world', + url: `http://localhost:${worldServicePort}/graphql` + }] + } + }) + + const query = ` + query { + hello + world + } + ` + + const res = await gateway.inject({ + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + url: '/graphql', + body: JSON.stringify({ + query + }) + }) + + t.same(JSON.parse(res.body), { + data: { + hello: 'World', + world: 'Hello' + } + }) +}) + test('It support variable inside nested arguments', async (t) => { const user = { id: 'u1', From 780c668742109d9375ab33d89433f52af1ed5980 Mon Sep 17 00:00:00 2001 From: Igor Luchenkov Date: Wed, 2 Nov 2022 11:12:13 +0000 Subject: [PATCH 10/16] Explicitly say in the docs that JIT is disabled by default (#901) --- docs/api/options.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/options.md b/docs/api/options.md index 3f89d568..cf57686e 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -52,6 +52,7 @@ - `jit`: Integer. The minimum number of execution a query needs to be executed before being jit'ed. + - Default: `0`, jit is disabled. - `routes`: boolean. Serves the Default: `true`. A graphql endpoint is exposed at `/graphql`. - `path`: string. Change default graphql `/graphql` route to another one. From b4d70fc50537273a12fbba337c1b7b4a1e08a371 Mon Sep 17 00:00:00 2001 From: Davide Fiorello Date: Wed, 2 Nov 2022 15:45:25 +0100 Subject: [PATCH 11/16] feat: add types for object in graphiql configuration (#907) * feat: add types for object in graphiql configuration * feat: add test to types --- index.d.ts | 32 ++++++++++++++++++++++++++++++-- test/types/index.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 39df195b..5673216c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -437,12 +437,40 @@ export interface MercuriusSchemaOptions { schemaTransforms?: ((originalSchema: GraphQLSchema) => GraphQLSchema) | Array<(originalSchema: GraphQLSchema) => GraphQLSchema>; } +export interface MercuriusGraphiQLOptions { + /** + * Expose the graphiql app, default `true` + */ + enabled?: boolean; + /** + * The list of plugin to add to GraphiQL + */ + plugins?: Array<{ + /** + * The name of the plugin, it should be the same exported in the `umd` + */ + name: string; + /** + * The props to be passed to the plugin + */ + props?: Object; + /** + * The urls of the plugin, it's downloaded at runtime. (eg. https://unpkg.com/myplugin/....) + */ + umdUrl: string; + /** + * A function name exported by the plugin to read/enrich the fetch response + */ + fetcherWrapper?: string; + }> +} + export interface MercuriusCommonOptions { /** * Serve GraphiQL on /graphiql if true or 'graphiql' and if routes is true */ - graphiql?: boolean | 'graphiql'; - ide?: boolean | 'graphiql'; + graphiql?: boolean | 'graphiql' | MercuriusGraphiQLOptions; + ide?: boolean | 'graphiql' | MercuriusGraphiQLOptions; /** * The minimum number of execution a query needs to be executed before being jit'ed. * @default 0 - disabled diff --git a/test/types/index.ts b/test/types/index.ts index 175a51c3..3e56828d 100644 --- a/test/types/index.ts +++ b/test/types/index.ts @@ -879,3 +879,40 @@ app.graphql.addHook('onResolution', async function (_execution, context) { expectType(context.operationsCount) expectType(context.__currentQuery) }) + +// Test graphiql configuration using an object as params +app.register(mercurius, { schema, resolvers, graphiql: { plugins: [] } }) + +app.register(mercurius, { schema, resolvers, ide: { enabled: false } }) + +app.register(mercurius, { + schema, + resolvers, + graphiql: { + enabled: true, + plugins: [ + { + fetcherWrapper: 'testFetchWrapper', + umdUrl: 'http://some-url', + props: { foo: 'bar' }, + name: 'pluginName' + } + ] + } +}) + +expectError(() => { + app.register(mercurius, { + schema, + resolvers, + graphiql: { + enabled: true, + plugins: [ + { + fetcherWrapper: 'testFetchWrapper', + props: { foo: 'bar' } + } + ] + } + }) +}) From e27e5b01ae0210cdbc0de259175b1c011d258c80 Mon Sep 17 00:00:00 2001 From: Bartek Igielski Date: Wed, 2 Nov 2022 15:46:07 +0100 Subject: [PATCH 12/16] Prevent parsing schema exceptions when importing directives (#900) * Prevent parsing schema extensions * Add unit test --- lib/federation.js | 2 +- test/federation.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/federation.js b/lib/federation.js index 6c1258e4..11d6131f 100644 --- a/lib/federation.js +++ b/lib/federation.js @@ -92,7 +92,7 @@ function getStubTypes (schemaDefinitions, isGateway) { const directiveDefinitions = [] for (const definition of schemaDefinitions) { - if (definition.kind === 'SchemaDefinition') { + if (definition.kind === 'SchemaDefinition' || definition.kind === 'SchemaExtension') { continue } diff --git a/test/federation.js b/test/federation.js index 2c551039..4c1bcfa4 100644 --- a/test/federation.js +++ b/test/federation.js @@ -1087,3 +1087,45 @@ test('basic federation support with \'schema\' in the schema', async (t) => { } }) }) + +test('should support directives import syntax', async (t) => { + const app = Fastify() + + const schema = ` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.0", + import: ["@key", "@shareable", "@override"]) + + extend type Query { + hello: String + } + ` + + const resolvers = { + Query: { + hello: () => 'world' + } + } + + app.register(GQL, { + schema, + resolvers, + federationMetadata: true + }) + + await app.ready() + + const query = '{ _service { sdl } }' + const res = await app.inject({ + method: 'GET', + url: `/graphql?query=${query}` + }) + + t.same(JSON.parse(res.body), { + data: { + _service: { + sdl: schema + } + } + }) +}) From 6933420149b5884084c41322abb71d4bccaf67e8 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 2 Nov 2022 15:51:54 +0100 Subject: [PATCH 13/16] Bumped v11.3.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c55ee9b..d9068f30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mercurius", - "version": "11.2.0", + "version": "11.3.0", "description": "Fastify GraphQL adapter with gateway and subscription support", "main": "index.js", "types": "index.d.ts", From d1d8485d738bae4109abcc9ee208bc566693ece7 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 2 Nov 2022 20:13:51 +0000 Subject: [PATCH 14/16] Removed canUseIncrementalExecution check --- index.js | 2 +- lib/util.js | 7 +- test/defer.js | 283 +++++++++++++++++++++++++------------------------- 3 files changed, 143 insertions(+), 149 deletions(-) diff --git a/index.js b/index.js index 99b2a19b..ad99787a 100644 --- a/index.js +++ b/index.js @@ -562,7 +562,7 @@ const plugin = fp(async function (app, opts) { return maybeFormatErrors(execution, context) } - const execution = await executeGraphql({ + const execution = await executeGraphql(opts.defer, { schema: modifiedSchema || fastifyGraphQl.schema, document: modifiedDocument || document, rootValue: root, diff --git a/lib/util.js b/lib/util.js index de348228..b2509f2a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -26,11 +26,9 @@ function hasExtensionDirective (node) { } } -const canUseIncrementalExecution = !!require('graphql/execution').experimentalExecuteIncrementally - // istanbul ignore next -function executeGraphql (args) { - if (canUseIncrementalExecution) { +function executeGraphql (isDeferEnabled, args) { + if (isDeferEnabled) { return experimentalExecuteIncrementally(args) } @@ -45,7 +43,6 @@ const MEDIA_TYPES = { module.exports = { hasDirective, hasExtensionDirective, - canUseIncrementalExecution, executeGraphql, MEDIA_TYPES } diff --git a/test/defer.js b/test/defer.js index 2d70ef81..f9feb3c3 100644 --- a/test/defer.js +++ b/test/defer.js @@ -2,89 +2,116 @@ const { test } = require('tap') const Fastify = require('fastify') const { fetch } = require('undici') const mercurius = require('../index') -const { canUseIncrementalExecution } = require('../lib/util') -if (canUseIncrementalExecution) { - const schema = ` - type Query { - allProducts: [Product!]! - } - - type Product { - delivery: DeliveryEstimates! - sku: String! - id: ID! +const schema = ` + type Query { + allProducts: [Product!]! + } + + type Product { + delivery: DeliveryEstimates! + sku: String! + id: ID! + } + + type DeliveryEstimates { + estimatedDelivery: String! + fastestDelivery: String! + }` + +const allProducts = new Array(1).fill(0).map((_, index) => ({ + id: `${index}`, + sku: 'sku' +})) + +const resolvers = { + Query: { + allProducts: () => { + return allProducts } - - type DeliveryEstimates { - estimatedDelivery: String! - fastestDelivery: String! - }` - - const allProducts = new Array(1).fill(0).map((_, index) => ({ - id: `${index}`, - sku: 'sku' - })) - - const resolvers = { - Query: { - allProducts: () => { - return allProducts - } - }, - Product: { - delivery: () => { - return { - estimatedDelivery: '25.01.2000', - fastestDelivery: '25.01.2000' - } + }, + Product: { + delivery: () => { + return { + estimatedDelivery: '25.01.2000', + fastestDelivery: '25.01.2000' } } } +} - const query = ` - query deferVariation { - allProducts { - delivery { - ...MyFragment @defer - } - sku - id +const query = ` + query deferVariation { + allProducts { + delivery { + ...MyFragment @defer } + sku + id } - - fragment MyFragment on DeliveryEstimates { - estimatedDelivery - fastestDelivery - } - ` + } + + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } +` + +test('errors with @defer when opts.defer is not true', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true }) - const wrongAcceptValues = [ - '', - 'application/json', - 'multipart/mixed', - 'multipart/mixed; deferSpec=12345' - ] + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ query }) + }) - test('errors with @defer when opts.defer is not true', async t => { + t.match(res, { + statusCode: 400, + body: JSON.stringify({ + data: null, + errors: [{ + message: 'Unknown directive "@defer".', locations: [{ line: 5, column: 23 }] + }] + }) + }) + + await app.close() + t.end() +}) + +const wrongAcceptValues = [ + '', + 'application/json', + 'multipart/mixed', + 'multipart/mixed; deferSpec=12345' +] + +for (const accept of wrongAcceptValues) { + test('errors with @defer when used with wrong "accept" header', async t => { const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true }) + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) const res = await app.inject({ method: 'POST', url: '/graphql', headers: { - 'content-type': 'application/json' + 'content-type': 'application/json', + accept }, body: JSON.stringify({ query }) }) t.match(res, { - statusCode: 400, + statusCode: 500, body: JSON.stringify({ data: null, errors: [{ - message: 'Unknown directive "@defer".', locations: [{ line: 5, column: 25 }] + message: '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".' }] }) }) @@ -92,64 +119,35 @@ if (canUseIncrementalExecution) { await app.close() t.end() }) +} - for (const accept of wrongAcceptValues) { - test('errors with @defer when used with wrong "accept" header', async t => { - const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) - - const res = await app.inject({ - method: 'POST', - url: '/graphql', - headers: { - 'content-type': 'application/json', - accept - }, - body: JSON.stringify({ query }) - }) +const correctAcceptValues = [ + 'multipart/mixed; deferSpec=20220824', + 'multipart/mixed; deferSpec=20220824, application/json', + 'application/json, multipart/mixed; deferSpec=20220824' +] - t.match(res, { - statusCode: 500, - body: JSON.stringify({ - data: null, - errors: [{ - message: '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".' - }] - }) - }) +for (const accept of correctAcceptValues) { + test('works with @defer when used with correct "accept" header', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) - await app.close() - t.end() + const res = await app.inject({ + method: 'POST', + url: '/graphql', + headers: { + 'content-type': 'application/json', + accept + }, + body: JSON.stringify({ query }) }) - } - const correctAcceptValues = [ - 'multipart/mixed; deferSpec=20220824', - 'multipart/mixed; deferSpec=20220824, application/json', - 'application/json, multipart/mixed; deferSpec=20220824' - ] - - for (const accept of correctAcceptValues) { - test('works with @defer when used with correct "accept" header', async t => { - const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) - - const res = await app.inject({ - method: 'POST', - url: '/graphql', - headers: { - 'content-type': 'application/json', - accept - }, - body: JSON.stringify({ query }) - }) - - t.match(res, { - statusCode: 200, - headers: { - 'content-type': 'multipart/mixed; boundary="-"; deferSpec=20220824' - }, - body: `\r + t.match(res, { + statusCode: 200, + headers: { + 'content-type': 'multipart/mixed; boundary="-"; deferSpec=20220824' + }, + body: `\r ---\r content-type: application/json; charset=utf-8\r \r @@ -160,32 +158,32 @@ content-type: application/json; charset=utf-8\r {"hasNext":false,"incremental":[{"path":["allProducts",0,"delivery"],"data":{"estimatedDelivery":"25.01.2000","fastestDelivery":"25.01.2000"}}]}\r -----\r ` - }) - - await app.close() - t.end() }) - } - test('returns stream when using undici.fetch with @defer', async t => { - const app = Fastify() - await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) - const url = await app.listen({ port: 0 }) + await app.close() + t.end() + }) +} - const res = await fetch(`${url}/graphql`, { - method: 'POST', - headers: { - 'content-type': 'application/json', - accept: 'multipart/mixed; deferSpec=20220824' - }, - body: JSON.stringify({ query }) - }) +test('returns stream when using undici.fetch with @defer', async t => { + const app = Fastify() + await app.register(mercurius, { schema, resolvers, graphiql: true, defer: true }) + const url = await app.listen({ port: 0 }) - const reader = res.body.getReader() - const { value } = await reader.read() - const result = new TextDecoder('utf-8').decode(value) + const res = await fetch(`${url}/graphql`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'multipart/mixed; deferSpec=20220824' + }, + body: JSON.stringify({ query }) + }) - t.same(result, `\r + const reader = res.body.getReader() + const { value } = await reader.read() + const result = new TextDecoder('utf-8').decode(value) + + t.same(result, `\r ---\r content-type: application/json; charset=utf-8\r \r @@ -197,15 +195,14 @@ content-type: application/json; charset=utf-8\r -----\r `) - t.same(res.status, 200) - t.same(res.headers.get('content-type'), 'multipart/mixed; boundary="-"; deferSpec=20220824') - - t.teardown(async () => { - await reader.releaseLock() - app.close() - process.exit() - }) + t.same(res.status, 200) + t.same(res.headers.get('content-type'), 'multipart/mixed; boundary="-"; deferSpec=20220824') - t.end() + t.teardown(async () => { + await reader.releaseLock() + app.close() + process.exit() }) -} + + t.end() +}) From 640300f3dae06f3652af34d9aa6d36410a241bd7 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 2 Nov 2022 20:40:19 +0000 Subject: [PATCH 15/16] Throw an error if JIT is used together with defer, update the docs --- docs/api/options.md | 3 +++ index.js | 4 ++++ test/defer.js | 14 ++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/docs/api/options.md b/docs/api/options.md index cf57686e..1512f8f8 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -53,6 +53,9 @@ - `jit`: Integer. The minimum number of execution a query needs to be executed before being jit'ed. - Default: `0`, jit is disabled. + - _jit can't be used together with `defer: true`_ +- `defer`: boolean. Default: `false`. Enable @defer directive execution. + - _defer can't be used together with `jit`_ - `routes`: boolean. Serves the Default: `true`. A graphql endpoint is exposed at `/graphql`. - `path`: string. Change default graphql `/graphql` route to another one. diff --git a/index.js b/index.js index ad99787a..2e547690 100644 --- a/index.js +++ b/index.js @@ -190,6 +190,10 @@ const plugin = fp(async function (app, opts) { } if (opts.defer) { + if (opts.jit) { + throw new MER_ERR_INVALID_OPTS("@defer and JIT can't be used together") + } + app.register(require('@fastify/accepts')) schema = extendSchema( diff --git a/test/defer.js b/test/defer.js index f9feb3c3..3b3f559a 100644 --- a/test/defer.js +++ b/test/defer.js @@ -84,6 +84,20 @@ test('errors with @defer when opts.defer is not true', async t => { t.end() }) +test('errors when used with both JIT and @defer', async t => { + const app = Fastify() + + try { + await app.register(mercurius, { jit: 1, defer: true }) + t.fail('Should not successfully start the server') + } catch (ex) { + t.equal(ex.message, "Invalid options: @defer and JIT can't be used together") + } + + await app.close() + t.end() +}) + const wrongAcceptValues = [ '', 'application/json', From 8b7e9b24d456861d9d0db133b5463637f064f973 Mon Sep 17 00:00:00 2001 From: "igor.luckenkov" Date: Wed, 2 Nov 2022 20:40:19 +0000 Subject: [PATCH 16/16] Throw an error if JIT is used together with defer, update the docs --- docs/api/options.md | 3 +++ index.d.ts | 7 +++++++ index.js | 4 ++++ test/defer.js | 14 ++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/docs/api/options.md b/docs/api/options.md index cf57686e..2743aa7d 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -53,6 +53,9 @@ - `jit`: Integer. The minimum number of execution a query needs to be executed before being jit'ed. - Default: `0`, jit is disabled. + - _jit can't be used together with `defer: true`_ +- `defer`: boolean. Default: `false`. Enable @defer directive execution support. + - _defer can't be used together with `jit`_ - `routes`: boolean. Serves the Default: `true`. A graphql endpoint is exposed at `/graphql`. - `path`: string. Change default graphql `/graphql` route to another one. diff --git a/index.d.ts b/index.d.ts index 5673216c..1d84a466 100644 --- a/index.d.ts +++ b/index.d.ts @@ -473,9 +473,16 @@ export interface MercuriusCommonOptions { ide?: boolean | 'graphiql' | MercuriusGraphiQLOptions; /** * The minimum number of execution a query needs to be executed before being jit'ed. + * Can't be enabled with MercuriusCommonOptions.defer * @default 0 - disabled */ jit?: number; + /** + * Enable @defer directive execution support. + * Can't be enabled with MercuriusCommonOptions.jit + * @default false + */ + defer?: boolean; /** * A graphql endpoint is exposed at /graphql when true * @default true diff --git a/index.js b/index.js index ad99787a..2e547690 100644 --- a/index.js +++ b/index.js @@ -190,6 +190,10 @@ const plugin = fp(async function (app, opts) { } if (opts.defer) { + if (opts.jit) { + throw new MER_ERR_INVALID_OPTS("@defer and JIT can't be used together") + } + app.register(require('@fastify/accepts')) schema = extendSchema( diff --git a/test/defer.js b/test/defer.js index f9feb3c3..3b3f559a 100644 --- a/test/defer.js +++ b/test/defer.js @@ -84,6 +84,20 @@ test('errors with @defer when opts.defer is not true', async t => { t.end() }) +test('errors when used with both JIT and @defer', async t => { + const app = Fastify() + + try { + await app.register(mercurius, { jit: 1, defer: true }) + t.fail('Should not successfully start the server') + } catch (ex) { + t.equal(ex.message, "Invalid options: @defer and JIT can't be used together") + } + + await app.close() + t.end() +}) + const wrongAcceptValues = [ '', 'application/json',