From 0b8ccf63ba8d5cde4b292fe11330add80a5e2c51 Mon Sep 17 00:00:00 2001 From: Mark Reynolds Date: Wed, 8 Nov 2023 17:04:10 -0500 Subject: [PATCH] feat: support passing options to graphql validate and parse --- docs/api/options.md | 3 + index.d.ts | 14 ++++ index.js | 15 +++- test/graphql-option-override.js | 142 ++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 test/graphql-option-override.js diff --git a/docs/api/options.md b/docs/api/options.md index c30f9483..386a84f6 100644 --- a/docs/api/options.md +++ b/docs/api/options.md @@ -34,6 +34,9 @@ - `loaders`: Object. See [defineLoaders](#appgraphqlextendschemaschema-appgraphqldefineresolversresolvers-and-appgraphqldefineloadersloaders) for more details. - `schemaTransforms`: Array of schema-transformation functions. Accept a schema as an argument and return a schema. +- `graphql`: Object. Override options for graphql function that Mercurius utilizes. + - `parseOptions`: Object. [GraphQL's parse function options](https://github.com/graphql/graphql-js/blob/main/src/language/parser.ts#L77-L133) + - `validateOptions`: Object. [GraphQL's validate function options](https://github.com/graphql/graphql-js/blob/main/src/validation/validate.ts#L44) - `graphiql`: boolean | string | Object. Serve [GraphiQL](https://www.npmjs.com/package/graphiql) on `/graphiql` if `true` or `'graphiql'`. Leave empty or `false` to disable. _only applies if `onlyPersisted` option is not `true`_ diff --git a/index.d.ts b/index.d.ts index 2be6c4d8..0826a4a8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,6 +13,7 @@ import { GraphQLScalarType, ValidationRule, FormattedExecutionResult, + ParseOptions, } from "graphql"; import { SocketStream } from "@fastify/websocket" import { IncomingMessage, IncomingHttpHeaders, OutgoingHttpHeaders } from "http"; @@ -356,6 +357,15 @@ export interface MercuriusGraphiQLOptions { }> } +export interface GrapQLValidateOptions { + maxErrors?: number; +} + +export interface MercuriusGraphQLOptions { + parseOptions?: ParseOptions, + validateOptions?: GrapQLValidateOptions +} + export interface MercuriusCommonOptions { /** * Serve GraphiQL on /graphiql if true or 'graphiql' and if routes is true @@ -413,6 +423,10 @@ export interface MercuriusCommonOptions { * The maximum depth allowed for a single query. */ queryDepth?: number; + /** + * GraphQL function optional option overrides + */ + graphql?: MercuriusGraphQLOptions, context?: ( request: FastifyRequest, reply: FastifyReply diff --git a/index.js b/index.js index 7788fb13..00ea743c 100644 --- a/index.js +++ b/index.js @@ -78,6 +78,10 @@ const plugin = fp(async function (app, opts) { const queryDepthLimit = opts.queryDepth const errorFormatter = typeof opts.errorFormatter === 'function' ? opts.errorFormatter : defaultErrorFormatter + opts.graphql = opts.graphql || {} + const gqlParseOpts = opts.graphql.parseOptions || {} + const gqlValidateOpts = opts.graphql.validateOptions || {} + if (opts.persistedQueries) { if (opts.onlyPersisted) { opts.persistedQueryProvider = persistedQueryDefaults.preparedOnly(opts.persistedQueries) @@ -246,7 +250,7 @@ const plugin = fp(async function (app, opts) { fastifyGraphQl.extendSchema = fastifyGraphQl.extendSchema || function (s) { if (typeof s === 'string') { - s = parse(s) + s = parse(s, gqlParseOpts) } else if (!s || typeof s !== 'object') { throw new MER_ERR_INVALID_OPTS('Must provide valid Document AST') } @@ -428,9 +432,14 @@ const plugin = fp(async function (app, opts) { } try { - document = parse(source) + document = parse(source, gqlParseOpts) } catch (syntaxError) { try { + // Do not try to JSON.parse maxToken exceeded validation errors + if (gqlParseOpts.maxTokens && syntaxError.message === `Syntax Error: Document contains more that ${gqlParseOpts.maxTokens} tokens. Parsing aborted.`) { + throw syntaxError + } + // Try to parse the source as ast document = JSON.parse(source) } catch { @@ -454,7 +463,7 @@ const plugin = fp(async function (app, opts) { validationRules = opts.validationRules({ source, variables, operationName }) } } - const validationErrors = validate(fastifyGraphQl.schema, document, [...specifiedRules, ...validationRules]) + const validationErrors = validate(fastifyGraphQl.schema, document, [...specifiedRules, ...validationRules], gqlValidateOpts) if (validationErrors.length > 0) { if (lruErrors) { diff --git a/test/graphql-option-override.js b/test/graphql-option-override.js new file mode 100644 index 00000000..2923a55b --- /dev/null +++ b/test/graphql-option-override.js @@ -0,0 +1,142 @@ +'use strict' + +const { test } = require('tap') +const Fastify = require('fastify') +const mercurius = require('..') + +const schema = ` +type User { + name: String! + password: String! +} + +type Query { + read: [User] +} +` + +const resolvers = { + Query: { + read: async (_, obj) => { + return [ + { + name: 'foo', + password: 'bar' + } + ] + } + } +} + +const query = `{ + read { + name + password + } +}` + +const query2 = `{ + read { + intentionallyUnknownField1 + intentionallyUnknownField2 + intentionallyUnknownField3 + } +}` + +test('do not override graphql function options', async t => { + const app = Fastify() + t.teardown(() => app.close()) + + await app.register(mercurius, { + schema, + resolvers + }) + + await app.ready() + + const res = await app.graphql(query) + + const expectedResult = { + data: { + read: [{ + name: 'foo', + password: 'bar' + }] + } + } + + t.same(res, expectedResult) +}) + +test('override graphql.parse options', async t => { + const app = Fastify() + t.teardown(() => app.close()) + + await app.register(mercurius, { + schema, + resolvers, + graphql: { + parseOptions: { + maxTokens: 1 + } + } + }) + + await app.ready() + + const expectedErr = { + errors: [{ + message: 'Syntax Error: Document contains more that 1 tokens. Parsing aborted.' + }] + } + + await t.rejects(app.graphql(query), expectedErr) +}) + +test('do not override graphql.validate options', async t => { + const app = Fastify() + t.teardown(() => app.close()) + + await app.register(mercurius, { + schema, + resolvers + }) + + await app.ready() + + const expectedErr = { + errors: [ + { message: 'Cannot query field "intentionallyUnknownField1" on type "User".' }, + { message: 'Cannot query field "intentionallyUnknownField2" on type "User".' }, + { message: 'Cannot query field "intentionallyUnknownField3" on type "User".' } + ] + } + + await t.rejects(app.graphql(query2), expectedErr) +}) + +test('override graphql.validate options', async t => { + const app = Fastify() + t.teardown(() => app.close()) + + await app.register(mercurius, { + schema, + resolvers, + graphql: { + validateOptions: { + maxErrors: 1 + } + } + }) + + await app.ready() + + const expectedErr = { + errors: [ + { message: 'Cannot query field "intentionallyUnknownField1" on type "User".' }, + { message: 'Too many validation errors, error limit reached. Validation aborted.' } + ] + } + + await t.rejects(app.graphql(query2), expectedErr) +})