Skip to content

Commit 0ea9241

Browse files
authored
feat: Allow skipping suggestions (#4214)
1 parent ace6727 commit 0ea9241

21 files changed

+466
-65
lines changed

src/execution/collectFields.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface CollectFieldsContext {
5555
operation: OperationDefinitionNode;
5656
runtimeType: GraphQLObjectType;
5757
visitedFragmentNames: Set<string>;
58+
hideSuggestions: boolean;
5859
}
5960

6061
/**
@@ -66,12 +67,14 @@ interface CollectFieldsContext {
6667
*
6768
* @internal
6869
*/
70+
// eslint-disable-next-line @typescript-eslint/max-params
6971
export function collectFields(
7072
schema: GraphQLSchema,
7173
fragments: ObjMap<FragmentDetails>,
7274
variableValues: VariableValues,
7375
runtimeType: GraphQLObjectType,
7476
operation: OperationDefinitionNode,
77+
hideSuggestions: boolean,
7578
): {
7679
groupedFieldSet: GroupedFieldSet;
7780
newDeferUsages: ReadonlyArray<DeferUsage>;
@@ -85,6 +88,7 @@ export function collectFields(
8588
runtimeType,
8689
operation,
8790
visitedFragmentNames: new Set(),
91+
hideSuggestions,
8892
};
8993

9094
collectFieldsImpl(
@@ -114,6 +118,7 @@ export function collectSubfields(
114118
operation: OperationDefinitionNode,
115119
returnType: GraphQLObjectType,
116120
fieldDetailsList: FieldDetailsList,
121+
hideSuggestions: boolean,
117122
): {
118123
groupedFieldSet: GroupedFieldSet;
119124
newDeferUsages: ReadonlyArray<DeferUsage>;
@@ -125,6 +130,7 @@ export function collectSubfields(
125130
runtimeType: returnType,
126131
operation,
127132
visitedFragmentNames: new Set(),
133+
hideSuggestions,
128134
};
129135
const subGroupedFieldSet = new AccumulatorMap<string, FieldDetails>();
130136
const newDeferUsages: Array<DeferUsage> = [];
@@ -166,6 +172,7 @@ function collectFieldsImpl(
166172
runtimeType,
167173
operation,
168174
visitedFragmentNames,
175+
hideSuggestions,
169176
} = context;
170177

171178
for (const selection of selectionSet.selections) {
@@ -265,6 +272,7 @@ function collectFieldsImpl(
265272
fragmentVariableSignatures,
266273
variableValues,
267274
fragmentVariableValues,
275+
hideSuggestions,
268276
);
269277
}
270278

src/execution/execute.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const collectSubfields = memoize3(
9898
returnType: GraphQLObjectType,
9999
fieldDetailsList: FieldDetailsList,
100100
) => {
101-
const { schema, fragments, operation, variableValues } =
101+
const { schema, fragments, operation, variableValues, hideSuggestions } =
102102
validatedExecutionArgs;
103103
return _collectSubfields(
104104
schema,
@@ -107,6 +107,7 @@ const collectSubfields = memoize3(
107107
operation,
108108
returnType,
109109
fieldDetailsList,
110+
hideSuggestions,
110111
);
111112
},
112113
);
@@ -155,6 +156,7 @@ export interface ValidatedExecutionArgs {
155156
validatedExecutionArgs: ValidatedExecutionArgs,
156157
) => PromiseOrValue<ExecutionResult>;
157158
enableEarlyExecution: boolean;
159+
hideSuggestions: boolean;
158160
}
159161

160162
export interface ExecutionContext {
@@ -184,6 +186,7 @@ export interface ExecutionArgs {
184186
) => PromiseOrValue<ExecutionResult>
185187
>;
186188
enableEarlyExecution?: Maybe<boolean>;
189+
hideSuggestions?: Maybe<boolean>;
187190
}
188191

189192
export interface StreamUsage {
@@ -308,8 +311,14 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
308311
cancellableStreams: undefined,
309312
};
310313
try {
311-
const { schema, fragments, rootValue, operation, variableValues } =
312-
validatedExecutionArgs;
314+
const {
315+
schema,
316+
fragments,
317+
rootValue,
318+
operation,
319+
variableValues,
320+
hideSuggestions,
321+
} = validatedExecutionArgs;
313322
const rootType = schema.getRootType(operation.operation);
314323
if (rootType == null) {
315324
throw new GraphQLError(
@@ -324,6 +333,7 @@ export function experimentalExecuteQueryOrMutationOrSubscriptionEvent(
324333
variableValues,
325334
rootType,
326335
operation,
336+
hideSuggestions,
327337
);
328338

329339
const { groupedFieldSet, newDeferUsages } = collectedFields;
@@ -554,12 +564,16 @@ export function validateExecutionArgs(
554564
// FIXME: https://github.com/graphql/graphql-js/issues/2203
555565
/* c8 ignore next */
556566
const variableDefinitions = operation.variableDefinitions ?? [];
567+
const hideSuggestions = args.hideSuggestions ?? false;
557568

558569
const variableValuesOrErrors = getVariableValues(
559570
schema,
560571
variableDefinitions,
561572
rawVariableValues ?? {},
562-
{ maxErrors: 50 },
573+
{
574+
maxErrors: 50,
575+
hideSuggestions,
576+
},
563577
);
564578

565579
if (variableValuesOrErrors.errors) {
@@ -579,6 +593,7 @@ export function validateExecutionArgs(
579593
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
580594
perEventExecutor: perEventExecutor ?? executeSubscriptionEvent,
581595
enableEarlyExecution: enableEarlyExecution === true,
596+
hideSuggestions,
582597
};
583598
}
584599

@@ -762,7 +777,8 @@ function executeField(
762777
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
763778
): PromiseOrValue<GraphQLWrappedResult<unknown>> | undefined {
764779
const validatedExecutionArgs = exeContext.validatedExecutionArgs;
765-
const { schema, contextValue, variableValues } = validatedExecutionArgs;
780+
const { schema, contextValue, variableValues, hideSuggestions } =
781+
validatedExecutionArgs;
766782
const fieldName = fieldDetailsList[0].node.name.value;
767783
const fieldDef = schema.getField(parentType, fieldName);
768784
if (!fieldDef) {
@@ -790,6 +806,7 @@ function executeField(
790806
fieldDef.args,
791807
variableValues,
792808
fieldDetailsList[0].fragmentVariableValues,
809+
hideSuggestions,
793810
);
794811

795812
// The resolve function's optional third argument is a context value that
@@ -2065,6 +2082,7 @@ function executeSubscription(
20652082
contextValue,
20662083
operation,
20672084
variableValues,
2085+
hideSuggestions,
20682086
} = validatedExecutionArgs;
20692087

20702088
const rootType = schema.getSubscriptionType();
@@ -2081,6 +2099,7 @@ function executeSubscription(
20812099
variableValues,
20822100
rootType,
20832101
operation,
2102+
hideSuggestions,
20842103
);
20852104

20862105
const firstRootField = groupedFieldSet.entries().next().value as [
@@ -2114,7 +2133,12 @@ function executeSubscription(
21142133

21152134
// Build a JS object of arguments from the field.arguments AST, using the
21162135
// variables scope to fulfill any variable references.
2117-
const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
2136+
const args = getArgumentValues(
2137+
fieldDef,
2138+
fieldNodes[0],
2139+
variableValues,
2140+
hideSuggestions,
2141+
);
21182142

21192143
// Call the `subscribe()` resolver or the default resolver to produce an
21202144
// AsyncIterable yielding raw payloads.

src/execution/values.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function getVariableValues(
5555
schema: GraphQLSchema,
5656
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
5757
inputs: { readonly [variable: string]: unknown },
58-
options?: { maxErrors?: number },
58+
options?: { maxErrors?: number; hideSuggestions?: boolean },
5959
): VariableValuesOrErrors {
6060
const errors: Array<GraphQLError> = [];
6161
const maxErrors = options?.maxErrors;
@@ -72,6 +72,7 @@ export function getVariableValues(
7272
}
7373
errors.push(error);
7474
},
75+
options?.hideSuggestions,
7576
);
7677

7778
if (errors.length === 0) {
@@ -89,6 +90,7 @@ function coerceVariableValues(
8990
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
9091
inputs: { readonly [variable: string]: unknown },
9192
onError: (error: GraphQLError) => void,
93+
hideSuggestions?: Maybe<boolean>,
9294
): VariableValues {
9395
const sources: ObjMap<VariableValueSource> = Object.create(null);
9496
const coerced: ObjMap<unknown> = Object.create(null);
@@ -105,7 +107,11 @@ function coerceVariableValues(
105107
const defaultValue = varSignature.defaultValue;
106108
if (defaultValue) {
107109
sources[varName] = { signature: varSignature };
108-
coerced[varName] = coerceDefaultValue(defaultValue, varType);
110+
coerced[varName] = coerceDefaultValue(
111+
defaultValue,
112+
varType,
113+
hideSuggestions,
114+
);
109115
} else if (isNonNullType(varType)) {
110116
const varTypeStr = inspect(varType);
111117
onError(
@@ -149,6 +155,7 @@ function coerceVariableValues(
149155
}),
150156
);
151157
},
158+
hideSuggestions,
152159
);
153160
}
154161

@@ -160,6 +167,7 @@ export function getFragmentVariableValues(
160167
fragmentSignatures: ReadOnlyObjMap<GraphQLVariableSignature>,
161168
variableValues: VariableValues,
162169
fragmentVariableValues?: Maybe<VariableValues>,
170+
hideSuggestions?: Maybe<boolean>,
163171
): VariableValues {
164172
const varSignatures: Array<GraphQLVariableSignature> = [];
165173
const sources = Object.create(null);
@@ -178,6 +186,7 @@ export function getFragmentVariableValues(
178186
varSignatures,
179187
variableValues,
180188
fragmentVariableValues,
189+
hideSuggestions,
181190
);
182191

183192
return { sources, coerced };
@@ -195,15 +204,23 @@ export function getArgumentValues(
195204
def: GraphQLField<unknown, unknown> | GraphQLDirective,
196205
node: FieldNode | DirectiveNode,
197206
variableValues?: Maybe<VariableValues>,
207+
hideSuggestions?: Maybe<boolean>,
198208
): { [argument: string]: unknown } {
199-
return experimentalGetArgumentValues(node, def.args, variableValues);
209+
return experimentalGetArgumentValues(
210+
node,
211+
def.args,
212+
variableValues,
213+
undefined,
214+
hideSuggestions,
215+
);
200216
}
201217

202218
export function experimentalGetArgumentValues(
203219
node: FieldNode | DirectiveNode | FragmentSpreadNode,
204220
argDefs: ReadonlyArray<GraphQLArgument | GraphQLVariableSignature>,
205221
variableValues: Maybe<VariableValues>,
206222
fragmentVariablesValues?: Maybe<VariableValues>,
223+
hideSuggestions?: Maybe<boolean>,
207224
): { [argument: string]: unknown } {
208225
const coercedValues: { [argument: string]: unknown } = {};
209226

@@ -222,6 +239,7 @@ export function experimentalGetArgumentValues(
222239
coercedValues[name] = coerceDefaultValue(
223240
argDef.defaultValue,
224241
argDef.type,
242+
hideSuggestions,
225243
);
226244
} else if (isNonNullType(argType)) {
227245
throw new GraphQLError(
@@ -251,6 +269,7 @@ export function experimentalGetArgumentValues(
251269
coercedValues[name] = coerceDefaultValue(
252270
argDef.defaultValue,
253271
argDef.type,
272+
hideSuggestions,
254273
);
255274
} else if (isNonNullType(argType)) {
256275
throw new GraphQLError(
@@ -277,6 +296,7 @@ export function experimentalGetArgumentValues(
277296
argType,
278297
variableValues,
279298
fragmentVariablesValues,
299+
hideSuggestions,
280300
);
281301
if (coercedValue === undefined) {
282302
// Note: ValuesOfCorrectTypeRule validation should catch this before
@@ -310,6 +330,7 @@ export function getDirectiveValues(
310330
node: { readonly directives?: ReadonlyArray<DirectiveNode> | undefined },
311331
variableValues?: Maybe<VariableValues>,
312332
fragmentVariableValues?: Maybe<VariableValues>,
333+
hideSuggestions?: Maybe<boolean>,
313334
): undefined | { [argument: string]: unknown } {
314335
const directiveNode = node.directives?.find(
315336
(directive) => directive.name.value === directiveDef.name,
@@ -321,6 +342,7 @@ export function getDirectiveValues(
321342
directiveDef.args,
322343
variableValues,
323344
fragmentVariableValues,
345+
hideSuggestions,
324346
);
325347
}
326348
}

src/graphql.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import type { ExecutionResult } from './execution/types.js';
6161
export interface GraphQLArgs {
6262
schema: GraphQLSchema;
6363
source: string | Source;
64+
hideSuggestions?: Maybe<boolean>;
6465
rootValue?: unknown;
6566
contextValue?: unknown;
6667
variableValues?: Maybe<{ readonly [variable: string]: unknown }>;
@@ -101,6 +102,7 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
101102
operationName,
102103
fieldResolver,
103104
typeResolver,
105+
hideSuggestions,
104106
} = args;
105107

106108
// Validate Schema
@@ -118,7 +120,9 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
118120
}
119121

120122
// Validate
121-
const validationErrors = validate(schema, document);
123+
const validationErrors = validate(schema, document, undefined, {
124+
hideSuggestions,
125+
});
122126
if (validationErrors.length > 0) {
123127
return { errors: validationErrors };
124128
}
@@ -133,5 +137,6 @@ function graphqlImpl(args: GraphQLArgs): PromiseOrValue<ExecutionResult> {
133137
operationName,
134138
fieldResolver,
135139
typeResolver,
140+
hideSuggestions,
136141
});
137142
}

src/type/__tests__/enumType-test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,14 @@ const schema = new GraphQLSchema({
135135
function executeQuery(
136136
source: string,
137137
variableValues?: { readonly [variable: string]: unknown },
138+
hideSuggestions = false,
138139
) {
139-
return graphqlSync({ schema, source, variableValues });
140+
return graphqlSync({
141+
schema,
142+
source,
143+
variableValues,
144+
hideSuggestions,
145+
});
140146
}
141147

142148
describe('Type System: Enum Values', () => {
@@ -192,6 +198,23 @@ describe('Type System: Enum Values', () => {
192198
});
193199
});
194200

201+
it('does not accept values not in the enum (no suggestions)', () => {
202+
const result = executeQuery(
203+
'{ colorEnum(fromEnum: GREENISH) }',
204+
undefined,
205+
true,
206+
);
207+
208+
expectJSON(result).toDeepEqual({
209+
errors: [
210+
{
211+
message: 'Value "GREENISH" does not exist in "Color" enum.',
212+
locations: [{ line: 1, column: 23 }],
213+
},
214+
],
215+
});
216+
});
217+
195218
it('does not accept values with incorrect casing', () => {
196219
const result = executeQuery('{ colorEnum(fromEnum: green) }');
197220

0 commit comments

Comments
 (0)