diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index bd653e4694..323b3181d7 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -54,6 +54,7 @@ interface CollectFieldsContext { variableValues: VariableValues; operation: OperationDefinitionNode; runtimeType: GraphQLObjectType; + maskSuggestions: boolean; visitedFragmentNames: Set; } @@ -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, + maskSuggestions: boolean, ): { groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; @@ -84,6 +87,7 @@ export function collectFields( variableValues, runtimeType, operation, + maskSuggestions, visitedFragmentNames: new Set(), }; @@ -114,6 +118,7 @@ export function collectSubfields( operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldDetailsList: FieldDetailsList, + maskSuggestions: boolean, ): { groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; @@ -125,6 +130,7 @@ export function collectSubfields( runtimeType: returnType, operation, visitedFragmentNames: new Set(), + maskSuggestions, }; const subGroupedFieldSet = new AccumulatorMap(); const newDeferUsages: Array = []; @@ -165,6 +171,7 @@ function collectFieldsImpl( variableValues, runtimeType, operation, + maskSuggestions, visitedFragmentNames, } = context; @@ -263,6 +270,7 @@ function collectFieldsImpl( newFragmentVariableValues = getFragmentVariableValues( selection, fragmentVariableSignatures, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -310,6 +318,7 @@ function getDeferUsage( const defer = getDirectiveValues( GraphQLDeferDirective, node, + false, variableValues, fragmentVariableValues, ); @@ -345,6 +354,7 @@ function shouldIncludeNode( const skip = getDirectiveValues( GraphQLSkipDirective, node, + false, variableValues, fragmentVariableValues, ); @@ -355,6 +365,7 @@ function shouldIncludeNode( const include = getDirectiveValues( GraphQLIncludeDirective, node, + false, variableValues, fragmentVariableValues, ); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index af4e6b9928..7d393fdc9a 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, maskSuggestions } = validatedExecutionArgs; return _collectSubfields( schema, @@ -107,6 +107,7 @@ const collectSubfields = memoize3( operation, returnType, fieldDetailsList, + maskSuggestions, ); }, ); @@ -155,6 +156,7 @@ export interface ValidatedExecutionArgs { validatedExecutionArgs: ValidatedExecutionArgs, ) => PromiseOrValue; enableEarlyExecution: boolean; + maskSuggestions: boolean; } export interface ExecutionContext { @@ -184,6 +186,7 @@ export interface ExecutionArgs { ) => PromiseOrValue >; enableEarlyExecution?: Maybe; + maskSuggestions?: 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, + maskSuggestions, + } = validatedExecutionArgs; const rootType = schema.getRootType(operation.operation); if (rootType == null) { throw new GraphQLError( @@ -324,6 +333,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent( variableValues, rootType, operation, + maskSuggestions, ); 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 maskSuggestions = args.maskSuggestions ?? false; const variableValuesOrErrors = getVariableValues( schema, variableDefinitions, rawVariableValues ?? {}, - { maxErrors: 50 }, + { + maxErrors: 50, + maskSuggestions, + }, ); if (variableValuesOrErrors.errors) { @@ -579,6 +593,7 @@ export function validateExecutionArgs( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, perEventExecutor: perEventExecutor ?? executeSubscriptionEvent, enableEarlyExecution: enableEarlyExecution === true, + maskSuggestions, }; } @@ -762,7 +777,8 @@ function executeField( deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { const validatedExecutionArgs = exeContext.validatedExecutionArgs; - const { schema, contextValue, variableValues } = validatedExecutionArgs; + const { schema, contextValue, variableValues, maskSuggestions } = + validatedExecutionArgs; const fieldName = fieldDetailsList[0].node.name.value; const fieldDef = schema.getField(parentType, fieldName); if (!fieldDef) { @@ -788,6 +804,7 @@ function executeField( const args = experimentalGetArgumentValues( fieldDetailsList[0].node, fieldDef.args, + maskSuggestions, variableValues, fieldDetailsList[0].fragmentVariableValues, ); @@ -1099,6 +1116,7 @@ function getStreamUsage( const stream = getDirectiveValues( GraphQLStreamDirective, fieldDetailsList[0].node, + false, variableValues, fieldDetailsList[0].fragmentVariableValues, ); @@ -2065,6 +2083,7 @@ function executeSubscription( contextValue, operation, variableValues, + maskSuggestions, } = validatedExecutionArgs; const rootType = schema.getSubscriptionType(); @@ -2081,6 +2100,7 @@ function executeSubscription( variableValues, rootType, operation, + maskSuggestions, ); const firstRootField = groupedFieldSet.entries().next().value as [ @@ -2114,7 +2134,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], + maskSuggestions, + variableValues, + ); // 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..84c88bedad 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -55,10 +55,11 @@ export function getVariableValues( schema: GraphQLSchema, varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, - options?: { maxErrors?: number }, + options?: { maxErrors?: number; maskSuggestions?: boolean }, ): VariableValuesOrErrors { const errors: Array = []; const maxErrors = options?.maxErrors; + const maskSuggestions = options?.maskSuggestions; try { const variableValues = coerceVariableValues( schema, @@ -72,6 +73,7 @@ export function getVariableValues( } errors.push(error); }, + maskSuggestions, ); if (errors.length === 0) { @@ -89,6 +91,7 @@ function coerceVariableValues( varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, onError: (error: GraphQLError) => void, + maskSuggestions: boolean | undefined, ): VariableValues { const sources: ObjMap = Object.create(null); const coerced: ObjMap = Object.create(null); @@ -105,7 +108,11 @@ function coerceVariableValues( const defaultValue = varSignature.defaultValue; if (defaultValue) { sources[varName] = { signature: varSignature }; - coerced[varName] = coerceDefaultValue(defaultValue, varType); + coerced[varName] = coerceDefaultValue( + defaultValue, + varType, + maskSuggestions, + ); } else if (isNonNullType(varType)) { const varTypeStr = inspect(varType); onError( @@ -136,6 +143,7 @@ function coerceVariableValues( coerced[varName] = coerceInputValue( value, varType, + maskSuggestions, (path, invalidValue, error) => { let prefix = `Variable "$${varName}" got invalid value ` + inspect(invalidValue); @@ -158,6 +166,7 @@ function coerceVariableValues( export function getFragmentVariableValues( fragmentSpreadNode: FragmentSpreadNode, fragmentSignatures: ReadOnlyObjMap, + maskSuggestions: boolean, variableValues: VariableValues, fragmentVariableValues?: Maybe, ): VariableValues { @@ -176,6 +185,7 @@ export function getFragmentVariableValues( const coerced = experimentalGetArgumentValues( fragmentSpreadNode, varSignatures, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -194,15 +204,22 @@ export function getFragmentVariableValues( export function getArgumentValues( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, + maskSuggestions?: boolean | undefined, variableValues?: Maybe, ): { [argument: string]: unknown } { - return experimentalGetArgumentValues(node, def.args, variableValues); + return experimentalGetArgumentValues( + node, + def.args, + maskSuggestions, + variableValues, + ); } export function experimentalGetArgumentValues( node: FieldNode | DirectiveNode | FragmentSpreadNode, argDefs: ReadonlyArray, - variableValues: Maybe, + maskSuggestions?: boolean | undefined, + variableValues?: Maybe, fragmentVariablesValues?: Maybe, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -222,6 +239,7 @@ export function experimentalGetArgumentValues( coercedValues[name] = coerceDefaultValue( argDef.defaultValue, argDef.type, + maskSuggestions, ); } else if (isNonNullType(argType)) { throw new GraphQLError( @@ -251,6 +269,7 @@ export function experimentalGetArgumentValues( coercedValues[name] = coerceDefaultValue( argDef.defaultValue, argDef.type, + maskSuggestions, ); } else if (isNonNullType(argType)) { throw new GraphQLError( @@ -275,6 +294,7 @@ export function experimentalGetArgumentValues( const coercedValue = coerceInputLiteral( valueNode, argType, + maskSuggestions, variableValues, fragmentVariablesValues, ); @@ -308,6 +328,7 @@ export function experimentalGetArgumentValues( export function getDirectiveValues( directiveDef: GraphQLDirective, node: { readonly directives?: ReadonlyArray | undefined }, + maskSuggestions?: boolean | undefined, variableValues?: Maybe, fragmentVariableValues?: Maybe, ): undefined | { [argument: string]: unknown } { @@ -319,6 +340,7 @@ export function getDirectiveValues( return experimentalGetArgumentValues( directiveNode, directiveDef.args, + maskSuggestions, variableValues, fragmentVariableValues, ); diff --git a/src/graphql.ts b/src/graphql.ts index 7596cf524f..7fcda20e05 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,6 +67,7 @@ export interface GraphQLArgs { operationName?: Maybe; fieldResolver?: Maybe>; typeResolver?: Maybe>; + maskSuggestions?: boolean; } export function graphql(args: GraphQLArgs): Promise { @@ -101,6 +102,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + maskSuggestions, } = args; // Validate Schema @@ -118,7 +120,9 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { } // Validate - const validationErrors = validate(schema, document); + const validationErrors = validate(schema, document, undefined, { + maskSuggestions: maskSuggestions ?? false, + }); if (validationErrors.length > 0) { return { errors: validationErrors }; } @@ -133,5 +137,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue { operationName, fieldResolver, typeResolver, + maskSuggestions, }); } diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 8cc43fabd8..9f0671c12d 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 }, + maskSuggestions: boolean = false, ) { - return graphqlSync({ schema, source, variableValues }); + return graphqlSync({ + schema, + source, + variableValues, + maskSuggestions, + }); } 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..ed3a78366c 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, + maskSuggestions?: boolean | undefined, + ): 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), + (maskSuggestions ? '' : 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), + (maskSuggestions ? '' : didYouMeanEnumValue(this, inputValue)), ); } return enumValue.value; @@ -1445,17 +1448,21 @@ export class GraphQLEnumType /* */ { parseLiteral( valueNode: ValueNode, _variables: Maybe>, + maskSuggestions?: boolean | undefined, ): 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, maskSuggestions); } - parseConstLiteral(valueNode: ConstValueNode): Maybe /* T */ { + parseConstLiteral( + valueNode: ConstValueNode, + maskSuggestions?: 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), + (maskSuggestions ? '' : 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), + (maskSuggestions ? '' : didYouMeanEnumValue(this, valueStr)), { nodes: valueNode }, ); } diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index c6d10c3b9a..92cad3f198 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -50,11 +50,13 @@ interface CoerceError { function coerceValue( inputValue: unknown, type: GraphQLInputType, + maskSuggestions: boolean = false, ): CoerceResult { const errors: Array = []; const value = coerceInputValue( inputValue, type, + maskSuggestions, (path, invalidValue, error) => { errors.push({ path, value: invalidValue, error: error.message }); }, @@ -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), true), ).to.throw( 'Invalid value null: Expected non-nullable type "Int!" not to be null.', ); @@ -549,6 +600,7 @@ describe('coerceInputValue', () => { coerceInputValue( [null], new GraphQLList(new GraphQLNonNull(GraphQLInt)), + true, ), ).to.throw( 'Invalid value null at "value[0]": Expected non-nullable type "Int!" not to be null.', @@ -565,7 +617,7 @@ describe('coerceInputLiteral', () => { variableValues?: VariableValues, ) { const ast = parseValue(valueText); - const value = coerceInputLiteral(ast, type, variableValues); + const value = coerceInputLiteral(ast, type, true, variableValues); expect(value).to.deep.equal(expected); } @@ -892,10 +944,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..a748a80dfd 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -43,9 +43,16 @@ type OnErrorCB = ( export function coerceInputValue( inputValue: unknown, type: GraphQLInputType, + maskSuggestions?: boolean, onError: OnErrorCB = defaultOnError, ): unknown { - return coerceInputValueImpl(inputValue, type, onError, undefined); + return coerceInputValueImpl( + inputValue, + type, + onError, + undefined, + maskSuggestions ?? false, + ); } function defaultOnError( @@ -66,10 +73,17 @@ function coerceInputValueImpl( type: GraphQLInputType, onError: OnErrorCB, path: Path | undefined, + maskSuggestions: boolean, ): unknown { if (isNonNullType(type)) { if (inputValue != null) { - return coerceInputValueImpl(inputValue, type.ofType, onError, path); + return coerceInputValueImpl( + inputValue, + type.ofType, + onError, + path, + maskSuggestions, + ); } 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, + maskSuggestions, + ); }); } // Lists accept a non-list value as a list of one. - return [coerceInputValueImpl(inputValue, itemType, onError, path)]; + return [ + coerceInputValueImpl( + inputValue, + itemType, + onError, + path, + maskSuggestions, + ), + ]; } if (isInputObjectType(type)) { @@ -119,6 +147,7 @@ function coerceInputValueImpl( coercedValue[field.name] = coerceDefaultValue( field.defaultValue, field.type, + maskSuggestions, ); } 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), + maskSuggestions, ); } @@ -153,7 +183,7 @@ function coerceInputValueImpl( inputValue, new GraphQLError( `Field "${fieldName}" is not defined by type "${type}".` + - didYouMean(suggestions), + (maskSuggestions ? '' : 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, maskSuggestions); } catch (error) { if (error instanceof GraphQLError) { onError(pathToArray(path), inputValue, error); @@ -230,6 +260,7 @@ function coerceInputValueImpl( export function coerceInputLiteral( valueNode: ValueNode, type: GraphQLInputType, + maskSuggestions?: boolean, variableValues?: Maybe, fragmentVariableValues?: Maybe, ): unknown { @@ -254,6 +285,7 @@ export function coerceInputLiteral( return coerceInputLiteral( valueNode, type.ofType, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -269,6 +301,7 @@ export function coerceInputLiteral( const itemValue = coerceInputLiteral( valueNode, type.ofType, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -282,6 +315,7 @@ export function coerceInputLiteral( let itemValue = coerceInputLiteral( itemNode, type.ofType, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -340,12 +374,14 @@ export function coerceInputLiteral( coercedValue[field.name] = coerceDefaultValue( field.defaultValue, field.type, + maskSuggestions, ); } } else { const fieldValue = coerceInputLiteral( fieldNode.value, field.type, + maskSuggestions, variableValues, fragmentVariableValues, ); @@ -375,8 +411,13 @@ export function coerceInputLiteral( return leafType.parseConstLiteral ? leafType.parseConstLiteral( replaceVariables(valueNode, variableValues, fragmentVariableValues), + maskSuggestions, ) - : leafType.parseLiteral(valueNode, variableValues?.coerced); + : leafType.parseLiteral( + valueNode, + variableValues?.coerced, + maskSuggestions, + ); } catch (_error) { // Invalid: ignore error and intentionally return no value. } @@ -402,12 +443,13 @@ function getCoercedVariableValue( export function coerceDefaultValue( defaultValue: GraphQLDefaultValueUsage, type: GraphQLInputType, + maskSuggestions?: boolean, ): 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, maskSuggestions) : defaultValue.value; (defaultValue as any)._memoizedCoercedValue = coercedValue; } diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index e0ff5d8bf2..c871d39e71 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -148,7 +148,7 @@ export function valueFromAST( // no value is returned. let result; try { - result = type.parseLiteral(valueNode, variables); + result = type.parseLiteral(valueNode, variables, true); } catch (_error) { return; // Invalid: intentionally return no value. } diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index d45e7a46a4..3b057276d2 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -154,6 +154,10 @@ export class SDLValidationContext extends ASTValidationContext { this._schema = schema; } + get maskSuggestions() { + return false; + } + get [Symbol.toStringTag]() { return 'SDLValidationContext'; } @@ -177,24 +181,31 @@ export class ValidationContext extends ASTValidationContext { OperationDefinitionNode, ReadonlyArray >; + private _maskSuggestions: boolean; constructor( schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, onError: (error: GraphQLError) => void, + maskSuggestions?: boolean, ) { super(ast, onError); this._schema = schema; this._typeInfo = typeInfo; this._variableUsages = new Map(); this._recursiveVariableUsages = new Map(); + this._maskSuggestions = maskSuggestions ?? false; } get [Symbol.toStringTag]() { return 'ValidationContext'; } + get maskSuggestions() { + return this._maskSuggestions; + } + getSchema(): GraphQLSchema { return this._schema; } diff --git a/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts b/src/validation/__tests__/FieldsOnCorrectTypeRule-test.ts index 1c7fbc0351..838d9b41fe 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, maskSuggestions: boolean = false) { return expectValidationErrorsWithSchema( testSchema, FieldsOnCorrectTypeRule, queryStr, + maskSuggestions, ); } @@ -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..32cd8cb600 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, maskSuggestions: boolean = false) { + return expectValidationErrors( + KnownArgumentNamesRule, + queryStr, + maskSuggestions, + ); } 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..0c7861ed9b 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, maskSuggestions: boolean = false) { + return expectValidationErrors(KnownTypeNamesRule, queryStr, maskSuggestions); } 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..9762463017 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, maskSuggestions: boolean = false) { + return expectValidationErrors( + ValuesOfCorrectTypeRule, + queryStr, + maskSuggestions, + ); } 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..9d81667010 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, + maskSuggestions: boolean = false, ): any { const doc = parse(queryStr, { experimentalFragmentArguments: true }); - const errors = validate(schema, doc, [rule]); + const errors = validate(schema, doc, [rule], { maskSuggestions }); return expectJSON(errors); } export function expectValidationErrors( rule: ValidationRule, queryStr: string, + maskSuggestions = false, ): any { - return expectValidationErrorsWithSchema(testSchema, rule, queryStr); + return expectValidationErrorsWithSchema( + testSchema, + rule, + queryStr, + maskSuggestions, + ); } export function expectSDLValidationErrors( diff --git a/src/validation/rules/FieldsOnCorrectTypeRule.ts b/src/validation/rules/FieldsOnCorrectTypeRule.ts index c6fce9e89b..3637d5cfd2 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.maskSuggestions + ? [] + : 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.maskSuggestions + ? [] + : 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..258899fdb4 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.maskSuggestions + ? [] + : 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.maskSuggestions + ? [] + : 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.maskSuggestions ? '' : didYouMean(suggestions)), { nodes: argNode }, ), ); diff --git a/src/validation/rules/KnownTypeNamesRule.ts b/src/validation/rules/KnownTypeNamesRule.ts index 789e93eac1..516d3297b2 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.maskSuggestions + ? [] + : suggestionList( + typeName, + isSDL ? [...standardTypeNames, ...typeNames] : [...typeNames], + ); context.reportError( new GraphQLError( `Unknown type "${typeName}".` + didYouMean(suggestedTypes), diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 2322ca4fdb..80a6980058 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -51,6 +51,7 @@ export function SingleFieldSubscriptionsRule( variableValues, subscriptionType, node, + context.maskSuggestions, ); if (groupedFieldSet.size > 1) { const fieldDetailsLists = [...groupedFieldSet.values()]; diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 73357e1317..4095bb2bf8 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.maskSuggestions + ? [] + : 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.maskSuggestions) + : type.parseLiteral(node, undefined, context.maskSuggestions); if (parseResult === undefined) { const typeStr = inspect(locationType); context.reportError( diff --git a/src/validation/validate.ts b/src/validation/validate.ts index e380d167d9..a24145b3cb 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; maskSuggestions?: boolean }, ): ReadonlyArray { const maxErrors = options?.maxErrors ?? 100; + const maskSuggestions = options?.maskSuggestions ?? false; // If the schema used for validation is invalid, throw an error. assertValidSchema(schema); @@ -63,6 +64,7 @@ export function validate( } errors.push(error); }, + maskSuggestions, ); // This uses a specialized visitor which runs multiple visitors in parallel,