From ad69cea175b95b60ed61034997465f1498303df8 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Wed, 21 Apr 2021 00:27:35 +0900 Subject: [PATCH] RFC: Default value validation & coercion Implements https://github.com/graphql/graphql-spec/pull/793/ * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value. --- cspell.yml | 4 + src/execution/__tests__/variables-test.ts | 1 - src/index.ts | 2 - src/type/__tests__/enumType-test.ts | 4 +- src/type/__tests__/validation-test.ts | 555 ++++++++++++++++++- src/type/introspection.ts | 4 +- src/type/validate.ts | 317 ++++++++++- src/utilities/__tests__/astFromValue-test.ts | 379 ------------- src/utilities/astFromValue.ts | 150 ----- src/utilities/coerceInputValue.ts | 2 +- src/utilities/findBreakingChanges.ts | 4 +- src/utilities/index.ts | 3 - src/utilities/printSchema.ts | 4 +- 13 files changed, 867 insertions(+), 562 deletions(-) delete mode 100644 src/utilities/__tests__/astFromValue-test.ts delete mode 100644 src/utilities/astFromValue.ts diff --git a/cspell.yml b/cspell.yml index 6ae58bbd3f0..87be8e82707 100644 --- a/cspell.yml +++ b/cspell.yml @@ -27,6 +27,10 @@ ignoreRegExpList: words: - graphiql + - sublinks + - instanceof + - uncoerce + - uncoerced # Different names used inside tests - Skywalker diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index 21aa0636b80..21ca1336f82 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -126,7 +126,6 @@ const TestType = new GraphQLObjectType({ }), fieldWithNestedInputObject: fieldWithInputArg({ type: TestNestedInputObject, - defaultValue: 'Hello World', }), list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }), nnList: fieldWithInputArg({ diff --git a/src/index.ts b/src/index.ts index 68cb56ad8a5..0ccd2d7e2ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -440,8 +440,6 @@ export { typeFromAST, // Create a JavaScript value from a GraphQL language AST without a Type. valueFromASTUntyped, - // Create a GraphQL language AST from a JavaScript value. - astFromValue, // A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system. TypeInfo, visitWithTypeInfo, diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 3a218726f12..c7e6a94f405 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -63,9 +63,7 @@ const QueryType = new GraphQLObjectType({ args: { fromEnum: { type: ComplexEnum, - // Note: defaultValue is provided an *internal* representation for - // Enums, rather than the string name. - defaultValue: Complex1, + defaultValue: 'ONE', }, provideGoodValue: { type: GraphQLBoolean }, provideBadValue: { type: GraphQLBoolean }, diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 6d579fc4b92..1540b0f7209 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -33,10 +33,11 @@ import { GraphQLList, GraphQLNonNull, GraphQLObjectType, + GraphQLScalarType, GraphQLUnionType, } from '../definition.js'; import { assertDirective, GraphQLDirective } from '../directives.js'; -import { GraphQLString } from '../scalars.js'; +import { GraphQLInt, GraphQLString } from '../scalars.js'; import { GraphQLSchema } from '../schema.js'; import { assertValidSchema, validateSchema } from '../validate.js'; @@ -872,7 +873,7 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".', + 'Invalid circular reference. The Input Object SomeInputObject references itself in the non-null field SomeInputObject.nonNullSelf.', locations: [{ line: 7, column: 9 }], }, ]); @@ -900,7 +901,7 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".', + 'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop.', locations: [ { line: 7, column: 9 }, { line: 11, column: 9 }, @@ -934,7 +935,7 @@ describe('Type System: Input Objects must have fields', () => { expectJSON(validateSchema(schema)).toDeepEqual([ { message: - 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".', + 'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.closeLoop.', locations: [ { line: 7, column: 9 }, { line: 11, column: 9 }, @@ -942,7 +943,7 @@ describe('Type System: Input Objects must have fields', () => { }, { message: - 'Cannot reference Input Object "AnotherInputObject" within itself through a series of non-null fields: "startSecondLoop.closeSecondLoop".', + 'Invalid circular reference. The Input Object AnotherInputObject references itself via the non-null fields: AnotherInputObject.startSecondLoop, YetAnotherInputObject.closeSecondLoop.', locations: [ { line: 12, column: 9 }, { line: 16, column: 9 }, @@ -950,12 +951,294 @@ describe('Type System: Input Objects must have fields', () => { }, { message: - 'Cannot reference Input Object "YetAnotherInputObject" within itself through a series of non-null fields: "nonNullSelf".', + 'Invalid circular reference. The Input Object YetAnotherInputObject references itself in the non-null field YetAnotherInputObject.nonNullSelf.', locations: [{ line: 17, column: 9 }], }, ]); }); + it('accepts Input Objects with default values without circular references (SDL)', () => { + const validSchema = buildSchema(` + type Query { + field(arg1: A, arg2: B): String + } + + input A { + x: A = null + y: A = { x: null, y: null } + z: [A] = [] + } + + input B { + x: B2! = {} + y: String = "abc" + z: Custom = {} + } + + input B2 { + x: B3 = {} + } + + input B3 { + x: B = { x: { x: null } } + } + + scalar Custom + `); + + expect(validateSchema(validSchema)).to.deep.equal([]); + }); + + it('accepts Input Objects with default values without circular references (programmatic)', () => { + const AType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'A', + fields: () => ({ + x: { type: AType, defaultValue: null }, + y: { type: AType, defaultValue: { x: null, y: null } }, + z: { type: new GraphQLList(AType), defaultValue: [] }, + }), + }); + + const BType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B', + fields: () => ({ + x: { type: new GraphQLNonNull(B2Type), defaultValue: {} }, + y: { type: GraphQLString, defaultValue: 'abc' }, + z: { type: CustomType, defaultValue: {} }, + }), + }); + + const B2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B2', + fields: () => ({ + x: { type: B3Type, defaultValue: {} }, + }), + }); + + const B3Type: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B3', + fields: () => ({ + x: { type: BType, defaultValue: { x: { x: null } } }, + }), + }); + + const CustomType = new GraphQLScalarType({ name: 'Custom' }); + + const validSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLString, + args: { + arg1: { type: AType }, + arg2: { type: BType }, + }, + }, + }, + }), + }); + + expect(validateSchema(validSchema)).to.deep.equal([]); + }); + + it('rejects Input Objects with default value circular reference (SDL)', () => { + const invalidSchema = buildSchema(` + type Query { + field(arg1: A, arg2: B, arg3: C, arg4: D, arg5: E): String + } + + input A { + x: A = {} + } + + input B { + x: B2 = {} + } + + input B2 { + x: B3 = {} + } + + input B3 { + x: B = {} + } + + input C { + x: [C] = [{}] + } + + input D { + x: D = { x: { x: {} } } + } + + input E { + x: E = { x: null } + y: E = { y: null } + } + + input F { + x: F2! = {} + } + + input F2 { + x: F = { x: {} } + } + `); + + expectJSON(validateSchema(invalidSchema)).toDeepEqual([ + { + message: + 'Invalid circular reference. The default value of Input Object field A.x references itself.', + locations: [{ line: 7, column: 16 }], + }, + { + message: + 'Invalid circular reference. The default value of Input Object field B.x references itself via the default values of: B2.x, B3.x.', + locations: [ + { line: 11, column: 17 }, + { line: 15, column: 17 }, + { line: 19, column: 16 }, + ], + }, + { + message: + 'Invalid circular reference. The default value of Input Object field C.x references itself.', + locations: [{ line: 23, column: 18 }], + }, + { + message: + 'Invalid circular reference. The default value of Input Object field D.x references itself.', + locations: [{ line: 27, column: 16 }], + }, + { + message: + 'Invalid circular reference. The default value of Input Object field E.x references itself via the default values of: E.y.', + locations: [ + { line: 31, column: 16 }, + { line: 32, column: 16 }, + ], + }, + { + message: + 'Invalid circular reference. The default value of Input Object field F2.x references itself.', + locations: [{ line: 40, column: 16 }], + }, + ]); + }); + + it('rejects Input Objects with default value circular reference (programmatic)', () => { + const AType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'A', + fields: () => ({ + x: { type: AType, defaultValue: {} }, + }), + }); + + const BType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B', + fields: () => ({ + x: { type: B2Type, defaultValue: {} }, + }), + }); + + const B2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B2', + fields: () => ({ + x: { type: B3Type, defaultValue: {} }, + }), + }); + + const B3Type: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'B3', + fields: () => ({ + x: { type: BType, defaultValue: {} }, + }), + }); + + const CType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'C', + fields: () => ({ + x: { type: new GraphQLList(CType), defaultValue: [{}] }, + }), + }); + + const DType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'D', + fields: () => ({ + x: { type: DType, defaultValue: { x: { x: {} } } }, + }), + }); + + const EType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'E', + fields: () => ({ + x: { type: EType, defaultValue: { x: null } }, + y: { type: EType, defaultValue: { y: null } }, + }), + }); + + const FType: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'F', + fields: () => ({ + x: { type: new GraphQLNonNull(F2Type), defaultValue: {} }, + }), + }); + + const F2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'F2', + fields: () => ({ + x: { type: FType, defaultValue: { x: {} } }, + }), + }); + + const invalidSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLString, + args: { + arg1: { type: AType }, + arg2: { type: BType }, + arg3: { type: CType }, + arg4: { type: DType }, + arg5: { type: EType }, + arg6: { type: FType }, + }, + }, + }, + }), + }); + + expectJSON(validateSchema(invalidSchema)).toDeepEqual([ + { + message: + 'Invalid circular reference. The default value of Input Object field A.x references itself.', + }, + { + message: + 'Invalid circular reference. The default value of Input Object field B.x references itself via the default values of: B2.x, B3.x.', + }, + { + message: + 'Invalid circular reference. The default value of Input Object field C.x references itself.', + }, + { + message: + 'Invalid circular reference. The default value of Input Object field D.x references itself.', + }, + { + message: + 'Invalid circular reference. The default value of Input Object field E.x references itself via the default values of: E.y.', + }, + { + message: + 'Invalid circular reference. The default value of Input Object field F2.x references itself.', + }, + ]); + }); + it('rejects an Input Object type with incorrectly typed fields', () => { const schema = buildSchema(` type Query { @@ -988,7 +1271,7 @@ describe('Type System: Input Objects must have fields', () => { ]); }); - it('rejects an Input Object type with required argument that is deprecated', () => { + it('rejects an Input Object type with required field that is deprecated', () => { const schema = buildSchema(` type Query { field(arg: SomeInputObject): String @@ -1653,6 +1936,209 @@ describe('Type System: Arguments must have input types', () => { }); }); +describe('Type System: Argument default values must be valid', () => { + it('rejects an argument with invalid default values (SDL)', () => { + const schema = buildSchema(` + type Query { + field(arg: Int = 3.14): Int + } + + directive @bad(arg: Int = 2.718) on FIELD + `); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + '@bad(arg:) has invalid default value: Int cannot represent non-integer value: 2.718', + locations: [{ line: 6, column: 33 }], + }, + { + message: + 'Query.field(arg:) has invalid default value: Int cannot represent non-integer value: 3.14', + locations: [{ line: 3, column: 26 }], + }, + ]); + }); + + it('rejects an argument with invalid default values (programmatic)', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLInt, + args: { + arg: { type: GraphQLInt, defaultValue: 3.14 }, + }, + }, + }, + }), + directives: [ + new GraphQLDirective({ + name: 'bad', + args: { + arg: { type: GraphQLInt, defaultValue: 2.718 }, + }, + locations: [DirectiveLocation.FIELD], + }), + ], + }); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + '@bad(arg:) has invalid default value: Int cannot represent non-integer value: 2.718', + }, + { + message: + 'Query.field(arg:) has invalid default value: Int cannot represent non-integer value: 3.14', + }, + ]); + }); + + it('Attempts to offer a suggested fix if possible (programmatic)', () => { + const Exotic = Symbol('Exotic'); + + const testEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + ONE: { value: 1 }, + TWO: { value: Exotic }, + }, + }); + + const testInput: GraphQLInputObjectType = new GraphQLInputObjectType({ + name: 'TestInput', + fields: () => ({ + self: { type: testInput }, + string: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }, + enum: { type: new GraphQLList(testEnum) }, + }), + }); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLInt, + args: { + argWithPossibleFix: { + type: testInput, + defaultValue: { self: null, string: [1], enum: Exotic }, + }, + argWithInvalidPossibleFix: { + type: testInput, + defaultValue: { string: null }, + }, + argWithoutPossibleFix: { + type: testInput, + defaultValue: { enum: 'Exotic' }, + }, + }, + }, + }, + }), + }); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'Query.field(argWithPossibleFix:) has invalid default value: { self: null, string: [1], enum: Symbol(Exotic) }. Did you mean: { self: null, string: ["1"], enum: ["TWO"] }?', + }, + { + message: + 'Query.field(argWithInvalidPossibleFix:) has invalid default value at .string: Expected value of non-null type [String]! not to be null.', + }, + { + message: + 'Query.field(argWithoutPossibleFix:) has invalid default value: Expected value of type TestInput to include required field "string", found: { enum: "Exotic" }.', + }, + { + message: + 'Query.field(argWithoutPossibleFix:) has invalid default value at .enum: Value "Exotic" does not exist in "TestEnum" enum.', + }, + ]); + }); + + it('Attempts to offer a suggested fix if possible (SDL)', () => { + const originalSchema = buildSchema(` + enum TestEnum { + ONE + TWO + } + + input TestInput { + self: TestInput + string: [String]! + enum: [TestEnum] + } + + type Query { + field( + argWithPossibleFix: TestInput + argWithInvalidPossibleFix: TestInput + argWithoutPossibleFix: TestInput + ): Int + } + `); + + const Exotic = Symbol('Exotic'); + + // workaround as we cannot inject custom internal values into enums defined in SDL + const testEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + ONE: { value: 1 }, + TWO: { value: Exotic }, + }, + }); + + const testInput = assertInputObjectType( + originalSchema.getType('TestInput'), + ); + testInput.getFields().enum.type = new GraphQLList(testEnum); + + // workaround as we cannot inject exotic default values into arguments defined in SDL + const QueryType = assertObjectType(originalSchema.getType('Query')); + for (const arg of QueryType.getFields().field.args) { + arg.type = testInput; + switch (arg.name) { + case 'argWithPossibleFix': + arg.defaultValue = { + value: { self: null, string: [1], enum: Exotic }, + }; + break; + case 'argWithInvalidPossibleFix': + arg.defaultValue = { value: { string: null } }; + break; + case 'argWithoutPossibleFix': + arg.defaultValue = { value: { enum: 'Exotic' } }; + break; + } + } + + expectJSON(validateSchema(originalSchema)).toDeepEqual([ + { + message: + 'Query.field(argWithPossibleFix:) has invalid default value: { self: null, string: [1], enum: Symbol(Exotic) }. Did you mean: { self: null, string: ["1"], enum: ["TWO"] }?', + }, + { + message: + 'Query.field(argWithInvalidPossibleFix:) has invalid default value at .string: Expected value of non-null type [String]! not to be null.', + }, + { + message: + 'Query.field(argWithoutPossibleFix:) has invalid default value: Expected value of type TestInput to include required field "string", found: { enum: "Exotic" }.', + }, + { + message: + 'Query.field(argWithoutPossibleFix:) has invalid default value at .enum: Value "Exotic" does not exist in "TestEnum" enum.', + }, + ]); + }); +}); + describe('Type System: Input Object fields must have input types', () => { function schemaWithInputField( inputFieldConfig: GraphQLInputFieldConfig, @@ -1749,6 +2235,61 @@ describe('Type System: Input Object fields must have input types', () => { }); }); +describe('Type System: Input Object field default values must be valid', () => { + it('rejects an Input Object field with invalid default values (SDL)', () => { + const schema = buildSchema(` + type Query { + field(arg: SomeInputObject): Int + } + + input SomeInputObject { + field: Int = 3.14 + } + `); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'SomeInputObject.field has invalid default value: Int cannot represent non-integer value: 3.14', + locations: [{ line: 7, column: 20 }], + }, + ]); + }); + + it('rejects an Input Object field with invalid default values (programmatic)', () => { + const someInputObject = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + field: { + type: GraphQLInt, + defaultValue: 3.14, + }, + }, + }); + + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + field: { + type: GraphQLInt, + args: { + arg: { type: someInputObject }, + }, + }, + }, + }), + }); + + expectJSON(validateSchema(schema)).toDeepEqual([ + { + message: + 'SomeInputObject.field has invalid default value: Int cannot represent non-integer value: 3.14', + }, + ]); + }); +}); + describe('Objects must adhere to Interface they implement', () => { it('accepts an Object which implements an Interface', () => { const schema = buildSchema(` diff --git a/src/type/introspection.ts b/src/type/introspection.ts index be211cc6a33..422c49755ad 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -4,7 +4,7 @@ import { invariant } from '../jsutils/invariant.js'; import { DirectiveLocation } from '../language/directiveLocation.js'; import { print } from '../language/printer.js'; -import { astFromValue } from '../utilities/astFromValue.js'; +import { valueToLiteral } from '../utilities/valueToLiteral.js'; import type { GraphQLEnumValue, @@ -399,7 +399,7 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ return null; } const literal = - defaultValue.literal ?? astFromValue(defaultValue.value, type); + defaultValue.literal ?? valueToLiteral(defaultValue.value, type); invariant(literal != null, 'Invalid default value'); return print(literal); }, diff --git a/src/type/validate.ts b/src/type/validate.ts index 0a17263defe..dd5df1d5a01 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -1,13 +1,21 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; import { capitalize } from '../jsutils/capitalize.js'; import { andList } from '../jsutils/formatList.js'; +import { hasOwnProperty } from '../jsutils/hasOwnProperty.js'; import { inspect } from '../jsutils/inspect.js'; +import { invariant } from '../jsutils/invariant.js'; +import { isIterableObject } from '../jsutils/isIterableObject.js'; +import { isObjectLike } from '../jsutils/isObjectLike.js'; +import { keyMap } from '../jsutils/keyMap.js'; +import { mapValue } from '../jsutils/mapValue.js'; import type { Maybe } from '../jsutils/Maybe.js'; +import { printPathArray } from '../jsutils/printPathArray.js'; import { GraphQLError } from '../error/GraphQLError.js'; import type { ASTNode, + ConstValueNode, DirectiveNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, @@ -18,22 +26,32 @@ import type { UnionTypeExtensionNode, } from '../language/ast.js'; import { OperationTypeNode } from '../language/ast.js'; +import { Kind } from '../language/kinds.js'; import { isEqualType, isTypeSubTypeOf } from '../utilities/typeComparators.js'; +import { + validateInputLiteral, + validateInputValue, +} from '../utilities/validateInputValue.js'; import type { + GraphQLArgument, GraphQLEnumType, GraphQLInputField, GraphQLInputObjectType, + GraphQLInputType, GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, } from './definition.js'; import { + assertLeafType, + getNamedType, isEnumType, isInputObjectType, isInputType, isInterfaceType, + isListType, isNamedType, isNonNullType, isObjectType, @@ -205,8 +223,126 @@ function validateDirectives(context: SchemaValidationContext): void { arg.astNode?.type, ]); } + + validateDefaultValue(context, arg); + } + } +} + +function validateDefaultValue( + context: SchemaValidationContext, + inputValue: GraphQLArgument | GraphQLInputField, +): void { + const defaultValue = inputValue.defaultValue; + + if (!defaultValue) { + return; + } + + if (defaultValue.literal) { + validateInputLiteral( + defaultValue.literal, + inputValue.type, + undefined, + (error, path) => { + context.reportError( + `${inputValue} has invalid default value${printPathArray(path)}: ${ + error.message + }`, + error.nodes, + ); + }, + ); + } else { + const errors: Array<[GraphQLError, ReadonlyArray]> = []; + validateInputValue(defaultValue.value, inputValue.type, (error, path) => { + errors.push([error, path]); + }); + + // If there were validation errors, check to see if it can be "uncoerced" + // and then correctly validated. If so, report a clear error with a path + // to resolution. + if (errors.length > 0) { + try { + const uncoercedValue = uncoerceDefaultValue( + defaultValue.value, + inputValue.type, + ); + + const uncoercedErrors = []; + validateInputValue(uncoercedValue, inputValue.type, (error, path) => { + uncoercedErrors.push([error, path]); + }); + + if (uncoercedErrors.length === 0) { + context.reportError( + `${inputValue} has invalid default value: ${inspect( + defaultValue.value, + )}. Did you mean: ${inspect(uncoercedValue)}?`, + inputValue.astNode?.defaultValue, + ); + return; + } + } catch (_error) { + // ignore + } + } + + // Otherwise report the original set of errors. + for (const [error, path] of errors) { + context.reportError( + `${inputValue} has invalid default value${printPathArray(path)}: ${ + error.message + }`, + inputValue.astNode?.defaultValue, + ); + } + } +} + +/** + * Historically GraphQL.js allowed default values to be provided as + * assumed-coerced "internal" values, however default values should be provided + * as "external" pre-coerced values. `uncoerceDefaultValue()` will convert such + * "internal" values to "external" values to display as part of validation. + * + * This performs the "opposite" of `coerceInputValue()`. Given an "internal" + * coerced value, reverse the process to provide an "external" uncoerced value. + */ +function uncoerceDefaultValue(value: unknown, type: GraphQLInputType): unknown { + if (isNonNullType(type)) { + return uncoerceDefaultValue(value, type.ofType); + } + + if (value === null) { + return null; + } + + if (isListType(type)) { + if (isIterableObject(value)) { + return Array.from(value, (itemValue) => + uncoerceDefaultValue(itemValue, type.ofType), + ); } + return [uncoerceDefaultValue(value, type.ofType)]; + } + + if (isInputObjectType(type)) { + invariant(isObjectLike(value)); + const fieldDefs = type.getFields(); + return mapValue(value, (fieldValue, fieldName) => { + invariant(fieldName in fieldDefs); + return uncoerceDefaultValue(fieldValue, fieldDefs[fieldName].type); + }); } + + assertLeafType(type); + + // For most leaf types (Scalars, Enums), result coercion ("serialize") is + // the inverse of input coercion ("parseValue") and will produce an + // "external" value. Historically, this method was also used as part of the + // now-removed "astFromValue" to perform the same behavior. + return type.serialize(value); } function validateName( @@ -223,8 +359,11 @@ function validateName( } function validateTypes(context: SchemaValidationContext): void { - const validateInputObjectCircularRefs = - createInputObjectCircularRefsValidator(context); + // Ensure Input Objects do not contain non-nullable circular references. + const validateInputObjectNonNullCircularRefs = + createInputObjectNonNullCircularRefsValidator(context); + const validateInputObjectDefaultValueCircularRefs = + createInputObjectDefaultValueCircularRefsValidator(context); const typeMap = context.schema.getTypeMap(); for (const type of Object.values(typeMap)) { // Ensure all provided types are in fact GraphQL type. @@ -263,8 +402,12 @@ function validateTypes(context: SchemaValidationContext): void { // Ensure Input Object fields are valid. validateInputFields(context, type); - // Ensure Input Objects do not contain non-nullable circular references - validateInputObjectCircularRefs(type); + // Ensure Input Objects do not contain invalid field circular references. + // Ensure Input Objects do not contain non-nullable circular references. + validateInputObjectNonNullCircularRefs(type); + + // Ensure Input Objects do not contain invalid default value circular references. + validateInputObjectDefaultValueCircularRefs(type); } } } @@ -316,6 +459,8 @@ function validateFields( arg.astNode?.type, ]); } + + validateDefaultValue(context, arg); } } } @@ -411,8 +556,6 @@ function validateTypeImplementsInterface( [ifaceArg.astNode?.type, typeArg.astNode?.type], ); } - - // TODO: validate default values? } // Assert additional arguments must not be required. @@ -519,7 +662,7 @@ function validateInputFields( ); } - // Ensure the arguments are valid + // Ensure the input fields are valid for (const field of fields) { // Ensure they are named correctly. validateName(context, field); @@ -539,10 +682,12 @@ function validateInputFields( [getDeprecatedDirectiveNode(field.astNode), field.astNode?.type], ); } + + validateDefaultValue(context, field); } } -function createInputObjectCircularRefsValidator( +function createInputObjectNonNullCircularRefsValidator( context: SchemaValidationContext, ): (inputObj: GraphQLInputObjectType) => void { // Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'. @@ -580,9 +725,13 @@ function createInputObjectCircularRefsValidator( detectCycleRecursive(fieldType); } else { const cyclePath = fieldPath.slice(cycleIndex); - const pathStr = cyclePath.map((fieldObj) => fieldObj.name).join('.'); + const pathStr = cyclePath.join(', '); context.reportError( - `Cannot reference Input Object "${fieldType}" within itself through a series of non-null fields: "${pathStr}".`, + `Invalid circular reference. The Input Object ${fieldType} references itself ${ + cyclePath.length > 1 + ? 'via the non-null fields:' + : 'in the non-null field' + } ${pathStr}.`, cyclePath.map((fieldObj) => fieldObj.astNode), ); } @@ -594,6 +743,154 @@ function createInputObjectCircularRefsValidator( } } +function createInputObjectDefaultValueCircularRefsValidator( + context: SchemaValidationContext, +): (inputObj: GraphQLInputObjectType) => void { + // Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'. + // Tracks already visited types to maintain O(N) and to ensure that cycles + // are not redundantly reported. + const visitedFields = Object.create(null); + + // Array of coordinates and default values used to produce meaningful errors. + const fieldPath: Array< + [coordinate: string, defaultValue: ConstValueNode | undefined] + > = []; + + // Position in the path + const fieldPathIndex = Object.create(null); + + // This does a straight-forward DFS to find cycles. + // It does not terminate when a cycle was found but continues to explore + // the graph to find all possible cycles. + return function validateInputObjectDefaultValueCircularRefs( + inputObj: GraphQLInputObjectType, + ): void { + // Start with an empty object as a way to visit every field in this input + // object type and apply every default value. + return detectValueDefaultValueCycle(inputObj, {}); + }; + + function detectValueDefaultValueCycle( + inputObj: GraphQLInputObjectType, + defaultValue: unknown, + ): void { + // If the value is a List, recursively check each entry for a cycle. + // Otherwise, only object values can contain a cycle. + if (isIterableObject(defaultValue)) { + for (const itemValue of defaultValue) { + detectValueDefaultValueCycle(inputObj, itemValue); + } + return; + } else if (!isObjectLike(defaultValue)) { + return; + } + + // Check each defined field for a cycle. + for (const field of Object.values(inputObj.getFields())) { + const namedFieldType = getNamedType(field.type); + + // Only input object type fields can result in a cycle. + if (!isInputObjectType(namedFieldType)) { + continue; + } + + if (hasOwnProperty(defaultValue, field.name)) { + // If the provided value has this field defined, recursively check it + // for cycles. + detectValueDefaultValueCycle(namedFieldType, defaultValue[field.name]); + } else { + // Otherwise check this field's default value for cycles. + detectFieldDefaultValueCycle(field, namedFieldType); + } + } + } + + function detectLiteralDefaultValueCycle( + inputObj: GraphQLInputObjectType, + defaultValue: ConstValueNode, + ): void { + // If the value is a List, recursively check each entry for a cycle. + // Otherwise, only object values can contain a cycle. + if (defaultValue.kind === Kind.LIST) { + for (const itemLiteral of defaultValue.values) { + detectLiteralDefaultValueCycle(inputObj, itemLiteral); + } + return; + } else if (defaultValue.kind !== Kind.OBJECT) { + return; + } + + // Check each defined field for a cycle. + const fieldNodes = keyMap(defaultValue.fields, (field) => field.name.value); + for (const field of Object.values(inputObj.getFields())) { + const namedFieldType = getNamedType(field.type); + + // Only input object type fields can result in a cycle. + if (!isInputObjectType(namedFieldType)) { + continue; + } + + if (hasOwnProperty(fieldNodes, field.name)) { + // If the provided value has this field defined, recursively check it + // for cycles. + detectLiteralDefaultValueCycle( + namedFieldType, + fieldNodes[field.name].value, + ); + } else { + // Otherwise check this field's default value for cycles. + detectFieldDefaultValueCycle(field, namedFieldType); + } + } + } + + function detectFieldDefaultValueCycle( + field: GraphQLInputField, + fieldType: GraphQLInputObjectType, + ): void { + // Only a field with a default value can result in a cycle. + const defaultValue = field.defaultValue; + if (defaultValue === undefined) { + return; + } + + const fieldCoordinate = String(field); + + // Check to see if there is cycle. + const cycleIndex = fieldPathIndex[fieldCoordinate]; + if (cycleIndex > 0) { + context.reportError( + `Invalid circular reference. The default value of Input Object field ${field} references itself${ + cycleIndex < fieldPath.length + ? ` via the default values of: ${fieldPath + .slice(cycleIndex) + .map(([coordinate]) => coordinate) + .join(', ')}` + : '' + }.`, + fieldPath.slice(cycleIndex - 1).map(([, node]) => node), + ); + return; + } + + // Recurse into this field's default value once, tracking the path. + if (!visitedFields[fieldCoordinate]) { + visitedFields[fieldCoordinate] = true; + fieldPathIndex[fieldCoordinate] = fieldPath.push([ + fieldCoordinate, + field.astNode?.defaultValue, + ]); + if (defaultValue.literal) { + detectLiteralDefaultValueCycle(fieldType, defaultValue.literal); + } else { + detectValueDefaultValueCycle(fieldType, defaultValue.value); + } + fieldPath.pop(); + fieldPathIndex[fieldCoordinate] = undefined; + } + } +} + function getAllImplementsInterfaceNodes( type: GraphQLObjectType | GraphQLInterfaceType, iface: GraphQLInterfaceType, diff --git a/src/utilities/__tests__/astFromValue-test.ts b/src/utilities/__tests__/astFromValue-test.ts deleted file mode 100644 index 0f9d4742561..00000000000 --- a/src/utilities/__tests__/astFromValue-test.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLList, - GraphQLNonNull, - GraphQLScalarType, -} from '../../type/definition.js'; -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInt, - GraphQLString, -} from '../../type/scalars.js'; - -import { astFromValue } from '../astFromValue.js'; - -describe('astFromValue', () => { - it('converts boolean values to ASTs', () => { - expect(astFromValue(true, GraphQLBoolean)).to.deep.equal({ - kind: 'BooleanValue', - value: true, - }); - - expect(astFromValue(false, GraphQLBoolean)).to.deep.equal({ - kind: 'BooleanValue', - value: false, - }); - - expect(astFromValue(undefined, GraphQLBoolean)).to.deep.equal(null); - - expect(astFromValue(null, GraphQLBoolean)).to.deep.equal({ - kind: 'NullValue', - }); - - expect(astFromValue(0, GraphQLBoolean)).to.deep.equal({ - kind: 'BooleanValue', - value: false, - }); - - expect(astFromValue(1, GraphQLBoolean)).to.deep.equal({ - kind: 'BooleanValue', - value: true, - }); - - const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); - expect(astFromValue(0, NonNullBoolean)).to.deep.equal({ - kind: 'BooleanValue', - value: false, - }); - }); - - it('converts Int values to Int ASTs', () => { - expect(astFromValue(-1, GraphQLInt)).to.deep.equal({ - kind: 'IntValue', - value: '-1', - }); - - expect(astFromValue(123.0, GraphQLInt)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(astFromValue(1e4, GraphQLInt)).to.deep.equal({ - kind: 'IntValue', - value: '10000', - }); - - // GraphQL spec does not allow coercing non-integer values to Int to avoid - // accidental data loss. - expect(() => astFromValue(123.5, GraphQLInt)).to.throw( - 'Int cannot represent non-integer value: 123.5', - ); - - // Note: outside the bounds of 32bit signed int. - expect(() => astFromValue(1e40, GraphQLInt)).to.throw( - 'Int cannot represent non 32-bit signed integer value: 1e+40', - ); - - expect(() => astFromValue(NaN, GraphQLInt)).to.throw( - 'Int cannot represent non-integer value: NaN', - ); - }); - - it('converts Float values to Int/Float ASTs', () => { - expect(astFromValue(-1, GraphQLFloat)).to.deep.equal({ - kind: 'IntValue', - value: '-1', - }); - - expect(astFromValue(123.0, GraphQLFloat)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(astFromValue(123.5, GraphQLFloat)).to.deep.equal({ - kind: 'FloatValue', - value: '123.5', - }); - - expect(astFromValue(1e4, GraphQLFloat)).to.deep.equal({ - kind: 'IntValue', - value: '10000', - }); - - expect(astFromValue(1e40, GraphQLFloat)).to.deep.equal({ - kind: 'FloatValue', - value: '1e+40', - }); - }); - - it('converts String values to String ASTs', () => { - expect(astFromValue('hello', GraphQLString)).to.deep.equal({ - kind: 'StringValue', - value: 'hello', - }); - - expect(astFromValue('VALUE', GraphQLString)).to.deep.equal({ - kind: 'StringValue', - value: 'VALUE', - }); - - expect(astFromValue('VA\nLUE', GraphQLString)).to.deep.equal({ - kind: 'StringValue', - value: 'VA\nLUE', - }); - - expect(astFromValue(123, GraphQLString)).to.deep.equal({ - kind: 'StringValue', - value: '123', - }); - - expect(astFromValue(false, GraphQLString)).to.deep.equal({ - kind: 'StringValue', - value: 'false', - }); - - expect(astFromValue(null, GraphQLString)).to.deep.equal({ - kind: 'NullValue', - }); - - expect(astFromValue(undefined, GraphQLString)).to.deep.equal(null); - }); - - it('converts ID values to Int/String ASTs', () => { - expect(astFromValue('hello', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: 'hello', - }); - - expect(astFromValue('VALUE', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: 'VALUE', - }); - - // Note: EnumValues cannot contain non-identifier characters - expect(astFromValue('VA\nLUE', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: 'VA\nLUE', - }); - - // Note: IntValues are used when possible. - expect(astFromValue(-1, GraphQLID)).to.deep.equal({ - kind: 'IntValue', - value: '-1', - }); - - expect(astFromValue(123, GraphQLID)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(astFromValue('123', GraphQLID)).to.deep.equal({ - kind: 'IntValue', - value: '123', - }); - - expect(astFromValue('01', GraphQLID)).to.deep.equal({ - kind: 'StringValue', - value: '01', - }); - - expect(() => astFromValue(false, GraphQLID)).to.throw( - 'ID cannot represent value: false', - ); - - expect(astFromValue(null, GraphQLID)).to.deep.equal({ kind: 'NullValue' }); - - expect(astFromValue(undefined, GraphQLID)).to.deep.equal(null); - }); - - it('converts using serialize from a custom scalar type', () => { - const passthroughScalar = new GraphQLScalarType({ - name: 'PassthroughScalar', - serialize(value) { - return value; - }, - }); - - expect(astFromValue('value', passthroughScalar)).to.deep.equal({ - kind: 'StringValue', - value: 'value', - }); - - expect(() => astFromValue(NaN, passthroughScalar)).to.throw( - 'Cannot convert value to AST: NaN.', - ); - expect(() => astFromValue(Infinity, passthroughScalar)).to.throw( - 'Cannot convert value to AST: Infinity.', - ); - - const returnNullScalar = new GraphQLScalarType({ - name: 'ReturnNullScalar', - serialize() { - return null; - }, - }); - - expect(astFromValue('value', returnNullScalar)).to.equal(null); - - class SomeClass {} - - const returnCustomClassScalar = new GraphQLScalarType({ - name: 'ReturnCustomClassScalar', - serialize() { - return new SomeClass(); - }, - }); - - expect(() => astFromValue('value', returnCustomClassScalar)).to.throw( - 'Cannot convert value to AST: {}.', - ); - }); - - it('does not converts NonNull values to NullValue', () => { - const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); - expect(astFromValue(null, NonNullBoolean)).to.deep.equal(null); - }); - - const complexValue = { someArbitrary: 'complexValue' }; - - const myEnum = new GraphQLEnumType({ - name: 'MyEnum', - values: { - HELLO: {}, - GOODBYE: {}, - COMPLEX: { value: complexValue }, - }, - }); - - it('converts string values to Enum ASTs if possible', () => { - expect(astFromValue('HELLO', myEnum)).to.deep.equal({ - kind: 'EnumValue', - value: 'HELLO', - }); - - expect(astFromValue(complexValue, myEnum)).to.deep.equal({ - kind: 'EnumValue', - value: 'COMPLEX', - }); - - // Note: case sensitive - expect(() => astFromValue('hello', myEnum)).to.throw( - 'Enum "MyEnum" cannot represent value: "hello"', - ); - - // Note: Not a valid enum value - expect(() => astFromValue('UNKNOWN_VALUE', myEnum)).to.throw( - 'Enum "MyEnum" cannot represent value: "UNKNOWN_VALUE"', - ); - }); - - it('converts array values to List ASTs', () => { - expect( - astFromValue(['FOO', 'BAR'], new GraphQLList(GraphQLString)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'StringValue', value: 'FOO' }, - { kind: 'StringValue', value: 'BAR' }, - ], - }); - - expect( - astFromValue(['HELLO', 'GOODBYE'], new GraphQLList(myEnum)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'EnumValue', value: 'HELLO' }, - { kind: 'EnumValue', value: 'GOODBYE' }, - ], - }); - - function* listGenerator() { - yield 1; - yield 2; - yield 3; - } - - expect( - astFromValue(listGenerator(), new GraphQLList(GraphQLInt)), - ).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'IntValue', value: '1' }, - { kind: 'IntValue', value: '2' }, - { kind: 'IntValue', value: '3' }, - ], - }); - }); - - it('converts list singletons', () => { - expect(astFromValue('FOO', new GraphQLList(GraphQLString))).to.deep.equal({ - kind: 'StringValue', - value: 'FOO', - }); - }); - - it('skip invalid list items', () => { - const ast = astFromValue( - ['FOO', null, 'BAR'], - new GraphQLList(new GraphQLNonNull(GraphQLString)), - ); - - expect(ast).to.deep.equal({ - kind: 'ListValue', - values: [ - { kind: 'StringValue', value: 'FOO' }, - { kind: 'StringValue', value: 'BAR' }, - ], - }); - }); - - const inputObj = new GraphQLInputObjectType({ - name: 'MyInputObj', - fields: { - foo: { type: GraphQLFloat }, - bar: { type: myEnum }, - }, - }); - - it('converts input objects', () => { - expect(astFromValue({ foo: 3, bar: 'HELLO' }, inputObj)).to.deep.equal({ - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'foo' }, - value: { kind: 'IntValue', value: '3' }, - }, - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'bar' }, - value: { kind: 'EnumValue', value: 'HELLO' }, - }, - ], - }); - }); - - it('converts input objects with explicit nulls', () => { - expect(astFromValue({ foo: null }, inputObj)).to.deep.equal({ - kind: 'ObjectValue', - fields: [ - { - kind: 'ObjectField', - name: { kind: 'Name', value: 'foo' }, - value: { kind: 'NullValue' }, - }, - ], - }); - }); - - it('does not converts non-object values as input objects', () => { - expect(astFromValue(5, inputObj)).to.equal(null); - }); -}); diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts deleted file mode 100644 index bb03baf232e..00000000000 --- a/src/utilities/astFromValue.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { inspect } from '../jsutils/inspect.js'; -import { invariant } from '../jsutils/invariant.js'; -import { isIterableObject } from '../jsutils/isIterableObject.js'; -import { isObjectLike } from '../jsutils/isObjectLike.js'; -import type { Maybe } from '../jsutils/Maybe.js'; - -import type { ConstObjectFieldNode, ConstValueNode } from '../language/ast.js'; -import { Kind } from '../language/kinds.js'; - -import type { GraphQLInputType } from '../type/definition.js'; -import { - isEnumType, - isInputObjectType, - isLeafType, - isListType, - isNonNullType, -} from '../type/definition.js'; -import { GraphQLID } from '../type/scalars.js'; - -/** - * Produces a GraphQL Value AST given a JavaScript object. - * Function will match JavaScript/JSON values to GraphQL AST schema format - * by using suggested GraphQLInputType. For example: - * - * astFromValue("value", GraphQLString) - * - * A GraphQL type must be provided, which will be used to interpret different - * JavaScript values. - * - * | JSON Value | GraphQL Value | - * | ------------- | -------------------- | - * | Object | Input Object | - * | Array | List | - * | Boolean | Boolean | - * | String | String / Enum Value | - * | Number | Int / Float | - * | Unknown | Enum Value | - * | null | NullValue | - * - */ -export function astFromValue( - value: unknown, - type: GraphQLInputType, -): Maybe { - if (isNonNullType(type)) { - const astValue = astFromValue(value, type.ofType); - if (astValue?.kind === Kind.NULL) { - return null; - } - return astValue; - } - - // only explicit null, not undefined, NaN - if (value === null) { - return { kind: Kind.NULL }; - } - - // undefined - if (value === undefined) { - return null; - } - - // Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but - // the value is not an array, convert the value using the list's item type. - if (isListType(type)) { - const itemType = type.ofType; - if (isIterableObject(value)) { - const valuesNodes = []; - for (const item of value) { - const itemNode = astFromValue(item, itemType); - if (itemNode != null) { - valuesNodes.push(itemNode); - } - } - return { kind: Kind.LIST, values: valuesNodes }; - } - return astFromValue(value, itemType); - } - - // Populate the fields of the input object by creating ASTs from each value - // in the JavaScript object according to the fields in the input type. - if (isInputObjectType(type)) { - if (!isObjectLike(value)) { - return null; - } - const fieldNodes: Array = []; - for (const field of Object.values(type.getFields())) { - const fieldValue = astFromValue(value[field.name], field.type); - if (fieldValue) { - fieldNodes.push({ - kind: Kind.OBJECT_FIELD, - name: { kind: Kind.NAME, value: field.name }, - value: fieldValue, - }); - } - } - return { kind: Kind.OBJECT, fields: fieldNodes }; - } - - if (isLeafType(type)) { - // Since value is an internally represented value, it must be serialized - // to an externally represented value before converting into an AST. - const serialized = type.serialize(value); - if (serialized == null) { - return null; - } - - // Others serialize based on their corresponding JavaScript scalar types. - if (typeof serialized === 'boolean') { - return { kind: Kind.BOOLEAN, value: serialized }; - } - - // JavaScript numbers can be Int or Float values. - if (typeof serialized === 'number' && Number.isFinite(serialized)) { - const stringNum = String(serialized); - return integerStringRegExp.test(stringNum) - ? { kind: Kind.INT, value: stringNum } - : { kind: Kind.FLOAT, value: stringNum }; - } - - if (typeof serialized === 'string') { - // Enum types use Enum literals. - if (isEnumType(type)) { - return { kind: Kind.ENUM, value: serialized }; - } - - // ID types can use Int literals. - if (type === GraphQLID && integerStringRegExp.test(serialized)) { - return { kind: Kind.INT, value: serialized }; - } - - return { - kind: Kind.STRING, - value: serialized, - }; - } - - throw new TypeError(`Cannot convert value to AST: ${inspect(serialized)}.`); - } - /* c8 ignore next 3 */ - // Not reachable, all possible types have been considered. - invariant(false, 'Unexpected input type: ' + inspect(type)); -} - -/** - * IntValue: - * - NegativeSign? 0 - * - NegativeSign? NonZeroDigit ( Digit+ )? - */ -const integerStringRegExp = /^-?(?:0|[1-9][0-9]*)$/; diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 185dfc3179b..c5e0466b95f 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -254,7 +254,7 @@ export function coerceDefaultValue( if (coercedValue === undefined) { coercedValue = defaultValue.literal ? coerceInputLiteral(defaultValue.literal, type) - : defaultValue.value; + : coerceInputValue(defaultValue.value, type); invariant(coercedValue !== undefined); (defaultValue as any)._memoizedCoercedValue = coercedValue; } diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index c636cd85b42..08ce42483ae 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -32,8 +32,8 @@ import { import { isSpecifiedScalarType } from '../type/scalars.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { astFromValue } from './astFromValue.js'; import { sortValueNode } from './sortValueNode.js'; +import { valueToLiteral } from './valueToLiteral.js'; export enum BreakingChangeType { TYPE_REMOVED = 'TYPE_REMOVED', @@ -530,7 +530,7 @@ function stringifyValue( defaultValue: GraphQLDefaultValueUsage, type: GraphQLInputType, ): string { - const ast = defaultValue.literal ?? astFromValue(defaultValue.value, type); + const ast = defaultValue.literal ?? valueToLiteral(defaultValue.value, type); invariant(ast != null); return print(sortValueNode(ast)); } diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 1a1ef78b461..1690ce0fd1f 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -58,9 +58,6 @@ export { typeFromAST } from './typeFromAST.js'; // Create a JavaScript value from a GraphQL language AST without a type. export { valueFromASTUntyped } from './valueFromASTUntyped.js'; -// Create a GraphQL language AST from a JavaScript value. -export { astFromValue } from './astFromValue.js'; - // A helper to use within recursive-descent visitors which need to be aware of the GraphQL type system. export { TypeInfo, visitWithTypeInfo } from './TypeInfo.js'; diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 88ee7f0b4b3..0b0a9c4addc 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -34,7 +34,7 @@ import { isIntrospectionType } from '../type/introspection.js'; import { isSpecifiedScalarType } from '../type/scalars.js'; import type { GraphQLSchema } from '../type/schema.js'; -import { astFromValue } from './astFromValue.js'; +import { valueToLiteral } from './valueToLiteral.js'; export function printSchema(schema: GraphQLSchema): string { return printFilteredSchema( @@ -265,7 +265,7 @@ function printInputValue(arg: GraphQLInputField): string { if (arg.defaultValue) { const literal = arg.defaultValue.literal ?? - astFromValue(arg.defaultValue.value, arg.type); + valueToLiteral(arg.defaultValue.value, arg.type); invariant(literal != null, 'Invalid default value'); argDecl += ` = ${print(literal)}`; }