From 4b489abcccccac3cb7cc971ad4e7b1cb0e3b667d Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 18 Nov 2024 06:02:11 +0200 Subject: [PATCH] ease upgrade path for programmatic default values --- integrationTests/ts/basic-test.ts | 2 +- integrationTests/ts/esm.ts | 2 +- src/execution/__tests__/executor-test.ts | 2 +- src/execution/__tests__/variables-test.ts | 6 +- src/execution/getVariableSignature.ts | 16 ++-- src/execution/values.ts | 16 ++-- src/type/__tests__/definition-test.ts | 22 +++-- src/type/__tests__/directive-test.ts | 2 + src/type/__tests__/enumType-test.ts | 28 +++++- src/type/__tests__/predicate-test.ts | 26 +++--- src/type/__tests__/validation-test.ts | 54 ++++++------ src/type/definition.ts | 49 +++++++---- src/type/directives.ts | 8 +- src/type/introspection.ts | 30 ++++--- src/type/validate.ts | 9 +- src/utilities/TypeInfo.ts | 13 +-- .../__tests__/coerceInputValue-test.ts | 18 ++-- src/utilities/__tests__/printSchema-test.ts | 16 ++-- .../__tests__/validateInputValue-test.ts | 13 +-- src/utilities/__tests__/valueFromAST-test.ts | 2 +- src/utilities/coerceInputValue.ts | 54 +++++++----- src/utilities/findSchemaChanges.ts | 85 ++++++++----------- src/utilities/getDefaultValueAST.ts | 30 +++++++ src/utilities/printSchema.ts | 19 ++--- src/utilities/replaceVariables.ts | 7 +- src/utilities/valueFromAST.ts | 4 +- src/validation/ValidationContext.ts | 3 +- .../rules/VariablesInAllowedPositionRule.ts | 7 +- 28 files changed, 321 insertions(+), 222 deletions(-) create mode 100644 src/utilities/getDefaultValueAST.ts diff --git a/integrationTests/ts/basic-test.ts b/integrationTests/ts/basic-test.ts index a28bd840e7..9f4c302262 100644 --- a/integrationTests/ts/basic-test.ts +++ b/integrationTests/ts/basic-test.ts @@ -11,7 +11,7 @@ const queryType: GraphQLObjectType = new GraphQLObjectType({ args: { who: { type: GraphQLString, - defaultValue: 'World', + externalDefaultValue: 'World', }, }, resolve(_root, args: { who: string }) { diff --git a/integrationTests/ts/esm.ts b/integrationTests/ts/esm.ts index 4554d1efec..89794fd9e6 100644 --- a/integrationTests/ts/esm.ts +++ b/integrationTests/ts/esm.ts @@ -15,7 +15,7 @@ const queryType: GraphQLObjectType = new GraphQLObjectType({ args: { who: { type: GraphQLString, - defaultValue: 'World', + externalDefaultValue: 'World', }, }, resolve(_root, args: { who: string }) { diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 173dcc9483..b428df79a6 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -246,7 +246,7 @@ describe('Execute: Handles basic execution tasks', () => { signature: { name: 'var', type: GraphQLString, - defaultValue: undefined, + externalDefaultValue: undefined, }, value: 'abc', }, diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index ca729d0248..f3e04066fc 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -142,11 +142,11 @@ const TestType = new GraphQLObjectType({ }), fieldWithDefaultArgumentValue: fieldWithInputArg({ type: GraphQLString, - defaultValue: 'Hello World', + externalDefaultValue: 'Hello World', }), fieldWithNonNullableStringInputAndDefaultArgumentValue: fieldWithInputArg({ type: new GraphQLNonNull(GraphQLString), - defaultValue: 'Hello World', + externalDefaultValue: 'Hello World', }), fieldWithNestedInputObject: fieldWithInputArg({ type: TestNestedInputObject, @@ -187,7 +187,7 @@ const schema = new GraphQLSchema({ type: new GraphQLNonNull(GraphQLBoolean), description: 'Skipped when true.', // default values will override operation variables in the setting of defined fragment variables that are not provided - defaultValue: true, + externalDefaultValue: true, }, }, }), diff --git a/src/execution/getVariableSignature.ts b/src/execution/getVariableSignature.ts index 8f9fd4a5d2..7a86582e5b 100644 --- a/src/execution/getVariableSignature.ts +++ b/src/execution/getVariableSignature.ts @@ -1,14 +1,13 @@ import { GraphQLError } from '../error/GraphQLError.js'; -import type { VariableDefinitionNode } from '../language/ast.js'; +import type { + ConstValueNode, + VariableDefinitionNode, +} from '../language/ast.js'; import { print } from '../language/printer.js'; import { isInputType } from '../type/definition.js'; -import type { - GraphQLDefaultValueUsage, - GraphQLInputType, - GraphQLSchema, -} from '../type/index.js'; +import type { GraphQLInputType, GraphQLSchema } from '../type/index.js'; import { typeFromAST } from '../utilities/typeFromAST.js'; @@ -21,7 +20,8 @@ import { typeFromAST } from '../utilities/typeFromAST.js'; export interface GraphQLVariableSignature { name: string; type: GraphQLInputType; - defaultValue: GraphQLDefaultValueUsage | undefined; + defaultValue?: never; + externalDefaultValue: { literal: ConstValueNode } | undefined; } export function getVariableSignature( @@ -46,6 +46,6 @@ export function getVariableSignature( return { name: varName, type: varType, - defaultValue: defaultValue ? { literal: defaultValue } : undefined, + externalDefaultValue: defaultValue ? { literal: defaultValue } : undefined, }; } diff --git a/src/execution/values.ts b/src/execution/values.ts index ed869e10bd..4cb98a9e64 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -226,11 +226,9 @@ export function experimentalGetArgumentValues( { nodes: node }, ); } - if (argDef.defaultValue) { - coercedValues[name] = coerceDefaultValue( - argDef.defaultValue, - argDef.type, - ); + const coercedDefaultValue = coerceDefaultValue(argDef); + if (coercedDefaultValue !== undefined) { + coercedValues[name] = coercedDefaultValue; } continue; } @@ -249,11 +247,9 @@ export function experimentalGetArgumentValues( !Object.hasOwn(scopedVariableValues.coerced, variableName)) && !isRequiredArgument(argDef) ) { - if (argDef.defaultValue) { - coercedValues[name] = coerceDefaultValue( - argDef.defaultValue, - argDef.type, - ); + const coercedDefaultValue = coerceDefaultValue(argDef); + if (coercedDefaultValue !== undefined) { + coercedValues[name] = coercedDefaultValue; } continue; } diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index ad35af937f..c5e73945cc 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -195,7 +195,8 @@ describe('Type System: Objects', () => { input: { description: 'Argument description.', type: ScalarType, - defaultValue: 'DefaultValue', + defaultValue: undefined, + externalDefaultValue: 'DefaultValue', defaultValueLiteral: undefined, deprecationReason: 'Argument deprecation reason.', extensions: { someExtension: 'extension' }, @@ -343,6 +344,7 @@ describe('Type System: Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + externalDefaultValue: undefined, deprecationReason: undefined, extensions: {}, astNode: undefined, @@ -467,6 +469,7 @@ describe('Type System: Interfaces', () => { description: 'Argument description.', type: ScalarType, defaultValue: undefined, + externalDefaultValue: undefined, defaultValueLiteral: dummyAny, deprecationReason: 'Argument deprecation reason.', extensions: { someExtension: 'extension' }, @@ -802,7 +805,8 @@ describe('Type System: Input Objects', () => { input: { description: 'Argument description.', type: ScalarType, - defaultValue: 'DefaultValue', + defaultValue: undefined, + externalDefaultValue: 'DefaultValue', defaultValueLiteral: undefined, deprecationReason: 'Argument deprecation reason.', extensions: { someExtension: 'extension' }, @@ -832,6 +836,7 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + externalDefaultValue: undefined, deprecationReason: undefined, extensions: {}, astNode: undefined, @@ -852,6 +857,7 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + externalDefaultValue: undefined, extensions: {}, deprecationReason: undefined, astNode: undefined, @@ -901,14 +907,15 @@ describe('Type System: Input Objects', () => { const inputObjType = new GraphQLInputObjectType({ name: 'SomeInputObject', fields: { - f: { type: ScalarType, defaultValue: 3 }, + f: { type: ScalarType, externalDefaultValue: 3 }, }, }); expect(inputObjType.getFields().f).to.deep.include({ name: 'f', description: undefined, type: ScalarType, - defaultValue: { value: 3 }, + defaultValue: undefined, + externalDefaultValue: { value: 3 }, deprecationReason: undefined, extensions: {}, astNode: undefined, @@ -929,7 +936,8 @@ describe('Type System: Input Objects', () => { name: 'f', description: undefined, type: ScalarType, - defaultValue: { literal: { kind: 'IntValue', value: '3' } }, + defaultValue: undefined, + externalDefaultValue: { literal: { kind: 'IntValue', value: '3' } }, deprecationReason: undefined, extensions: {}, astNode: undefined, @@ -942,13 +950,13 @@ describe('Type System: Input Objects', () => { fields: { f: { type: ScalarType, - defaultValue: 3, + externalDefaultValue: 3, defaultValueLiteral: { kind: Kind.INT, value: '3' }, }, }, }); expect(() => inputObjType.getFields()).to.throw( - 'Argument "f" has both a defaultValue and a defaultValueLiteral property, but only one must be provided.', + 'Argument "f" has both an externalDefaultValue and a defaultValueLiteral property, but only one must be provided.', ); }); }); diff --git a/src/type/__tests__/directive-test.ts b/src/type/__tests__/directive-test.ts index 03af5e1fd9..cb36cb3b11 100644 --- a/src/type/__tests__/directive-test.ts +++ b/src/type/__tests__/directive-test.ts @@ -39,6 +39,7 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLString, defaultValue: undefined, + externalDefaultValue: undefined, deprecationReason: undefined, extensions: {}, astNode: undefined, @@ -48,6 +49,7 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLInt, defaultValue: undefined, + externalDefaultValue: undefined, deprecationReason: undefined, extensions: {}, astNode: undefined, diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index e4f7219908..88fa5cf04d 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -71,7 +71,7 @@ const QueryType = new GraphQLObjectType({ args: { fromEnum: { type: ComplexEnum, - defaultValue: 'ONE', + externalDefaultValue: 'ONE', }, provideGoodValue: { type: GraphQLBoolean }, provideBadValue: { type: GraphQLBoolean }, @@ -90,6 +90,18 @@ const QueryType = new GraphQLObjectType({ return fromEnum; }, }, + complexEnumWithLegacyDefault: { + type: ComplexEnum, + args: { + fromEnum: { + type: ComplexEnum, + defaultValue: Complex1, + }, + }, + resolve(_source, { fromEnum }) { + return fromEnum; + }, + }, thunkValuesString: { type: GraphQLString, args: { @@ -438,6 +450,20 @@ describe('Type System: Enum Values', () => { }); }); + it('may be internally represented with complex values using legacy internal defaults', () => { + const result = executeQuery(` + { + complexEnumWithLegacyDefault + } + `); + + expectJSON(result).toDeepEqual({ + data: { + complexEnumWithLegacyDefault: 'ONE', + }, + }); + }); + it('may have values specified via a callback', () => { const result = executeQuery('{ thunkValuesString(fromEnum: B) }'); diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index a71ce8c012..f2d1e7f1f3 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -568,15 +568,16 @@ describe('Type predicates', () => { describe('isRequiredArgument', () => { function buildArg(config: { type: GraphQLInputType; - defaultValue?: unknown; + externalDefaultValue?: unknown; }): GraphQLArgument { return { name: 'someArg', type: config.type, description: undefined, - defaultValue: - config.defaultValue !== undefined - ? { value: config.defaultValue } + defaultValue: undefined, + externalDefaultValue: + config.externalDefaultValue !== undefined + ? { value: config.externalDefaultValue } : undefined, deprecationReason: null, extensions: Object.create(null), @@ -599,7 +600,7 @@ describe('Type predicates', () => { const optArg2 = buildArg({ type: GraphQLString, - defaultValue: null, + externalDefaultValue: null, }); expect(isRequiredArgument(optArg2)).to.equal(false); @@ -610,7 +611,7 @@ describe('Type predicates', () => { const optArg4 = buildArg({ type: new GraphQLNonNull(GraphQLString), - defaultValue: 'default', + externalDefaultValue: 'default', }); expect(isRequiredArgument(optArg4)).to.equal(false); }); @@ -619,15 +620,16 @@ describe('Type predicates', () => { describe('isRequiredInputField', () => { function buildInputField(config: { type: GraphQLInputType; - defaultValue?: unknown; + externalDefaultValue?: unknown; }): GraphQLInputField { return { name: 'someInputField', type: config.type, description: undefined, - defaultValue: - config.defaultValue !== undefined - ? { value: config.defaultValue } + defaultValue: undefined, + externalDefaultValue: + config.externalDefaultValue !== undefined + ? { value: config.externalDefaultValue } : undefined, deprecationReason: null, extensions: Object.create(null), @@ -650,7 +652,7 @@ describe('Type predicates', () => { const optField2 = buildInputField({ type: GraphQLString, - defaultValue: null, + externalDefaultValue: null, }); expect(isRequiredInputField(optField2)).to.equal(false); @@ -661,7 +663,7 @@ describe('Type predicates', () => { const optField4 = buildInputField({ type: new GraphQLNonNull(GraphQLString), - defaultValue: 'default', + externalDefaultValue: 'default', }); expect(isRequiredInputField(optField4)).to.equal(false); }); diff --git a/src/type/__tests__/validation-test.ts b/src/type/__tests__/validation-test.ts index 71bfba3527..5bf288aab5 100644 --- a/src/type/__tests__/validation-test.ts +++ b/src/type/__tests__/validation-test.ts @@ -1044,32 +1044,32 @@ describe('Type System: Input Objects must have fields', () => { 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: [] }, + x: { type: AType, externalDefaultValue: null }, + y: { type: AType, externalDefaultValue: { x: null, y: null } }, + z: { type: new GraphQLList(AType), externalDefaultValue: [] }, }), }); const BType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B', fields: () => ({ - x: { type: new GraphQLNonNull(B2Type), defaultValue: {} }, - y: { type: GraphQLString, defaultValue: 'abc' }, - z: { type: CustomType, defaultValue: {} }, + x: { type: new GraphQLNonNull(B2Type), externalDefaultValue: {} }, + y: { type: GraphQLString, externalDefaultValue: 'abc' }, + z: { type: CustomType, externalDefaultValue: {} }, }), }); const B2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B2', fields: () => ({ - x: { type: B3Type, defaultValue: {} }, + x: { type: B3Type, externalDefaultValue: {} }, }), }); const B3Type: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B3', fields: () => ({ - x: { type: BType, defaultValue: { x: { x: null } } }, + x: { type: BType, externalDefaultValue: { x: { x: null } } }, }), }); @@ -1182,64 +1182,64 @@ describe('Type System: Input Objects must have fields', () => { const AType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'A', fields: () => ({ - x: { type: AType, defaultValue: {} }, + x: { type: AType, externalDefaultValue: {} }, }), }); const BType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B', fields: () => ({ - x: { type: B2Type, defaultValue: {} }, + x: { type: B2Type, externalDefaultValue: {} }, }), }); const B2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B2', fields: () => ({ - x: { type: B3Type, defaultValue: {} }, + x: { type: B3Type, externalDefaultValue: {} }, }), }); const B3Type: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'B3', fields: () => ({ - x: { type: BType, defaultValue: {} }, + x: { type: BType, externalDefaultValue: {} }, }), }); const CType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'C', fields: () => ({ - x: { type: new GraphQLList(CType), defaultValue: [{}] }, + x: { type: new GraphQLList(CType), externalDefaultValue: [{}] }, }), }); const DType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'D', fields: () => ({ - x: { type: DType, defaultValue: { x: { x: {} } } }, + x: { type: DType, externalDefaultValue: { x: { x: {} } } }, }), }); const EType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'E', fields: () => ({ - x: { type: EType, defaultValue: { x: null } }, - y: { type: EType, defaultValue: { y: null } }, + x: { type: EType, externalDefaultValue: { x: null } }, + y: { type: EType, externalDefaultValue: { y: null } }, }), }); const FType: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'F', fields: () => ({ - x: { type: new GraphQLNonNull(F2Type), defaultValue: {} }, + x: { type: new GraphQLNonNull(F2Type), externalDefaultValue: {} }, }), }); const F2Type: GraphQLInputObjectType = new GraphQLInputObjectType({ name: 'F2', fields: () => ({ - x: { type: FType, defaultValue: { x: {} } }, + x: { type: FType, externalDefaultValue: { x: {} } }, }), }); @@ -2048,7 +2048,7 @@ describe('Type System: Argument default values must be valid', () => { field: { type: GraphQLInt, args: { - arg: { type: GraphQLInt, defaultValue: 3.14 }, + arg: { type: GraphQLInt, externalDefaultValue: 3.14 }, }, }, }, @@ -2057,7 +2057,7 @@ describe('Type System: Argument default values must be valid', () => { new GraphQLDirective({ name: 'bad', args: { - arg: { type: GraphQLInt, defaultValue: 2.718 }, + arg: { type: GraphQLInt, externalDefaultValue: 2.718 }, }, locations: [DirectiveLocation.FIELD], }), @@ -2105,15 +2105,15 @@ describe('Type System: Argument default values must be valid', () => { args: { argWithPossibleFix: { type: testInput, - defaultValue: { self: null, string: [1], enum: Exotic }, + externalDefaultValue: { self: null, string: [1], enum: Exotic }, }, argWithInvalidPossibleFix: { type: testInput, - defaultValue: { string: null }, + externalDefaultValue: { string: null }, }, argWithoutPossibleFix: { type: testInput, - defaultValue: { enum: 'Exotic' }, + externalDefaultValue: { enum: 'Exotic' }, }, }, }, @@ -2185,15 +2185,15 @@ describe('Type System: Argument default values must be valid', () => { arg.type = testInput; switch (arg.name) { case 'argWithPossibleFix': - arg.defaultValue = { + arg.externalDefaultValue = { value: { self: null, string: [1], enum: Exotic }, }; break; case 'argWithInvalidPossibleFix': - arg.defaultValue = { value: { string: null } }; + arg.externalDefaultValue = { value: { string: null } }; break; case 'argWithoutPossibleFix': - arg.defaultValue = { value: { enum: 'Exotic' } }; + arg.externalDefaultValue = { value: { enum: 'Exotic' } }; break; } } @@ -2342,7 +2342,7 @@ describe('Type System: Input Object field default values must be valid', () => { fields: { field: { type: GraphQLInt, - defaultValue: 3.14, + externalDefaultValue: 3.14, }, }, }); diff --git a/src/type/definition.ts b/src/type/definition.ts index 8d51201070..78976af242 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -905,7 +905,8 @@ export function defineArguments( name: assertName(argName), description: argConfig.description, type: argConfig.type, - defaultValue: defineDefaultValue(argName, argConfig), + defaultValue: argConfig.defaultValue, + externalDefaultValue: defineExternalDefaultValue(argName, argConfig), deprecationReason: argConfig.deprecationReason, extensions: toObjMapWithSymbols(argConfig.extensions), astNode: argConfig.astNode, @@ -939,8 +940,9 @@ export function argsToArgsConfig( (arg) => ({ description: arg.description, type: arg.type, - defaultValue: arg.defaultValue?.value, - defaultValueLiteral: arg.defaultValue?.literal, + defaultValue: arg.defaultValue, + externalDefaultValue: arg.externalDefaultValue?.value, + defaultValueLiteral: arg.externalDefaultValue?.literal, deprecationReason: arg.deprecationReason, extensions: arg.extensions, astNode: arg.astNode, @@ -1054,6 +1056,7 @@ export interface GraphQLArgumentConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + externalDefaultValue?: unknown; defaultValueLiteral?: ConstValueNode | undefined; deprecationReason?: Maybe; extensions?: Maybe>; @@ -1080,7 +1083,8 @@ export interface GraphQLArgument { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: GraphQLDefaultValueUsage | undefined; + defaultValue: unknown; + externalDefaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; @@ -1089,7 +1093,11 @@ export interface GraphQLArgument { export function isRequiredArgument( arg: GraphQLArgument | GraphQLVariableSignature, ): boolean { - return isNonNullType(arg.type) && arg.defaultValue === undefined; + return ( + isNonNullType(arg.type) && + arg.externalDefaultValue === undefined && + arg.defaultValue === undefined + ); } export type GraphQLFieldMap = ObjMap< @@ -1100,20 +1108,23 @@ export type GraphQLDefaultValueUsage = | { value: unknown; literal?: never } | { literal: ConstValueNode; value?: never }; -export function defineDefaultValue( +export function defineExternalDefaultValue( argName: string, config: GraphQLArgumentConfig | GraphQLInputFieldConfig, ): GraphQLDefaultValueUsage | undefined { - if (config.defaultValue === undefined && !config.defaultValueLiteral) { + if ( + config.externalDefaultValue === undefined && + !config.defaultValueLiteral + ) { return; } devAssert( - !(config.defaultValue !== undefined && config.defaultValueLiteral), - `Argument "${argName}" has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`, + !(config.externalDefaultValue !== undefined && config.defaultValueLiteral), + `Argument "${argName}" has both an externalDefaultValue and a defaultValueLiteral property, but only one must be provided.`, ); return config.defaultValueLiteral ? { literal: config.defaultValueLiteral } - : { value: config.defaultValue }; + : { value: config.externalDefaultValue }; } /** @@ -1703,8 +1714,9 @@ export class GraphQLInputObjectType { const fields = mapValue(this.getFields(), (field) => ({ description: field.description, type: field.type, - defaultValue: field.defaultValue?.value, - defaultValueLiteral: field.defaultValue?.literal, + defaultValue: field.defaultValue, + externalDefaultValue: field.externalDefaultValue?.value, + defaultValueLiteral: field.externalDefaultValue?.literal, deprecationReason: field.deprecationReason, extensions: field.extensions, astNode: field.astNode, @@ -1738,7 +1750,8 @@ function defineInputFieldMap( name: assertName(fieldName), description: fieldConfig.description, type: fieldConfig.type, - defaultValue: defineDefaultValue(fieldName, fieldConfig), + defaultValue: fieldConfig.defaultValue, + externalDefaultValue: defineExternalDefaultValue(fieldName, fieldConfig), deprecationReason: fieldConfig.deprecationReason, extensions: toObjMapWithSymbols(fieldConfig.extensions), astNode: fieldConfig.astNode, @@ -1779,6 +1792,7 @@ export interface GraphQLInputFieldConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + externalDefaultValue?: unknown; defaultValueLiteral?: ConstValueNode | undefined; deprecationReason?: Maybe; extensions?: Maybe>; @@ -1791,14 +1805,19 @@ export interface GraphQLInputField { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: GraphQLDefaultValueUsage | undefined; + defaultValue: unknown; + externalDefaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; } export function isRequiredInputField(field: GraphQLInputField): boolean { - return isNonNullType(field.type) && field.defaultValue === undefined; + return ( + isNonNullType(field.type) && + field.defaultValue === undefined && + field.externalDefaultValue === undefined + ); } export type GraphQLInputFieldMap = ObjMap; diff --git a/src/type/directives.ts b/src/type/directives.ts index 23faa33717..b0564a5064 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -168,7 +168,7 @@ export const GraphQLDeferDirective = new GraphQLDirective({ if: { type: new GraphQLNonNull(GraphQLBoolean), description: 'Deferred when true or undefined.', - defaultValue: true, + externalDefaultValue: true, }, label: { type: GraphQLString, @@ -189,14 +189,14 @@ export const GraphQLStreamDirective = new GraphQLDirective({ if: { type: new GraphQLNonNull(GraphQLBoolean), description: 'Stream when true or undefined.', - defaultValue: true, + externalDefaultValue: true, }, label: { type: GraphQLString, description: 'Unique name', }, initialCount: { - defaultValue: 0, + externalDefaultValue: 0, type: GraphQLInt, description: 'Number of items to return immediately', }, @@ -226,7 +226,7 @@ export const GraphQLDeprecatedDirective: GraphQLDirective = type: GraphQLString, description: 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).', - defaultValue: DEFAULT_DEPRECATION_REASON, + externalDefaultValue: DEFAULT_DEPRECATION_REASON, }, }, }); diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2f6c498f9a..b8f2b7cf0c 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 { valueToLiteral } from '../utilities/valueToLiteral.js'; +import { getDefaultValueAST } from '../utilities/getDefaultValueAST.js'; import type { GraphQLEnumValue, @@ -108,7 +108,7 @@ export const __Directive: GraphQLObjectType = new GraphQLObjectType({ args: { includeDeprecated: { type: GraphQLBoolean, - defaultValue: false, + externalDefaultValue: false, }, }, resolve(field, { includeDeprecated }) { @@ -265,7 +265,10 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ fields: { type: new GraphQLList(new GraphQLNonNull(__Field)), args: { - includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + includeDeprecated: { + type: GraphQLBoolean, + externalDefaultValue: false, + }, }, resolve(type, { includeDeprecated }) { if (isObjectType(type) || isInterfaceType(type)) { @@ -295,7 +298,10 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ enumValues: { type: new GraphQLList(new GraphQLNonNull(__EnumValue)), args: { - includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + includeDeprecated: { + type: GraphQLBoolean, + externalDefaultValue: false, + }, }, resolve(type, { includeDeprecated }) { if (isEnumType(type)) { @@ -311,7 +317,7 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ args: { includeDeprecated: { type: GraphQLBoolean, - defaultValue: false, + externalDefaultValue: false, }, }, resolve(type, { includeDeprecated }) { @@ -359,7 +365,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ args: { includeDeprecated: { type: GraphQLBoolean, - defaultValue: false, + externalDefaultValue: false, }, }, resolve(field, { includeDeprecated }) { @@ -406,14 +412,11 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ description: 'A GraphQL-formatted string representing the default value for this input value.', resolve(inputValue) { - const { type, defaultValue } = inputValue; - if (!defaultValue) { - return null; + const ast = getDefaultValueAST(inputValue); + if (ast) { + return print(ast); } - const literal = - defaultValue.literal ?? valueToLiteral(defaultValue.value, type); - invariant(literal != null, 'Invalid default value'); - return print(literal); + return null; }, }, isDeprecated: { @@ -537,6 +540,7 @@ export const TypeMetaFieldDef: GraphQLField = { description: undefined, type: new GraphQLNonNull(GraphQLString), defaultValue: undefined, + externalDefaultValue: undefined, deprecationReason: undefined, extensions: Object.create(null), astNode: undefined, diff --git a/src/type/validate.ts b/src/type/validate.ts index 48cc70299a..a64c1cfb13 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -238,7 +238,7 @@ function validateDefaultValue( inputValue: GraphQLArgument | GraphQLInputField, argStr: string, ): void { - const defaultValue = inputValue.defaultValue; + const defaultValue = inputValue.externalDefaultValue; if (!defaultValue) { return; @@ -717,7 +717,10 @@ function validateOneOfInputObjectField( ); } - if (field.defaultValue !== undefined) { + if ( + field.externalDefaultValue !== undefined || + field.defaultValue !== undefined + ) { context.reportError( `OneOf input field ${type}.${field.name} cannot have a default value.`, field.astNode, @@ -902,7 +905,7 @@ function createInputObjectDefaultValueCircularRefsValidator( fieldStr: string, ): void { // Only a field with a default value can result in a cycle. - const defaultValue = field.defaultValue; + const defaultValue = field.externalDefaultValue; if (defaultValue === undefined) { return; } diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index e49733f0c2..52fd6ffb78 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -14,7 +14,6 @@ import { getEnterLeaveForKind } from '../language/visitor.js'; import type { GraphQLArgument, GraphQLCompositeType, - GraphQLDefaultValueUsage, GraphQLEnumValue, GraphQLField, GraphQLInputField, @@ -54,7 +53,7 @@ export class TypeInfo { private _parentTypeStack: Array>; private _inputTypeStack: Array>; private _fieldDefStack: Array>>; - private _defaultValueStack: Array; + private _defaultValueStack: Array; private _directive: Maybe; private _argument: Maybe; private _enumValue: Maybe; @@ -125,7 +124,7 @@ export class TypeInfo { return this._fieldDefStack.at(-1); } - getDefaultValue(): GraphQLDefaultValueUsage | undefined { + getDefaultValue(): unknown { return this._defaultValueStack.at(-1); } @@ -232,7 +231,9 @@ export class TypeInfo { } } this._argument = argDef; - this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined); + this._defaultValueStack.push( + argDef?.externalDefaultValue ?? argDef?.defaultValue ?? undefined, + ); this._inputTypeStack.push(isInputType(argType) ? argType : undefined); break; } @@ -270,7 +271,9 @@ export class TypeInfo { } } this._defaultValueStack.push( - inputField ? inputField.defaultValue : undefined, + inputField?.externalDefaultValue ?? + inputField?.defaultValue ?? + undefined, ); this._inputTypeStack.push( isInputType(inputFieldType) ? inputFieldType : undefined, diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 85de0d8e4e..fa796d1059 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -187,13 +187,13 @@ describe('coerceInputValue', () => { }); describe('for GraphQLInputObject with default value', () => { - const makeTestInputObject = (defaultValue: any) => + const makeTestInputObject = (externalDefaultValue: any) => new GraphQLInputObjectType({ name: 'TestInputObject', fields: { foo: { type: new GraphQLScalarType({ name: 'TestScalar' }), - defaultValue, + externalDefaultValue, }, }, }); @@ -473,7 +473,7 @@ describe('coerceInputLiteral', () => { const type = new GraphQLInputObjectType({ name: 'TestInput', fields: { - int: { type: GraphQLInt, defaultValue: 42 }, + int: { type: GraphQLInt, externalDefaultValue: 42 }, float: { type: GraphQLFloat, defaultValueLiteral: { kind: Kind.FLOAT, value: '3.14' }, @@ -487,7 +487,7 @@ describe('coerceInputLiteral', () => { const testInputObj = new GraphQLInputObjectType({ name: 'TestInput', fields: { - int: { type: GraphQLInt, defaultValue: 42 }, + int: { type: GraphQLInt, externalDefaultValue: 42 }, bool: { type: GraphQLBoolean }, requiredBool: { type: nonNullBool }, }, @@ -631,10 +631,16 @@ describe('coerceDefaultValue', () => { const defaultValueUsage = { literal: { kind: Kind.STRING, value: 'hello' }, } as const; - expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + + const inputValue = { + externalDefaultValue: defaultValueUsage, + type: spyScalar, + }; + + expect(coerceDefaultValue(inputValue)).to.equal('hello'); // Call a second time - expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + expect(coerceDefaultValue(inputValue)).to.equal('hello'); expect(coerceInputValueCalls).to.deep.equal(['hello']); }); }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index ac9002e388..01addafb7e 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -148,7 +148,7 @@ describe('Type System Printer', () => { it('Prints String Field With Int Arg With Default', () => { const schema = buildSingleFieldSchema({ type: GraphQLString, - args: { argOne: { type: GraphQLInt, defaultValue: 2 } }, + args: { argOne: { type: GraphQLInt, externalDefaultValue: 2 } }, }); expectPrintedSchema(schema).to.equal(dedent` @@ -161,7 +161,9 @@ describe('Type System Printer', () => { it('Prints String Field With String Arg With Default', () => { const schema = buildSingleFieldSchema({ type: GraphQLString, - args: { argOne: { type: GraphQLString, defaultValue: 'tes\t de\fault' } }, + args: { + argOne: { type: GraphQLString, externalDefaultValue: 'tes\t de\fault' }, + }, }); expectPrintedSchema(schema).to.equal( @@ -176,7 +178,7 @@ describe('Type System Printer', () => { it('Prints String Field With Int Arg With Default Null', () => { const schema = buildSingleFieldSchema({ type: GraphQLString, - args: { argOne: { type: GraphQLInt, defaultValue: null } }, + args: { argOne: { type: GraphQLInt, externalDefaultValue: null } }, }); expectPrintedSchema(schema).to.equal(dedent` @@ -219,7 +221,7 @@ describe('Type System Printer', () => { const schema = buildSingleFieldSchema({ type: GraphQLString, args: { - argOne: { type: GraphQLInt, defaultValue: 1 }, + argOne: { type: GraphQLInt, externalDefaultValue: 1 }, argTwo: { type: GraphQLString }, argThree: { type: GraphQLBoolean }, }, @@ -237,7 +239,7 @@ describe('Type System Printer', () => { type: GraphQLString, args: { argOne: { type: GraphQLInt }, - argTwo: { type: GraphQLString, defaultValue: 'foo' }, + argTwo: { type: GraphQLString, externalDefaultValue: 'foo' }, argThree: { type: GraphQLBoolean }, }, }); @@ -255,7 +257,7 @@ describe('Type System Printer', () => { args: { argOne: { type: GraphQLInt }, argTwo: { type: GraphQLString }, - argThree: { type: GraphQLBoolean, defaultValue: false }, + argThree: { type: GraphQLBoolean, externalDefaultValue: false }, }, }); @@ -601,7 +603,7 @@ describe('Type System Printer', () => { description: 'Complex Directive', args: { stringArg: { type: GraphQLString }, - intArg: { type: GraphQLInt, defaultValue: -1 }, + intArg: { type: GraphQLInt, externalDefaultValue: -1 }, }, isRepeatable: true, locations: [DirectiveLocation.FIELD, DirectiveLocation.QUERY], diff --git a/src/utilities/__tests__/validateInputValue-test.ts b/src/utilities/__tests__/validateInputValue-test.ts index fdf3083c19..3a78aa507c 100644 --- a/src/utilities/__tests__/validateInputValue-test.ts +++ b/src/utilities/__tests__/validateInputValue-test.ts @@ -319,13 +319,13 @@ describe('validateInputValue', () => { }); describe('for GraphQLInputObject with default value', () => { - function makeTestInputObject(defaultValue: unknown) { + function makeTestInputObject(externalDefaultValue: unknown) { return new GraphQLInputObjectType({ name: 'TestInputObject', fields: { foo: { type: new GraphQLScalarType({ name: 'TestScalar' }), - defaultValue, + externalDefaultValue, }, }, }); @@ -659,7 +659,10 @@ describe('validateInputLiteral', () => { fields: { foo: { type: new GraphQLNonNull(GraphQLInt) }, bar: { type: GraphQLInt }, - optional: { type: new GraphQLNonNull(GraphQLInt), defaultValue: 42 }, + optional: { + type: new GraphQLNonNull(GraphQLInt), + externalDefaultValue: 42, + }, }, }); @@ -792,13 +795,13 @@ describe('validateInputLiteral', () => { }); describe('for GraphQLInputObject with default value', () => { - function makeTestInputObject(defaultValue: unknown) { + function makeTestInputObject(externalDefaultValue: unknown) { return new GraphQLInputObjectType({ name: 'TestInputObject', fields: { foo: { type: new GraphQLScalarType({ name: 'TestScalar' }), - defaultValue, + externalDefaultValue, }, }, }); diff --git a/src/utilities/__tests__/valueFromAST-test.ts b/src/utilities/__tests__/valueFromAST-test.ts index 2bed756925..339cff1f50 100644 --- a/src/utilities/__tests__/valueFromAST-test.ts +++ b/src/utilities/__tests__/valueFromAST-test.ts @@ -191,7 +191,7 @@ describe('valueFromAST', () => { const testInputObj = new GraphQLInputObjectType({ name: 'TestInput', fields: { - int: { type: GraphQLInt, defaultValue: 42 }, + int: { type: GraphQLInt, externalDefaultValue: 42 }, bool: { type: GraphQLBoolean }, requiredBool: { type: nonNullBool }, }, diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index a946884bb0..116cd1db2a 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -82,11 +82,9 @@ export function coerceInputValue( if (isRequiredInputField(field)) { return; // Invalid: intentionally return no value. } - if (field.defaultValue) { - coercedValue[field.name] = coerceDefaultValue( - field.defaultValue, - field.type, - ); + const coercedDefaultValue = coerceDefaultValue(field); + if (coercedDefaultValue !== undefined) { + coercedValue[field.name] = coercedDefaultValue; } } else { const coercedField = coerceInputValue(fieldValue, field.type); @@ -237,11 +235,9 @@ export function coerceInputLiteral( if (isRequiredInputField(field)) { return; // Invalid: intentionally return no value. } - if (field.defaultValue) { - coercedValue[field.name] = coerceDefaultValue( - field.defaultValue, - field.type, - ); + const coercedDefaultValue = coerceDefaultValue(field); + if (coercedDefaultValue !== undefined) { + coercedValue[field.name] = coercedDefaultValue; } } else { const fieldValue = coerceInputLiteral( @@ -297,21 +293,35 @@ function getCoercedVariableValue( return variableValues?.coerced[varName]; } +interface InputValue { + type: GraphQLInputType; + externalDefaultValue?: GraphQLDefaultValueUsage | undefined; + defaultValue?: unknown; +} + /** * @internal */ -export function coerceDefaultValue( - defaultValue: GraphQLDefaultValueUsage, - type: GraphQLInputType, -): unknown { +export function coerceDefaultValue(inputValue: InputValue): 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) - : coerceInputValue(defaultValue.value, type); - invariant(coercedValue !== undefined); - (defaultValue as any)._memoizedCoercedValue = coercedValue; + let coercedDefaultValue = (inputValue as any)._memoizedCoercedDefaultValue; + if (coercedDefaultValue !== undefined) { + return coercedDefaultValue; + } + + const externalDefaultValue = inputValue.externalDefaultValue; + if (externalDefaultValue !== undefined) { + coercedDefaultValue = externalDefaultValue.literal + ? coerceInputLiteral(externalDefaultValue.literal, inputValue.type) + : coerceInputValue(externalDefaultValue.value, inputValue.type); + invariant(coercedDefaultValue !== undefined); + (inputValue as any)._memoizedCoercedDefaultValue = coercedDefaultValue; + return coercedDefaultValue; + } + + const defaultValue = inputValue.defaultValue; + if (defaultValue !== undefined) { + (inputValue as any)._memoizedCoercedDefaultValue = defaultValue; } - return coercedValue; + return defaultValue; } diff --git a/src/utilities/findSchemaChanges.ts b/src/utilities/findSchemaChanges.ts index 4cffd5aad6..d0b2f4b7d6 100644 --- a/src/utilities/findSchemaChanges.ts +++ b/src/utilities/findSchemaChanges.ts @@ -5,11 +5,11 @@ import { keyMap } from '../jsutils/keyMap.js'; import { print } from '../language/printer.js'; import type { - GraphQLDefaultValueUsage, + GraphQLArgument, GraphQLEnumType, GraphQLField, + GraphQLInputField, GraphQLInputObjectType, - GraphQLInputType, GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, @@ -32,8 +32,8 @@ import { import { isSpecifiedScalarType } from '../type/scalars.js'; import type { GraphQLSchema } from '../type/schema.js'; +import { getDefaultValueAST } from './getDefaultValueAST.js'; import { sortValueNode } from './sortValueNode.js'; -import { valueToLiteral } from './valueToLiteral.js'; export const BreakingChangeType = { TYPE_REMOVED: 'TYPE_REMOVED' as const, @@ -201,6 +201,8 @@ function findDirectiveChanges( newArg.type, ); + const oldDefaultValueStr = getDefaultValue(oldArg); + const newDefaultValueStr = getDefaultValue(newArg); if (!isSafe) { schemaChanges.push({ type: BreakingChangeType.ARG_CHANGED_KIND, @@ -208,34 +210,25 @@ function findDirectiveChanges( `Argument @${oldDirective.name}(${oldArg.name}:) has changed type from ` + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); - } else if (oldArg.defaultValue !== undefined) { - if (newArg.defaultValue === undefined) { + } else if (oldDefaultValueStr !== undefined) { + if (newDefaultValueStr === undefined) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, description: `@${oldDirective.name}(${oldArg.name}:) defaultValue was removed.`, }); - } else { - // Since we looking only for client's observable changes we should - // compare default values in the same representation as they are - // represented inside introspection. - const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type); - const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); - - if (oldValueStr !== newValueStr) { - schemaChanges.push({ - type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, - description: `@${oldDirective.name}(${oldArg.name}:) has changed defaultValue from ${oldValueStr} to ${newValueStr}.`, - }); - } + } else if (oldDefaultValueStr !== newDefaultValueStr) { + schemaChanges.push({ + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + description: `@${oldDirective.name}(${oldArg.name}:) has changed defaultValue from ${oldDefaultValueStr} to ${newDefaultValueStr}.`, + }); } } else if ( - newArg.defaultValue !== undefined && - oldArg.defaultValue === undefined + newDefaultValueStr !== undefined && + oldDefaultValueStr === undefined ) { - const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); schemaChanges.push({ type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED, - description: `@${oldDirective.name}(${oldArg.name}:) added a defaultValue ${newValueStr}.`, + description: `@${oldDirective.name}(${oldArg.name}:) added a defaultValue ${newDefaultValueStr}.`, }); } else if (oldArg.type.toString() !== newArg.type.toString()) { schemaChanges.push({ @@ -582,6 +575,8 @@ function findArgChanges( newArg.type, ); + const oldDefaultValueStr = getDefaultValue(oldArg); + const newDefaultValueStr = getDefaultValue(newArg); if (!isSafe) { schemaChanges.push({ type: BreakingChangeType.ARG_CHANGED_KIND, @@ -589,34 +584,25 @@ function findArgChanges( `Argument ${oldType}.${oldField.name}(${oldArg.name}:) has changed type from ` + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); - } else if (oldArg.defaultValue !== undefined) { - if (newArg.defaultValue === undefined) { + } else if (oldDefaultValueStr !== undefined) { + if (newDefaultValueStr === undefined) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, description: `${oldType}.${oldField.name}(${oldArg.name}:) defaultValue was removed.`, }); - } else { - // Since we looking only for client's observable changes we should - // compare default values in the same representation as they are - // represented inside introspection. - const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type); - const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); - - if (oldValueStr !== newValueStr) { - schemaChanges.push({ - type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, - description: `${oldType}.${oldField.name}(${oldArg.name}:) has changed defaultValue from ${oldValueStr} to ${newValueStr}.`, - }); - } + } else if (oldDefaultValueStr !== newDefaultValueStr) { + schemaChanges.push({ + type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, + description: `${oldType}.${oldField.name}(${oldArg.name}:) has changed defaultValue from ${oldDefaultValueStr} to ${newDefaultValueStr}.`, + }); } } else if ( - newArg.defaultValue !== undefined && - oldArg.defaultValue === undefined + newDefaultValueStr !== undefined && + oldDefaultValueStr === undefined ) { - const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); schemaChanges.push({ type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED, - description: `${oldType}.${oldField.name}(${oldArg.name}:) added a defaultValue ${newValueStr}.`, + description: `${oldType}.${oldField.name}(${oldArg.name}:) added a defaultValue ${newDefaultValueStr}.`, }); } else if (oldArg.type.toString() !== newArg.type.toString()) { schemaChanges.push({ @@ -742,13 +728,16 @@ function typeKindName(type: GraphQLNamedType): string { invariant(false, 'Unexpected type: ' + inspect(type)); } -function stringifyValue( - defaultValue: GraphQLDefaultValueUsage, - type: GraphQLInputType, -): string { - const ast = defaultValue.literal ?? valueToLiteral(defaultValue.value, type); - invariant(ast != null); - return print(sortValueNode(ast)); +// Since we looking only for client's observable changes we should +// compare default values in the same representation as they are +// represented inside introspection. +function getDefaultValue( + argOrInputField: GraphQLArgument | GraphQLInputField, +): string | undefined { + const ast = getDefaultValueAST(argOrInputField); + if (ast) { + return print(sortValueNode(ast)); + } } function diff( diff --git a/src/utilities/getDefaultValueAST.ts b/src/utilities/getDefaultValueAST.ts new file mode 100644 index 0000000000..8ae9d9fbe9 --- /dev/null +++ b/src/utilities/getDefaultValueAST.ts @@ -0,0 +1,30 @@ +import { invariant } from '../jsutils/invariant.js'; + +import type { ConstValueNode } from '../language/ast.js'; + +import type { GraphQLArgument, GraphQLInputField } from '../type/definition.js'; + +import { astFromValue } from './astFromValue.js'; +import { valueToLiteral } from './valueToLiteral.js'; + +export function getDefaultValueAST( + argOrInputField: GraphQLArgument | GraphQLInputField, +): ConstValueNode | undefined { + const type = argOrInputField.type; + const externalDefaultValue = argOrInputField.externalDefaultValue; + if (externalDefaultValue) { + const literal = + externalDefaultValue.literal ?? + valueToLiteral(externalDefaultValue.value, type); + invariant(literal != null, 'Invalid default value'); + return literal; + } + + const defaultValue = argOrInputField.defaultValue; + if (defaultValue !== undefined) { + const valueAST = astFromValue(defaultValue, type); + invariant(valueAST != null, 'Invalid default value'); + return valueAST; + } + return undefined; +} diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 52fc342626..be5238ae3a 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 { valueToLiteral } from './valueToLiteral.js'; +import { getDefaultValueAST } from './getDefaultValueAST.js'; export function printSchema(schema: GraphQLSchema): string { return printFilteredSchema( @@ -258,16 +258,15 @@ function printArgs( ); } -function printInputValue(arg: GraphQLInputField): string { - let argDecl = arg.name + ': ' + String(arg.type); - if (arg.defaultValue) { - const literal = - arg.defaultValue.literal ?? - valueToLiteral(arg.defaultValue.value, arg.type); - invariant(literal != null, 'Invalid default value'); - argDecl += ` = ${print(literal)}`; +function printInputValue( + argOrInputField: GraphQLArgument | GraphQLInputField, +): string { + let argDecl = argOrInputField.name + ': ' + String(argOrInputField.type); + const defaultValueAST = getDefaultValueAST(argOrInputField); + if (defaultValueAST) { + argDecl += ` = ${print(defaultValueAST)}`; } - return argDecl + printDeprecated(arg.deprecationReason); + return argDecl + printDeprecated(argOrInputField.deprecationReason); } export function printDirective(directive: GraphQLDirective): string { diff --git a/src/utilities/replaceVariables.ts b/src/utilities/replaceVariables.ts index 531863779a..58c8f7a795 100644 --- a/src/utilities/replaceVariables.ts +++ b/src/utilities/replaceVariables.ts @@ -37,9 +37,10 @@ export function replaceVariables( } if (scopedVariableSource.value === undefined) { - const defaultValue = scopedVariableSource.signature.defaultValue; + const defaultValue = + scopedVariableSource.signature.externalDefaultValue; if (defaultValue !== undefined) { - return defaultValue.literal as ConstValueNode; + return defaultValue.literal; } } @@ -58,7 +59,7 @@ export function replaceVariables( if ( scopedVariableSource?.value === undefined && - scopedVariableSource?.signature.defaultValue === undefined + scopedVariableSource?.signature.externalDefaultValue === undefined ) { continue; } diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index e0ff5d8bf2..85d5a5e8b9 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -114,8 +114,8 @@ export function valueFromAST( for (const field of Object.values(type.getFields())) { const fieldNode = fieldNodes.get(field.name); if (fieldNode == null || isMissingVariable(fieldNode.value, variables)) { - if (field.defaultValue !== undefined) { - coercedObj[field.name] = field.defaultValue.value; + if (field.externalDefaultValue !== undefined) { + coercedObj[field.name] = field.externalDefaultValue.value; } else if (isNonNullType(field.type)) { return; // Invalid: intentionally return no value. } diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index b750cccc28..82bdd964e1 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -19,7 +19,6 @@ import { visit } from '../language/visitor.js'; import type { GraphQLArgument, GraphQLCompositeType, - GraphQLDefaultValueUsage, GraphQLEnumValue, GraphQLField, GraphQLInputType, @@ -36,7 +35,7 @@ interface VariableUsage { readonly node: VariableNode; readonly type: Maybe; readonly parentType: Maybe; - readonly defaultValue: GraphQLDefaultValueUsage | undefined; + readonly defaultValue: unknown; readonly fragmentVariableDefinition: Maybe; } diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index dfcedbe547..d3e50c457a 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -7,10 +7,7 @@ import type { ValueNode, VariableDefinitionNode } from '../../language/ast.js'; import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import type { - GraphQLDefaultValueUsage, - GraphQLType, -} from '../../type/definition.js'; +import type { GraphQLType } from '../../type/definition.js'; import { isInputObjectType, isNonNullType, @@ -121,7 +118,7 @@ function allowedVariableUsage( varType: GraphQLType, varDefaultValue: Maybe, locationType: GraphQLType, - locationDefaultValue: GraphQLDefaultValueUsage | undefined, + locationDefaultValue: unknown, ): boolean { if (isNonNullType(locationType) && !isNonNullType(varType)) { const hasNonNullVariableDefaultValue =