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: support passing options to graphql validate and parse #1050

Merged
merged 1 commit into from
Nov 15, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.`) {
simoneb marked this conversation as resolved.
Show resolved Hide resolved
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)
})
Loading