Skip to content
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
164 changes: 164 additions & 0 deletions src/__tests__/graphql-test.ts
Original file line number Diff line number Diff line change
@@ -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.');
});
});
57 changes: 13 additions & 44 deletions src/graphql.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<ExecutionArgs, 'document'> {
source: string | Source;
hideSuggestions?: Maybe<boolean>;
rootValue?: unknown;
contextValue?: unknown;
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
operationName?: Maybe<string>;
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
abortSignal?: Maybe<AbortSignal>;
rules?: ReadonlyArray<ValidationRule> | undefined;
}

export function graphql(args: GraphQLArgs): Promise<ExecutionResult> {
Expand All @@ -94,18 +87,7 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult {
}

function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
const {
schema,
source,
rootValue,
contextValue,
variableValues,
operationName,
fieldResolver,
typeResolver,
hideSuggestions,
abortSignal,
} = args;
const { schema, source } = args;

// Validate Schema
const schemaValidationErrors = validateSchema(schema);
Expand All @@ -116,30 +98,17 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
// 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 });
}
7 changes: 6 additions & 1 deletion src/validation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import {
ValidationContext,
} from './ValidationContext.js';

export interface ValidationOptions {
maxErrors?: number;
hideSuggestions?: Maybe<boolean>;
}

/**
* Implements the "Validation" section of the spec.
*
Expand All @@ -41,7 +46,7 @@ export function validate(
schema: GraphQLSchema,
documentAST: DocumentNode,
rules: ReadonlyArray<ValidationRule> = specifiedRules,
options?: { maxErrors?: number; hideSuggestions?: Maybe<boolean> },
options?: ValidationOptions,
): ReadonlyArray<GraphQLError> {
const maxErrors = options?.maxErrors ?? 100;
const hideSuggestions = options?.hideSuggestions ?? false;
Expand Down
Loading