diff --git a/src/__tests__/graphql-test.ts b/src/__tests__/graphql-test.ts new file mode 100644 index 0000000000..fb0aff8a13 --- /dev/null +++ b/src/__tests__/graphql-test.ts @@ -0,0 +1,164 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { GraphQLError } from '../error/GraphQLError.js'; + +import { Source } from '../language/source.js'; + +import { GraphQLObjectType } from '../type/definition.js'; +import { GraphQLString } from '../type/scalars.js'; +import { GraphQLSchema } from '../type/schema.js'; + +import type { ValidationRule } from '../validation/ValidationContext.js'; + +import { graphql, graphqlSync } from '../graphql.js'; + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { + type: GraphQLString, + resolve: () => 'A', + }, + b: { + type: GraphQLString, + resolve: () => 'B', + }, + contextEcho: { + type: GraphQLString, + resolve: (_source, _args, contextValue) => String(contextValue), + }, + syncField: { + type: GraphQLString, + resolve: (rootValue) => rootValue, + }, + asyncField: { + type: GraphQLString, + resolve: (rootValue) => Promise.resolve(rootValue), + }, + }, + }), +}); + +describe('graphql', () => { + it('passes source through to parse', async () => { + const source = new Source('{', 'custom-query.graphql'); + + const result = await graphql({ schema, source }); + + expect(result.errors?.[0]?.source?.name).to.equal('custom-query.graphql'); + }); + + it('passes rules through to validate', async () => { + const customRule: ValidationRule = (context) => ({ + Field(node) { + context.reportError( + new GraphQLError('custom rule error', { + nodes: node, + }), + ); + }, + }); + + const result = await graphql({ + schema, + source: '{ a }', + rules: [customRule], + }); + + expect(result.errors?.[0]?.message).to.equal('custom rule error'); + }); + + it('passes parse options through to parse', async () => { + const customRule: ValidationRule = (context) => ({ + OperationDefinition(node) { + context.reportError( + new GraphQLError( + node.loc === undefined ? 'no location' : 'has location', + { + nodes: node, + }, + ), + ); + }, + }); + + const result = await graphql({ + schema, + source: '{ a }', + noLocation: true, + rules: [customRule], + }); + + expect(result.errors?.[0]?.message).to.equal('no location'); + }); + + it('passes validation options through to validate', async () => { + const result = await graphql({ + schema, + source: '{ contextEho }', + hideSuggestions: true, + }); + + expect(result.errors?.[0]?.message).to.equal( + 'Cannot query field "contextEho" on type "Query".', + ); + }); + + it('passes execution args through to execute', async () => { + const result = await graphql({ + schema, + source: ` + query First { + a + } + + query Second { + b + } + `, + operationName: 'Second', + }); + + expect(result).to.deep.equal({ + data: { + b: 'B', + }, + }); + }); + + it('returns schema validation errors', async () => { + const badSchema = new GraphQLSchema({}); + const result = await graphql({ + schema: badSchema, + source: '{ __typename }', + }); + + expect(result.errors?.[0]?.message).to.equal( + 'Query root type must be provided.', + ); + }); +}); + +describe('graphqlSync', () => { + it('returns result for synchronous execution', () => { + const result = graphqlSync({ + schema, + source: '{ syncField }', + rootValue: 'rootValue', + }); + + expect(result).to.deep.equal({ data: { syncField: 'rootValue' } }); + }); + + it('throws for asynchronous execution', () => { + expect(() => { + graphqlSync({ + schema, + source: '{ asyncField }', + rootValue: 'rootValue', + }); + }).to.throw('GraphQL execution failed to complete synchronously.'); + }); +}); diff --git a/src/graphql.ts b/src/graphql.ts index f71219b9e6..4e1d2af083 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -1,19 +1,17 @@ import { isPromise } from './jsutils/isPromise.js'; -import type { Maybe } from './jsutils/Maybe.js'; import type { PromiseOrValue } from './jsutils/PromiseOrValue.js'; +import type { ParseOptions } from './language/parser.js'; import { parse } from './language/parser.js'; import type { Source } from './language/source.js'; -import type { - GraphQLFieldResolver, - GraphQLTypeResolver, -} from './type/definition.js'; -import type { GraphQLSchema } from './type/schema.js'; import { validateSchema } from './type/validate.js'; +import type { ValidationOptions } from './validation/validate.js'; import { validate } from './validation/validate.js'; +import type { ValidationRule } from './validation/ValidationContext.js'; +import type { ExecutionArgs } from './execution/execute.js'; import { execute } from './execution/execute.js'; import type { ExecutionResult } from './execution/Executor.js'; @@ -58,17 +56,12 @@ import type { ExecutionResult } from './execution/Executor.js'; * If not provided, the default type resolver is used (which looks for a * `__typename` field or alternatively calls the `isTypeOf` method). */ -export interface GraphQLArgs { - schema: GraphQLSchema; +export interface GraphQLArgs + extends ParseOptions, + ValidationOptions, + Omit { source: string | Source; - hideSuggestions?: Maybe; - rootValue?: unknown; - contextValue?: unknown; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - typeResolver?: Maybe>; - abortSignal?: Maybe; + rules?: ReadonlyArray | undefined; } export function graphql(args: GraphQLArgs): Promise { @@ -94,18 +87,7 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult { } function graphqlImpl(args: GraphQLArgs): PromiseOrValue { - const { - schema, - source, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - hideSuggestions, - abortSignal, - } = args; + const { schema, source } = args; // Validate Schema const schemaValidationErrors = validateSchema(schema); @@ -116,30 +98,17 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { // Parse let document; try { - document = parse(source); + document = parse(source, args); } catch (syntaxError) { return { errors: [syntaxError] }; } // Validate - const validationErrors = validate(schema, document, undefined, { - hideSuggestions, - }); + const validationErrors = validate(schema, document, args.rules, args); if (validationErrors.length > 0) { return { errors: validationErrors }; } // Execute - return execute({ - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - hideSuggestions, - abortSignal, - }); + return execute({ ...args, document }); } diff --git a/src/validation/validate.ts b/src/validation/validate.ts index 05eeb39dbb..7aea79f592 100644 --- a/src/validation/validate.ts +++ b/src/validation/validate.ts @@ -17,6 +17,11 @@ import { ValidationContext, } from './ValidationContext.js'; +export interface ValidationOptions { + maxErrors?: number; + hideSuggestions?: Maybe; +} + /** * Implements the "Validation" section of the spec. * @@ -41,7 +46,7 @@ export function validate( schema: GraphQLSchema, documentAST: DocumentNode, rules: ReadonlyArray = specifiedRules, - options?: { maxErrors?: number; hideSuggestions?: Maybe }, + options?: ValidationOptions, ): ReadonlyArray { const maxErrors = options?.maxErrors ?? 100; const hideSuggestions = options?.hideSuggestions ?? false;