Skip to content

Commit 05db9bd

Browse files
committed
Preserve defaultValue literals
Fixes #3051
1 parent 9830fda commit 05db9bd

23 files changed

+259
-64
lines changed

src/execution/values.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { GraphQLSchema } from '../type/schema';
1717
import type { GraphQLField } from '../type/definition';
1818
import type { GraphQLDirective } from '../type/directives';
1919
import { isInputType, isNonNullType } from '../type/definition';
20+
import { getCoercedDefaultValue } from '../type/defaultValues';
2021

2122
import { typeFromAST } from '../utilities/typeFromAST';
2223
import { valueFromAST } from '../utilities/valueFromAST';
@@ -173,8 +174,11 @@ export function getArgumentValues(
173174
const argumentNode = argNodeMap[name];
174175

175176
if (!argumentNode) {
176-
if (argDef.defaultValue !== undefined) {
177-
coercedValues[name] = argDef.defaultValue;
177+
if (argDef.defaultValue) {
178+
coercedValues[name] = getCoercedDefaultValue(
179+
argDef.defaultValue,
180+
argDef.type,
181+
);
178182
} else if (isNonNullType(argType)) {
179183
throw new GraphQLError(
180184
`Argument "${name}" of required type "${inspect(argType)}" ` +
@@ -194,8 +198,11 @@ export function getArgumentValues(
194198
variableValues == null ||
195199
!hasOwnProperty(variableValues, variableName)
196200
) {
197-
if (argDef.defaultValue !== undefined) {
198-
coercedValues[name] = argDef.defaultValue;
201+
if (argDef.defaultValue) {
202+
coercedValues[name] = getCoercedDefaultValue(
203+
argDef.defaultValue,
204+
argDef.type,
205+
);
199206
} else if (isNonNullType(argType)) {
200207
throw new GraphQLError(
201208
`Argument "${name}" of required type "${inspect(argType)}" ` +

src/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export {
150150
GraphQLArgumentConfig,
151151
GraphQLArgumentExtensions,
152152
GraphQLInputValue,
153+
GraphQLDefaultValueUsage,
153154
GraphQLInputValueConfig,
154155
GraphQLEnumTypeConfig,
155156
GraphQLEnumTypeExtensions,

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export type {
146146
GraphQLArgument,
147147
GraphQLArgumentConfig,
148148
GraphQLInputValue,
149+
GraphQLDefaultValueUsage,
149150
GraphQLInputValueConfig,
150151
GraphQLEnumTypeConfig,
151152
GraphQLEnumValue,

src/type/__tests__/definition-test.js

+67
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,73 @@ describe('Type System: Input Objects', () => {
818818
);
819819
});
820820
});
821+
822+
describe('Input Object fields may have default values', () => {
823+
it('accepts an Input Object type with a default value', () => {
824+
const inputObjType = new GraphQLInputObjectType({
825+
name: 'SomeInputObject',
826+
fields: {
827+
f: { type: ScalarType, defaultValue: 3 },
828+
},
829+
});
830+
expect(inputObjType.getFields()).to.deep.equal({
831+
f: {
832+
name: 'f',
833+
description: undefined,
834+
type: ScalarType,
835+
defaultValue: {
836+
value: 3,
837+
literal: undefined,
838+
},
839+
deprecationReason: undefined,
840+
extensions: undefined,
841+
astNode: undefined,
842+
},
843+
});
844+
});
845+
846+
it('accepts an Input Object type with a default value literal', () => {
847+
const inputObjType = new GraphQLInputObjectType({
848+
name: 'SomeInputObject',
849+
fields: {
850+
f: {
851+
type: ScalarType,
852+
defaultValueLiteral: { kind: 'IntValue', value: '3' },
853+
},
854+
},
855+
});
856+
expect(inputObjType.getFields()).to.deep.equal({
857+
f: {
858+
name: 'f',
859+
description: undefined,
860+
type: ScalarType,
861+
defaultValue: {
862+
value: undefined,
863+
literal: { kind: 'IntValue', value: '3' },
864+
},
865+
deprecationReason: undefined,
866+
extensions: undefined,
867+
astNode: undefined,
868+
},
869+
});
870+
});
871+
872+
it('rejects an Input Object type with potentially conflicting default values', () => {
873+
const inputObjType = new GraphQLInputObjectType({
874+
name: 'SomeInputObject',
875+
fields: {
876+
f: {
877+
type: ScalarType,
878+
defaultValue: 3,
879+
defaultValueLiteral: { kind: 'IntValue', value: '3' },
880+
},
881+
},
882+
});
883+
expect(() => inputObjType.getFields()).to.throw(
884+
'f has both a defaultValue and a defaultValueLiteral property, but only one must be provided.',
885+
);
886+
});
887+
});
821888
});
822889

823890
describe('Type System: List', () => {

src/type/__tests__/predicate-test.js

+11-20
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
assertNamedType,
7070
getNullableType,
7171
getNamedType,
72+
defineInputValue,
7273
} from '../definition';
7374

7475
const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} });
@@ -562,19 +563,14 @@ describe('Type predicates', () => {
562563
});
563564

564565
describe('isRequiredInput', () => {
565-
function buildArg(config: {|
566+
function buildArg({
567+
type,
568+
defaultValue,
569+
}: {|
566570
type: GraphQLInputType,
567571
defaultValue?: mixed,
568572
|}): GraphQLArgument {
569-
return {
570-
name: 'someArg',
571-
type: config.type,
572-
description: undefined,
573-
defaultValue: config.defaultValue,
574-
deprecationReason: null,
575-
extensions: undefined,
576-
astNode: undefined,
577-
};
573+
return defineInputValue({ type, defaultValue }, 'someArg');
578574
}
579575

580576
it('returns true for required arguments', () => {
@@ -608,19 +604,14 @@ describe('Type predicates', () => {
608604
expect(isRequiredInput(optArg4)).to.equal(false);
609605
});
610606

611-
function buildInputField(config: {|
607+
function buildInputField({
608+
type,
609+
defaultValue,
610+
}: {|
612611
type: GraphQLInputType,
613612
defaultValue?: mixed,
614613
|}): GraphQLInputField {
615-
return {
616-
name: 'someInputField',
617-
type: config.type,
618-
description: undefined,
619-
defaultValue: config.defaultValue,
620-
deprecationReason: null,
621-
extensions: undefined,
622-
astNode: undefined,
623-
};
614+
return defineInputValue({ type, defaultValue }, 'someInputField');
624615
}
625616

626617
it('returns true for required input field', () => {

src/type/defaultValues.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { GraphQLInputType, GraphQLDefaultValueUsage } from './definition';
2+
3+
/**
4+
* @internal
5+
*/
6+
export function getCoercedDefaultValue(
7+
usage: GraphQLDefaultValueUsage,
8+
type: GraphQLInputType,
9+
): unknown;

src/type/defaultValues.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { invariant } from '../jsutils/invariant';
2+
3+
import { valueFromAST } from '../utilities/valueFromAST';
4+
5+
import type { GraphQLInputType, GraphQLDefaultValueUsage } from './definition';
6+
7+
/**
8+
* @internal
9+
*/
10+
export function getCoercedDefaultValue(
11+
usage: GraphQLDefaultValueUsage,
12+
type: GraphQLInputType,
13+
): mixed {
14+
if (usage.value !== undefined) {
15+
return usage.value;
16+
}
17+
// Memoize the result of coercing the default value in a hidden field.
18+
let coercedValue = (usage: any)._memoizedCoercedValue;
19+
// istanbul ignore else (memoized case)
20+
if (coercedValue === undefined) {
21+
coercedValue = valueFromAST(usage.literal, type);
22+
invariant(
23+
coercedValue !== undefined,
24+
'Literal cannot be converted to value for this type',
25+
);
26+
(usage: any)._memoizedCoercedValue = coercedValue;
27+
}
28+
return coercedValue;
29+
}

src/type/definition.d.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
FieldNode,
2424
FragmentDefinitionNode,
2525
ValueNode,
26+
ConstValueNode,
2627
ScalarTypeExtensionNode,
2728
UnionTypeExtensionNode,
2829
EnumTypeExtensionNode,
@@ -575,12 +576,17 @@ export interface GraphQLInputValue<Extensions> {
575576
name: string;
576577
description: Maybe<string>;
577578
type: GraphQLInputType;
578-
defaultValue: unknown;
579+
defaultValue: Maybe<GraphQLDefaultValueUsage>;
579580
deprecationReason: Maybe<string>;
580581
extensions: Maybe<Readonly<Extensions>>;
581582
astNode: Maybe<InputValueDefinitionNode>;
582583
}
583584

585+
export interface GraphQLDefaultValueUsage {
586+
value: unknown;
587+
literal: Maybe<ConstValueNode>;
588+
}
589+
584590
export interface GraphQLInputValueConfig<Extensions> {
585591
description?: Maybe<string>;
586592
type: GraphQLInputType;

src/type/definition.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type {
4141
FieldNode,
4242
FragmentDefinitionNode,
4343
ValueNode,
44+
ConstValueNode,
4445
} from '../language/ast';
4546

4647
import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
@@ -971,11 +972,22 @@ export function defineInputValue(
971972
!('resolve' in config),
972973
`${name} has a resolve property, but inputs cannot define resolvers.`,
973974
);
975+
let defaultValue;
976+
if (config.defaultValue !== undefined || config.defaultValueLiteral) {
977+
devAssert(
978+
config.defaultValue === undefined || !config.defaultValueLiteral,
979+
`${name} has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`,
980+
);
981+
defaultValue = {
982+
value: config.defaultValue,
983+
literal: config.defaultValueLiteral,
984+
};
985+
}
974986
return {
975987
name,
976988
description: config.description,
977989
type: config.type,
978-
defaultValue: config.defaultValue,
990+
defaultValue,
979991
deprecationReason: config.deprecationReason,
980992
extensions: config.extensions && toObjMap(config.extensions),
981993
astNode: config.astNode,
@@ -991,7 +1003,8 @@ export function inputValueToConfig(
9911003
return {
9921004
description: inputValue.description,
9931005
type: inputValue.type,
994-
defaultValue: inputValue.defaultValue,
1006+
defaultValue: inputValue.defaultValue?.value,
1007+
defaultValueLiteral: inputValue.defaultValue?.literal,
9951008
deprecationReason: inputValue.deprecationReason,
9961009
extensions: inputValue.extensions,
9971010
astNode: inputValue.astNode,
@@ -1002,16 +1015,22 @@ export type GraphQLInputValue = {|
10021015
name: string,
10031016
description: ?string,
10041017
type: GraphQLInputType,
1005-
defaultValue: mixed,
1018+
defaultValue: ?GraphQLDefaultValueUsage,
10061019
deprecationReason: ?string,
10071020
extensions: ?ReadOnlyObjMap<mixed>,
10081021
astNode: ?InputValueDefinitionNode,
10091022
|};
10101023

1024+
export type GraphQLDefaultValueUsage = {|
1025+
value: mixed,
1026+
literal: ?ConstValueNode,
1027+
|};
1028+
10111029
export type GraphQLInputValueConfig = {|
10121030
description?: ?string,
10131031
type: GraphQLInputType,
10141032
defaultValue?: mixed,
1033+
defaultValueLiteral?: ?ConstValueNode,
10151034
deprecationReason?: ?string,
10161035
extensions?: ?ReadOnlyObjMapLike<mixed>,
10171036
astNode?: ?InputValueDefinitionNode,

src/type/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export {
8282
GraphQLArgumentConfig,
8383
GraphQLArgumentExtensions,
8484
GraphQLInputValue,
85+
GraphQLDefaultValueUsage,
8586
GraphQLInputValueConfig,
8687
GraphQLEnumTypeConfig,
8788
GraphQLEnumTypeExtensions,

src/type/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export type {
136136
GraphQLArgument,
137137
GraphQLArgumentConfig,
138138
GraphQLInputValue,
139+
GraphQLDefaultValueUsage,
139140
GraphQLInputValueConfig,
140141
GraphQLEnumTypeConfig,
141142
GraphQLEnumValue,

src/type/introspection.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,15 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({
384384
'A GraphQL-formatted string representing the default value for this input value.',
385385
resolve(inputValue) {
386386
const { type, defaultValue } = inputValue;
387-
const valueAST = astFromValue(defaultValue, type);
388-
return valueAST ? print(valueAST) : null;
387+
if (!defaultValue) {
388+
return null;
389+
}
390+
let literal = defaultValue.literal;
391+
if (!literal) {
392+
literal = astFromValue(defaultValue.value, type);
393+
invariant(literal, 'Invalid default value');
394+
}
395+
return print(literal);
389396
},
390397
},
391398
isDeprecated: {

src/utilities/TypeInfo.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ export class TypeInfo {
209209
}
210210
}
211211
this._argument = argDef;
212-
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
212+
this._defaultValueStack.push(argDef?.defaultValue?.value);
213213
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
214214
break;
215215
}
@@ -233,9 +233,7 @@ export class TypeInfo {
233233
inputFieldType = inputField.type;
234234
}
235235
}
236-
this._defaultValueStack.push(
237-
inputField ? inputField.defaultValue : undefined,
238-
);
236+
this._defaultValueStack.push(inputField?.defaultValue?.value);
239237
this._inputTypeStack.push(
240238
isInputType(inputFieldType) ? inputFieldType : undefined,
241239
);

src/utilities/__tests__/astFromValue-test.js

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ describe('astFromValue', () => {
5151
kind: 'BooleanValue',
5252
value: false,
5353
});
54+
55+
expect(astFromValue(null, NonNullBoolean)).to.equal(null);
5456
});
5557

5658
it('converts Int values to Int ASTs', () => {

0 commit comments

Comments
 (0)