Skip to content

Commit

Permalink
Merge pull request #1050 from markrzen/master
Browse files Browse the repository at this point in the history
feat: support passing options to graphql validate and parse
  • Loading branch information
simoneb authored Nov 15, 2023
2 parents 62f41ee + ca67ab8 commit 65a9ec6
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 3 deletions.
3 changes: 3 additions & 0 deletions docs/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- `validateOptions`: Object. [GraphQL's validate function options](https://github.com/graphql/graphql-js/blob/main/src/validation/validate.ts)
- `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`_
Expand Down
14 changes: 14 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
GraphQLScalarType,
ValidationRule,
FormattedExecutionResult,
ParseOptions,
} from "graphql";
import { SocketStream } from "@fastify/websocket"
import { IncomingMessage, IncomingHttpHeaders, OutgoingHttpHeaders } from "http";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
142 changes: 142 additions & 0 deletions test/graphql-option-override.js
Original file line number Diff line number Diff line change
@@ -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)
})

0 comments on commit 65a9ec6

Please sign in to comment.