diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index bd653e4694..84d4db1595 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -55,6 +55,7 @@ interface CollectFieldsContext { operation: OperationDefinitionNode; runtimeType: GraphQLObjectType; visitedFragmentNames: Set; + hideSuggestions: boolean; } /** @@ -66,12 +67,14 @@ interface CollectFieldsContext { * * @internal */ +// eslint-disable-next-line @typescript-eslint/max-params export function collectFields( schema: GraphQLSchema, fragments: ObjMap, variableValues: VariableValues, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, + hideSuggestions: boolean, ): { groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; @@ -85,6 +88,7 @@ export function collectFields( runtimeType, operation, visitedFragmentNames: new Set(), + hideSuggestions, }; collectFieldsImpl( @@ -114,6 +118,7 @@ export function collectSubfields( operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldDetailsList: FieldDetailsList, + hideSuggestions: boolean, ): { groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; @@ -125,6 +130,7 @@ export function collectSubfields( runtimeType: returnType, operation, visitedFragmentNames: new Set(), + hideSuggestions, }; const subGroupedFieldSet = new AccumulatorMap(); const newDeferUsages: Array = []; @@ -166,6 +172,7 @@ function collectFieldsImpl( runtimeType, operation, visitedFragmentNames, + hideSuggestions, } = context; for (const selection of selectionSet.selections) { @@ -265,6 +272,7 @@ function collectFieldsImpl( fragmentVariableSignatures, variableValues, fragmentVariableValues, + hideSuggestions, ); } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index af4e6b9928..a35156cb8c 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -98,7 +98,7 @@ const collectSubfields = memoize3( returnType: GraphQLObjectType, fieldDetailsList: FieldDetailsList, ) => { - const { schema, fragments, operation, variableValues } = + const { schema, fragments, operation, variableValues, hideSuggestions } = validatedExecutionArgs; return _collectSubfields( schema, @@ -107,6 +107,7 @@ const collectSubfields = memoize3( operation, returnType, fieldDetailsList, + hideSuggestions, ); }, ); @@ -155,6 +156,7 @@ export interface ValidatedExecutionArgs { validatedExecutionArgs: ValidatedExecutionArgs, ) => PromiseOrValue; enableEarlyExecution: boolean; + hideSuggestions: boolean; } export interface ExecutionContext { @@ -184,6 +186,7 @@ export interface ExecutionArgs { ) => PromiseOrValue >; enableEarlyExecution?: Maybe; + hideSuggestions?: Maybe; } export interface StreamUsage { @@ -308,8 +311,14 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( cancellableStreams: undefined, }; try { - const { schema, fragments, rootValue, operation, variableValues } = - validatedExecutionArgs; + const { + schema, + fragments, + rootValue, + operation, + variableValues, + hideSuggestions, + } = validatedExecutionArgs; const rootType = schema.getRootType(operation.operation); if (rootType == null) { throw new GraphQLError( @@ -324,6 +333,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( variableValues, rootType, operation, + hideSuggestions, ); const { groupedFieldSet, newDeferUsages } = collectedFields; @@ -554,12 +564,16 @@ export function validateExecutionArgs( // FIXME: https://github.com/graphql/graphql-js/issues/2203 /* c8 ignore next */ const variableDefinitions = operation.variableDefinitions ?? []; + const hideSuggestions = args.hideSuggestions ?? false; const variableValuesOrErrors = getVariableValues( schema, variableDefinitions, rawVariableValues ?? {}, - { maxErrors: 50 }, + { + maxErrors: 50, + hideSuggestions, + }, ); if (variableValuesOrErrors.errors) { @@ -579,6 +593,7 @@ export function validateExecutionArgs( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, perEventExecutor: perEventExecutor ?? executeSubscriptionEvent, enableEarlyExecution: enableEarlyExecution === true, + hideSuggestions, }; } @@ -762,7 +777,8 @@ function executeField( deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { const validatedExecutionArgs = exeContext.validatedExecutionArgs; - const { schema, contextValue, variableValues } = validatedExecutionArgs; + const { schema, contextValue, variableValues, hideSuggestions } = + validatedExecutionArgs; const fieldName = fieldDetailsList[0].node.name.value; const fieldDef = schema.getField(parentType, fieldName); if (!fieldDef) { @@ -790,6 +806,7 @@ function executeField( fieldDef.args, variableValues, fieldDetailsList[0].fragmentVariableValues, + hideSuggestions, ); // The resolve function's optional third argument is a context value that @@ -2065,6 +2082,7 @@ function executeSubscription( contextValue, operation, variableValues, + hideSuggestions, } = validatedExecutionArgs; const rootType = schema.getSubscriptionType(); @@ -2081,6 +2099,7 @@ function executeSubscription( variableValues, rootType, operation, + hideSuggestions, ); const firstRootField = groupedFieldSet.entries().next().value as [ @@ -2114,7 +2133,12 @@ function executeSubscription( // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + variableValues, + hideSuggestions, + ); // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. diff --git a/src/execution/values.ts b/src/execution/values.ts index cce0d1d12f..5040193c77 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -55,7 +55,7 @@ export function getVariableValues( schema: GraphQLSchema, varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, - options?: { maxErrors?: number }, + options?: { maxErrors?: number; hideSuggestions?: boolean }, ): VariableValuesOrErrors { const errors: Array = []; const maxErrors = options?.maxErrors; @@ -72,6 +72,7 @@ export function getVariableValues( } errors.push(error); }, + options?.hideSuggestions, ); if (errors.length === 0) { @@ -89,6 +90,7 @@ function coerceVariableValues( varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, onError: (error: GraphQLError) => void, + hideSuggestions?: Maybe, ): VariableValues { const sources: ObjMap = Object.create(null); const coerced: ObjMap = Object.create(null); @@ -105,7 +107,11 @@ function coerceVariableValues( const defaultValue = varSignature.defaultValue; if (defaultValue) { sources[varName] = { signature: varSignature }; - coerced[varName] = coerceDefaultValue(defaultValue, varType); + coerced[varName] = coerceDefaultValue( + defaultValue, + varType, + hideSuggestions, + ); } else if (isNonNullType(varType)) { const varTypeStr = inspect(varType); onError( @@ -149,6 +155,7 @@ function coerceVariableValues( }), ); }, + hideSuggestions, ); } @@ -160,6 +167,7 @@ export function getFragmentVariableValues( fragmentSignatures: ReadOnlyObjMap, variableValues: VariableValues, fragmentVariableValues?: Maybe, + hideSuggestions?: Maybe, ): VariableValues { const varSignatures: Array = []; const sources = Object.create(null); @@ -178,6 +186,7 @@ export function getFragmentVariableValues( varSignatures, variableValues, fragmentVariableValues, + hideSuggestions, ); return { sources, coerced }; @@ -195,8 +204,15 @@ export function getArgumentValues( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, variableValues?: Maybe, + hideSuggestions?: Maybe, ): { [argument: string]: unknown } { - return experimentalGetArgumentValues(node, def.args, variableValues); + return experimentalGetArgumentValues( + node, + def.args, + variableValues, + undefined, + hideSuggestions, + ); } export function experimentalGetArgumentValues( @@ -204,6 +220,7 @@ export function experimentalGetArgumentValues( argDefs: ReadonlyArray, variableValues: Maybe, fragmentVariablesValues?: Maybe, + hideSuggestions?: Maybe, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -222,6 +239,7 @@ export function experimentalGetArgumentValues( coercedValues[name] = coerceDefaultValue( argDef.defaultValue, argDef.type, + hideSuggestions, ); } else if (isNonNullType(argType)) { throw new GraphQLError( @@ -251,6 +269,7 @@ export function experimentalGetArgumentValues( coercedValues[name] = coerceDefaultValue( argDef.defaultValue, argDef.type, + hideSuggestions, ); } else if (isNonNullType(argType)) { throw new GraphQLError( @@ -277,6 +296,7 @@ export function experimentalGetArgumentValues( argType, variableValues, fragmentVariablesValues, + hideSuggestions, ); if (coercedValue === undefined) { // Note: ValuesOfCorrectTypeRule validation should catch this before @@ -310,6 +330,7 @@ export function getDirectiveValues( node: { readonly directives?: ReadonlyArray | undefined }, variableValues?: Maybe, fragmentVariableValues?: Maybe, + hideSuggestions?: Maybe, ): undefined | { [argument: string]: unknown } { const directiveNode = node.directives?.find( (directive) => directive.name.value === directiveDef.name, @@ -321,6 +342,7 @@ export function getDirectiveValues( directiveDef.args, variableValues, fragmentVariableValues, + hideSuggestions, ); } } diff --git a/src/graphql.ts b/src/graphql.ts index 7596cf524f..899edb9977 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -61,6 +61,7 @@ import type { ExecutionResult } from './execution/types.js'; export interface GraphQLArgs { schema: GraphQLSchema; source: string | Source; + hideSuggestions?: Maybe; rootValue?: unknown; contextValue?: unknown; variableValues?: Maybe<{ readonly [variable: string]: unknown }>; @@ -101,6 +102,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + hideSuggestions, } = args; // Validate Schema @@ -118,7 +120,9 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { } // Validate - const validationErrors = validate(schema, document); + const validationErrors = validate(schema, document, undefined, { + hideSuggestions, + }); if (validationErrors.length > 0) { return { errors: validationErrors }; } @@ -133,5 +137,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + hideSuggestions, }); } diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 8cc43fabd8..9e3423eb24 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -135,8 +135,14 @@ const schema = new GraphQLSchema({ function executeQuery( source: string, variableValues?: { readonly [variable: string]: unknown }, + hideSuggestions = false, ) { - return graphqlSync({ schema, source, variableValues }); + return graphqlSync({ + schema, + source, + variableValues, + hideSuggestions, + }); } describe('Type System: Enum Values', () => { @@ -192,6 +198,23 @@ describe('Type System: Enum Values', () => { }); }); + it('does not accept values not in the enum (no suggestions)', () => { + const result = executeQuery( + '{ colorEnum(fromEnum: GREENISH) }', + undefined, + true, + ); + + expectJSON(result).toDeepEqual({ + errors: [ + { + message: 'Value "GREENISH" does not exist in "Color" enum.', + locations: [{ line: 1, column: 23 }], + }, + ], + }); + }); + it('does not accept values with incorrect casing', () => { const result = executeQuery('{ colorEnum(fromEnum: green) }'); diff --git a/src/type/definition.ts b/src/type/definition.ts index f00e0d5694..a9e0d86836 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1422,12 +1422,15 @@ export class GraphQLEnumType /* */ { return enumValue.name; } - parseValue(inputValue: unknown): Maybe /* T */ { + parseValue( + inputValue: unknown, + hideSuggestions?: Maybe, + ): Maybe /* T */ { if (typeof inputValue !== 'string') { const valueStr = inspect(inputValue); throw new GraphQLError( `Enum "${this.name}" cannot represent non-string value: ${valueStr}.` + - didYouMeanEnumValue(this, valueStr), + (hideSuggestions ? '' : didYouMeanEnumValue(this, valueStr)), ); } @@ -1435,7 +1438,7 @@ export class GraphQLEnumType /* */ { if (enumValue == null) { throw new GraphQLError( `Value "${inputValue}" does not exist in "${this.name}" enum.` + - didYouMeanEnumValue(this, inputValue), + (hideSuggestions ? '' : didYouMeanEnumValue(this, inputValue)), ); } return enumValue.value; @@ -1445,17 +1448,21 @@ export class GraphQLEnumType /* */ { parseLiteral( valueNode: ValueNode, _variables: Maybe>, + hideSuggestions?: Maybe, ): Maybe /* T */ { // Note: variables will be resolved to a value before calling this function. - return this.parseConstLiteral(valueNode as ConstValueNode); + return this.parseConstLiteral(valueNode as ConstValueNode, hideSuggestions); } - parseConstLiteral(valueNode: ConstValueNode): Maybe /* T */ { + parseConstLiteral( + valueNode: ConstValueNode, + hideSuggestions?: Maybe, + ): Maybe /* T */ { if (valueNode.kind !== Kind.ENUM) { const valueStr = print(valueNode); throw new GraphQLError( `Enum "${this.name}" cannot represent non-enum value: ${valueStr}.` + - didYouMeanEnumValue(this, valueStr), + (hideSuggestions ? '' : didYouMeanEnumValue(this, valueStr)), { nodes: valueNode }, ); } @@ -1465,7 +1472,7 @@ export class GraphQLEnumType /* */ { const valueStr = print(valueNode); throw new GraphQLError( `Value "${valueStr}" does not exist in "${this.name}" enum.` + - didYouMeanEnumValue(this, valueStr), + (hideSuggestions ? '' : didYouMeanEnumValue(this, valueStr)), { nodes: valueNode }, ); } diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index c6d10c3b9a..136786ae8c 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -50,6 +50,7 @@ interface CoerceError { function coerceValue( inputValue: unknown, type: GraphQLInputType, + hideSuggestions = false, ): CoerceResult { const errors: Array = []; const value = coerceInputValue( @@ -58,6 +59,7 @@ function coerceValue( (path, invalidValue, error) => { errors.push({ path, value: invalidValue, error: error.message }); }, + hideSuggestions, ); return { errors, value }; @@ -183,6 +185,17 @@ describe('coerceInputValue', () => { ]); }); + it('returns an error for misspelled enum value (no suggestions)', () => { + const result = coerceValue('foo', TestEnum, true); + expectErrors(result).to.deep.equal([ + { + error: 'Value "foo" does not exist in "TestEnum" enum.', + path: [], + value: 'foo', + }, + ]); + }); + it('returns an error for incorrect value type', () => { const result1 = coerceValue(123, TestEnum); expectErrors(result1).to.deep.equal([ @@ -203,6 +216,27 @@ describe('coerceInputValue', () => { }, ]); }); + + it('returns an error for incorrect value type (no suggestions)', () => { + const result1 = coerceValue(123, TestEnum, true); + expectErrors(result1).to.deep.equal([ + { + error: 'Enum "TestEnum" cannot represent non-string value: 123.', + path: [], + value: 123, + }, + ]); + + const result2 = coerceValue({ field: 'value' }, TestEnum, false); + expectErrors(result2).to.deep.equal([ + { + error: + 'Enum "TestEnum" cannot represent non-string value: { field: "value" }.', + path: [], + value: { field: 'value' }, + }, + ]); + }); }); describe('for GraphQLInputObject', () => { @@ -400,6 +434,23 @@ describe('coerceInputValue', () => { }, ]); }); + + it('returns error for a misspelled field without suggestions', () => { + const result = coerceValue({ bart: 123 }, TestInputObject, true); + expectErrors(result).to.deep.equal([ + { + error: 'Field "bart" is not defined by type "TestInputObject".', + path: [], + value: { bart: 123 }, + }, + { + error: + 'Exactly one key must be specified for OneOf type "TestInputObject".', + path: [], + value: { bart: 123 }, + }, + ]); + }); }); describe('for GraphQLInputObject with default value', () => { @@ -538,7 +589,7 @@ describe('coerceInputValue', () => { describe('with default onError', () => { it('throw error without path', () => { expect(() => - coerceInputValue(null, new GraphQLNonNull(GraphQLInt)), + coerceInputValue(null, new GraphQLNonNull(GraphQLInt), undefined, true), ).to.throw( 'Invalid value null: Expected non-nullable type "Int!" not to be null.', ); @@ -549,6 +600,8 @@ describe('coerceInputValue', () => { coerceInputValue( [null], new GraphQLList(new GraphQLNonNull(GraphQLInt)), + undefined, + true, ), ).to.throw( 'Invalid value null at "value[0]": Expected non-nullable type "Int!" not to be null.', @@ -565,7 +618,13 @@ describe('coerceInputLiteral', () => { variableValues?: VariableValues, ) { const ast = parseValue(valueText); - const value = coerceInputLiteral(ast, type, variableValues); + const value = coerceInputLiteral( + ast, + type, + variableValues, + undefined, + true, + ); expect(value).to.deep.equal(expected); } @@ -892,10 +951,14 @@ describe('coerceDefaultValue', () => { const defaultValueUsage = { literal: { kind: Kind.STRING, value: 'hello' }, } as const; - expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + expect(coerceDefaultValue(defaultValueUsage, spyScalar, true)).to.equal( + 'hello', + ); // Call a second time - expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + expect(coerceDefaultValue(defaultValueUsage, spyScalar, true)).to.equal( + 'hello', + ); expect(parseValueCalls).to.deep.equal(['hello']); }); }); diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index cca2010bac..2f463951a7 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -44,8 +44,15 @@ export function coerceInputValue( inputValue: unknown, type: GraphQLInputType, onError: OnErrorCB = defaultOnError, + hideSuggestions?: Maybe, ): unknown { - return coerceInputValueImpl(inputValue, type, onError, undefined); + return coerceInputValueImpl( + inputValue, + type, + onError, + undefined, + hideSuggestions, + ); } function defaultOnError( @@ -66,10 +73,17 @@ function coerceInputValueImpl( type: GraphQLInputType, onError: OnErrorCB, path: Path | undefined, + hideSuggestions?: Maybe, ): unknown { if (isNonNullType(type)) { if (inputValue != null) { - return coerceInputValueImpl(inputValue, type.ofType, onError, path); + return coerceInputValueImpl( + inputValue, + type.ofType, + onError, + path, + hideSuggestions, + ); } onError( pathToArray(path), @@ -91,11 +105,25 @@ function coerceInputValueImpl( if (isIterableObject(inputValue)) { return Array.from(inputValue, (itemValue, index) => { const itemPath = addPath(path, index, undefined); - return coerceInputValueImpl(itemValue, itemType, onError, itemPath); + return coerceInputValueImpl( + itemValue, + itemType, + onError, + itemPath, + hideSuggestions, + ); }); } // Lists accept a non-list value as a list of one. - return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + return [ + coerceInputValueImpl( + inputValue, + itemType, + onError, + path, + hideSuggestions, + ), + ]; } if (isInputObjectType(type)) { @@ -119,6 +147,7 @@ function coerceInputValueImpl( coercedValue[field.name] = coerceDefaultValue( field.defaultValue, field.type, + hideSuggestions, ); } else if (isNonNullType(field.type)) { const typeStr = inspect(field.type); @@ -138,6 +167,7 @@ function coerceInputValueImpl( field.type, onError, addPath(path, field.name, type.name), + hideSuggestions, ); } @@ -153,7 +183,7 @@ function coerceInputValueImpl( inputValue, new GraphQLError( `Field "${fieldName}" is not defined by type "${type}".` + - didYouMean(suggestions), + (hideSuggestions ? '' : didYouMean(suggestions)), ), ); } @@ -192,7 +222,7 @@ function coerceInputValueImpl( // which can throw to indicate failure. If it throws, maintain a reference // to the original error. try { - parseResult = type.parseValue(inputValue); + parseResult = type.parseValue(inputValue, hideSuggestions); } catch (error) { if (error instanceof GraphQLError) { onError(pathToArray(path), inputValue, error); @@ -232,6 +262,7 @@ export function coerceInputLiteral( type: GraphQLInputType, variableValues?: Maybe, fragmentVariableValues?: Maybe, + hideSuggestions?: Maybe, ): unknown { if (valueNode.kind === Kind.VARIABLE) { const coercedVariableValue = getCoercedVariableValue( @@ -256,6 +287,7 @@ export function coerceInputLiteral( type.ofType, variableValues, fragmentVariableValues, + hideSuggestions, ); } @@ -271,6 +303,7 @@ export function coerceInputLiteral( type.ofType, variableValues, fragmentVariableValues, + hideSuggestions, ); if (itemValue === undefined) { return; // Invalid: intentionally return no value. @@ -284,6 +317,7 @@ export function coerceInputLiteral( type.ofType, variableValues, fragmentVariableValues, + hideSuggestions, ); if (itemValue === undefined) { if ( @@ -340,6 +374,7 @@ export function coerceInputLiteral( coercedValue[field.name] = coerceDefaultValue( field.defaultValue, field.type, + hideSuggestions, ); } } else { @@ -348,6 +383,7 @@ export function coerceInputLiteral( field.type, variableValues, fragmentVariableValues, + hideSuggestions, ); if (fieldValue === undefined) { return; // Invalid: intentionally return no value. @@ -375,8 +411,13 @@ export function coerceInputLiteral( return leafType.parseConstLiteral ? leafType.parseConstLiteral( replaceVariables(valueNode, variableValues, fragmentVariableValues), + hideSuggestions, ) - : leafType.parseLiteral(valueNode, variableValues?.coerced); + : leafType.parseLiteral( + valueNode, + variableValues?.coerced, + hideSuggestions, + ); } catch (_error) { // Invalid: ignore error and intentionally return no value. } @@ -402,12 +443,19 @@ function getCoercedVariableValue( export function coerceDefaultValue( defaultValue: GraphQLDefaultValueUsage, type: GraphQLInputType, + hideSuggestions?: Maybe, ): unknown { // Memoize the result of coercing the default value in a hidden field. let coercedValue = (defaultValue as any)._memoizedCoercedValue; if (coercedValue === undefined) { coercedValue = defaultValue.literal - ? coerceInputLiteral(defaultValue.literal, type) + ? coerceInputLiteral( + defaultValue.literal, + type, + undefined, + undefined, + hideSuggestions, + ) : defaultValue.value; (defaultValue as any)._memoizedCoercedValue = coercedValue; } diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index d45e7a46a4..4ec1a0ebae 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -154,6 +154,10 @@ export class SDLValidationContext extends ASTValidationContext { this._schema = schema; } + get hideSuggestions() { + return false; + } + get [Symbol.toStringTag]() { return 'SDLValidationContext'; } @@ -177,24 +181,31 @@ export class ValidationContext extends ASTValidationContext { OperationDefinitionNode, ReadonlyArray >; + private _hideSuggestions: boolean; constructor( schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, onError: (error: GraphQLError) => void, + hideSuggestions?: Maybe, ) { super(ast, onError); this._schema = schema; this._typeInfo = typeInfo; this._variableUsages = new Map(); this._recursiveVariableUsages = new Map(); + this._hideSuggestions = hideSuggestions ?? false; } get [Symbol.toStringTag]() { return 'ValidationContext'; } + get hideSuggestions() { + return this._hideSuggestions; + } + getSchema(): GraphQLSchema { return this._schema; } diff --git a/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts b/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts index 1c7fbc0351..6e77055b3c 100644 --- a/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts +++ b/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts @@ -12,11 +12,12 @@ import { validate } from '../validate.js'; import { expectValidationErrorsWithSchema } from './harness.js'; -function expectErrors(queryStr: string) { +function expectErrors(queryStr: string, hideSuggestions = false) { return expectValidationErrorsWithSchema( testSchema, FieldsOnCorrectTypeRule, queryStr, + hideSuggestions, ); } @@ -140,6 +141,22 @@ describe('Validate: Fields on correct type', () => { ]); }); + it('Field not defined on fragment (no suggestions)', () => { + expectErrors( + ` + fragment fieldNotDefined on Dog { + meowVolume + } + `, + true, + ).toDeepEqual([ + { + message: 'Cannot query field "meowVolume" on type "Dog".', + locations: [{ line: 3, column: 9 }], + }, + ]); + }); + it('Ignores deeply unknown field', () => { expectErrors(` fragment deepFieldNotDefined on Dog { diff --git a/src/validation/__tests__/KnownArgumentNamesRule-test.ts b/src/validation/__tests__/KnownArgumentNamesRule-test.ts index 28e3b564cb..073f9eded3 100644 --- a/src/validation/__tests__/KnownArgumentNamesRule-test.ts +++ b/src/validation/__tests__/KnownArgumentNamesRule-test.ts @@ -14,8 +14,12 @@ import { expectValidationErrors, } from './harness.js'; -function expectErrors(queryStr: string) { - return expectValidationErrors(KnownArgumentNamesRule, queryStr); +function expectErrors(queryStr: string, hideSuggestions = false) { + return expectValidationErrors( + KnownArgumentNamesRule, + queryStr, + hideSuggestions, + ); } function expectValid(queryStr: string) { @@ -161,6 +165,22 @@ describe('Validate: Known argument names', () => { ]); }); + it('misspelled directive args are reported (no suggestions)', () => { + expectErrors( + ` + { + dog @skip(iff: true) + } + `, + true, + ).toDeepEqual([ + { + message: 'Unknown argument "iff" on directive "@skip".', + locations: [{ line: 3, column: 19 }], + }, + ]); + }); + it('arg passed to fragment without arg is reported', () => { expectErrors(` { @@ -198,6 +218,27 @@ describe('Validate: Known argument names', () => { ]); }); + it('misspelled fragment args are reported (no suggestions)', () => { + expectErrors( + ` + { + dog { + ...withArg(command: SIT) + } + } + fragment withArg($dogCommand: DogCommand) on Dog { + doesKnowCommand(dogCommand: $dogCommand) + } + `, + true, + ).toDeepEqual([ + { + message: 'Unknown argument "command" on fragment "withArg".', + locations: [{ line: 4, column: 22 }], + }, + ]); + }); + it('invalid arg name', () => { expectErrors(` fragment invalidArgName on Dog { @@ -225,6 +266,23 @@ describe('Validate: Known argument names', () => { ]); }); + it('misspelled arg name is reported (no suggestions)', () => { + expectErrors( + ` + fragment invalidArgName on Dog { + doesKnowCommand(DogCommand: true) + } + `, + true, + ).toDeepEqual([ + { + message: + 'Unknown argument "DogCommand" on field "Dog.doesKnowCommand".', + locations: [{ line: 3, column: 25 }], + }, + ]); + }); + it('unknown args amongst known args', () => { expectErrors(` fragment oneGoodArgOneInvalidArg on Dog { diff --git a/src/validation/__tests__/KnownTypeNamesRule-test.ts b/src/validation/__tests__/KnownTypeNamesRule-test.ts index 0440c094d0..c01b93638b 100644 --- a/src/validation/__tests__/KnownTypeNamesRule-test.ts +++ b/src/validation/__tests__/KnownTypeNamesRule-test.ts @@ -12,8 +12,8 @@ import { expectValidationErrorsWithSchema, } from './harness.js'; -function expectErrors(queryStr: string) { - return expectValidationErrors(KnownTypeNamesRule, queryStr); +function expectErrors(queryStr: string, hideSuggestions = false) { + return expectValidationErrors(KnownTypeNamesRule, queryStr, hideSuggestions); } function expectErrorsWithSchema(schema: GraphQLSchema, queryStr: string) { @@ -78,6 +78,36 @@ describe('Validate: Known type names', () => { ]); }); + it('unknown type names are invalid (no suggestions)', () => { + expectErrors( + ` + query Foo($var: [JumbledUpLetters!]!) { + user(id: 4) { + name + pets { ... on Badger { name }, ...PetFields } + } + } + fragment PetFields on Peat { + name + } + `, + true, + ).toDeepEqual([ + { + message: 'Unknown type "JumbledUpLetters".', + locations: [{ line: 2, column: 24 }], + }, + { + message: 'Unknown type "Badger".', + locations: [{ line: 5, column: 25 }], + }, + { + message: 'Unknown type "Peat".', + locations: [{ line: 8, column: 29 }], + }, + ]); + }); + it('references to standard scalars that are missing in schema', () => { const schema = buildSchema('type Query { foo: String }'); const query = ` diff --git a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts index 819d103e6a..0959237ceb 100644 --- a/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts +++ b/src/validation/__tests__/ValuesOfCorrectTypeRule-test.ts @@ -19,8 +19,12 @@ import { expectValidationErrorsWithSchema, } from './harness.js'; -function expectErrors(queryStr: string) { - return expectValidationErrors(ValuesOfCorrectTypeRule, queryStr); +function expectErrors(queryStr: string, hideSuggestions = false) { + return expectValidationErrors( + ValuesOfCorrectTypeRule, + queryStr, + hideSuggestions, + ); } function expectErrorsWithSchema(schema: GraphQLSchema, queryStr: string) { @@ -526,6 +530,24 @@ describe('Validate: Values of correct type', () => { ]); }); + it('String into Enum (no suggestion)', () => { + expectErrors( + ` + { + dog { + doesKnowCommand(dogCommand: "SIT") + } + } + `, + true, + ).toDeepEqual([ + { + message: 'Enum "DogCommand" cannot represent non-enum value: "SIT".', + locations: [{ line: 4, column: 41 }], + }, + ]); + }); + it('Boolean into Enum', () => { expectErrors(` { @@ -571,6 +593,24 @@ describe('Validate: Values of correct type', () => { }, ]); }); + + it('Different case Enum Value into Enum (no suggestion)', () => { + expectErrors( + ` + { + dog { + doesKnowCommand(dogCommand: sit) + } + } + `, + true, + ).toDeepEqual([ + { + message: 'Value "sit" does not exist in "DogCommand" enum.', + locations: [{ line: 4, column: 41 }], + }, + ]); + }); }); describe('Valid List value', () => { @@ -968,6 +1008,28 @@ describe('Validate: Values of correct type', () => { ]); }); + it('Partial object, unknown field arg (no suggestions)', () => { + expectErrors( + ` + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + invalidField: "value" + }) + } + } + `, + true, + ).toDeepEqual([ + { + message: + 'Field "invalidField" is not defined by type "ComplexInput".', + locations: [{ line: 6, column: 15 }], + }, + ]); + }); + it('reports original error for custom scalar which throws', () => { const customScalar = new GraphQLScalarType({ name: 'Invalid', diff --git a/src/validation/__tests__/harness.ts b/src/validation/__tests__/harness.ts index 0db861f45b..cb0c424a0e 100644 --- a/src/validation/__tests__/harness.ts +++ b/src/validation/__tests__/harness.ts @@ -128,17 +128,24 @@ export function expectValidationErrorsWithSchema( schema: GraphQLSchema, rule: ValidationRule, queryStr: string, + hideSuggestions = false, ): any { const doc = parse(queryStr, { experimentalFragmentArguments: true }); - const errors = validate(schema, doc, [rule]); + const errors = validate(schema, doc, [rule], { hideSuggestions }); return expectJSON(errors); } export function expectValidationErrors( rule: ValidationRule, queryStr: string, + hideSuggestions = false, ): any { - return expectValidationErrorsWithSchema(testSchema, rule, queryStr); + return expectValidationErrorsWithSchema( + testSchema, + rule, + queryStr, + hideSuggestions, + ); } export function expectSDLValidationErrors( diff --git a/src/validation/rules/FieldsOnCorrectTypeRule.ts b/src/validation/rules/FieldsOnCorrectTypeRule.ts index c6fce9e89b..c8afc1a402 100644 --- a/src/validation/rules/FieldsOnCorrectTypeRule.ts +++ b/src/validation/rules/FieldsOnCorrectTypeRule.ts @@ -45,12 +45,18 @@ export function FieldsOnCorrectTypeRule( // First determine if there are any suggested types to condition on. let suggestion = didYouMean( 'to use an inline fragment on', - getSuggestedTypeNames(schema, type, fieldName), + context.hideSuggestions + ? [] + : getSuggestedTypeNames(schema, type, fieldName), ); // If there are no suggested types, then perhaps this was a typo? if (suggestion === '') { - suggestion = didYouMean(getSuggestedFieldNames(type, fieldName)); + suggestion = didYouMean( + context.hideSuggestions + ? [] + : getSuggestedFieldNames(type, fieldName), + ); } // Report an error, including helpful suggestions. diff --git a/src/validation/rules/KnownArgumentNamesRule.ts b/src/validation/rules/KnownArgumentNamesRule.ts index 9e88d5a99d..cbf3e94b37 100644 --- a/src/validation/rules/KnownArgumentNamesRule.ts +++ b/src/validation/rules/KnownArgumentNamesRule.ts @@ -34,12 +34,14 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { ); if (!varDef) { const argName = argNode.name.value; - const suggestions = suggestionList( - argName, - Array.from(fragmentSignature.variableDefinitions.values()).map( - (varSignature) => varSignature.variable.name.value, - ), - ); + const suggestions = context.hideSuggestions + ? [] + : suggestionList( + argName, + Array.from(fragmentSignature.variableDefinitions.values()).map( + (varSignature) => varSignature.variable.name.value, + ), + ); context.reportError( new GraphQLError( `Unknown argument "${argName}" on fragment "${fragmentSignature.definition.name.value}".` + @@ -57,10 +59,12 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { if (!argDef && fieldDef && parentType) { const argName = argNode.name.value; - const suggestions = suggestionList( - argName, - fieldDef.args.map((arg) => arg.name), - ); + const suggestions = context.hideSuggestions + ? [] + : suggestionList( + argName, + fieldDef.args.map((arg) => arg.name), + ); context.reportError( new GraphQLError( `Unknown argument "${argName}" on field "${parentType}.${fieldDef.name}".` + @@ -119,7 +123,7 @@ export function KnownArgumentNamesOnDirectivesRule( context.reportError( new GraphQLError( `Unknown argument "${argName}" on directive "@${directiveName}".` + - didYouMean(suggestions), + (context.hideSuggestions ? '' : didYouMean(suggestions)), { nodes: argNode }, ), ); diff --git a/src/validation/rules/KnownTypeNamesRule.ts b/src/validation/rules/KnownTypeNamesRule.ts index 789e93eac1..d36a57da14 100644 --- a/src/validation/rules/KnownTypeNamesRule.ts +++ b/src/validation/rules/KnownTypeNamesRule.ts @@ -48,10 +48,12 @@ export function KnownTypeNamesRule( return; } - const suggestedTypes = suggestionList( - typeName, - isSDL ? [...standardTypeNames, ...typeNames] : [...typeNames], - ); + const suggestedTypes = context.hideSuggestions + ? [] + : suggestionList( + typeName, + isSDL ? [...standardTypeNames, ...typeNames] : [...typeNames], + ); context.reportError( new GraphQLError( `Unknown type "${typeName}".` + didYouMean(suggestedTypes), diff --git a/src/validation/rules/PossibleTypeExtensionsRule.ts b/src/validation/rules/PossibleTypeExtensionsRule.ts index d9ccb73cfa..57f2b5541b 100644 --- a/src/validation/rules/PossibleTypeExtensionsRule.ts +++ b/src/validation/rules/PossibleTypeExtensionsRule.ts @@ -78,11 +78,10 @@ export function PossibleTypeExtensionsRule( ...Object.keys(schema?.getTypeMap() ?? {}), ]; - const suggestedTypes = suggestionList(typeName, allTypeNames); context.reportError( new GraphQLError( `Cannot extend type "${typeName}" because it is not defined.` + - didYouMean(suggestedTypes), + didYouMean(suggestionList(typeName, allTypeNames)), { nodes: node.name }, ), ); diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 2322ca4fdb..2603f6dbb3 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -51,6 +51,7 @@ export function SingleFieldSubscriptionsRule( variableValues, subscriptionType, node, + context.hideSuggestions, ); if (groupedFieldSet.size > 1) { const fieldDetailsLists = [...groupedFieldSet.values()]; diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 73357e1317..cfb5a2be3f 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -97,10 +97,12 @@ export function ValuesOfCorrectTypeRule( const parentType = getNamedType(context.getParentInputType()); const fieldType = context.getInputType(); if (!fieldType && isInputObjectType(parentType)) { - const suggestions = suggestionList( - node.name.value, - Object.keys(parentType.getFields()), - ); + const suggestions = context.hideSuggestions + ? [] + : suggestionList( + node.name.value, + Object.keys(parentType.getFields()), + ); context.reportError( new GraphQLError( `Field "${node.name.value}" is not defined by type "${parentType}".` + @@ -157,8 +159,8 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { // which may throw or return undefined to indicate an invalid value. try { const parseResult = type.parseConstLiteral - ? type.parseConstLiteral(replaceVariables(node)) - : type.parseLiteral(node, undefined); + ? type.parseConstLiteral(replaceVariables(node), context.hideSuggestions) + : type.parseLiteral(node, undefined, context.hideSuggestions); if (parseResult === undefined) { const typeStr = inspect(locationType); context.reportError( diff --git a/src/validation/validate.ts b/src/validation/validate.ts index e380d167d9..05eeb39dbb 100644 --- a/src/validation/validate.ts +++ b/src/validation/validate.ts @@ -41,9 +41,10 @@ export function validate( schema: GraphQLSchema, documentAST: DocumentNode, rules: ReadonlyArray = specifiedRules, - options?: { maxErrors?: number }, + options?: { maxErrors?: number; hideSuggestions?: Maybe }, ): ReadonlyArray { const maxErrors = options?.maxErrors ?? 100; + const hideSuggestions = options?.hideSuggestions ?? false; // If the schema used for validation is invalid, throw an error. assertValidSchema(schema); @@ -63,6 +64,7 @@ export function validate( } errors.push(error); }, + hideSuggestions, ); // This uses a specialized visitor which runs multiple visitors in parallel,