Skip to content

Commit

Permalink
Preserve defaultValue literals
Browse files Browse the repository at this point in the history
Fixes #3051
  • Loading branch information
leebyron committed May 13, 2021
1 parent 9830fda commit 05db9bd
Show file tree
Hide file tree
Showing 23 changed files with 259 additions and 64 deletions.
15 changes: 11 additions & 4 deletions src/execution/values.js
Expand Up @@ -17,6 +17,7 @@ import type { GraphQLSchema } from '../type/schema';
import type { GraphQLField } from '../type/definition';
import type { GraphQLDirective } from '../type/directives';
import { isInputType, isNonNullType } from '../type/definition';
import { getCoercedDefaultValue } from '../type/defaultValues';

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

if (!argumentNode) {
if (argDef.defaultValue !== undefined) {
coercedValues[name] = argDef.defaultValue;
if (argDef.defaultValue) {
coercedValues[name] = getCoercedDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument "${name}" of required type "${inspect(argType)}" ` +
Expand All @@ -194,8 +198,11 @@ export function getArgumentValues(
variableValues == null ||
!hasOwnProperty(variableValues, variableName)
) {
if (argDef.defaultValue !== undefined) {
coercedValues[name] = argDef.defaultValue;
if (argDef.defaultValue) {
coercedValues[name] = getCoercedDefaultValue(
argDef.defaultValue,
argDef.type,
);
} else if (isNonNullType(argType)) {
throw new GraphQLError(
`Argument "${name}" of required type "${inspect(argType)}" ` +
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Expand Up @@ -150,6 +150,7 @@ export {
GraphQLArgumentConfig,
GraphQLArgumentExtensions,
GraphQLInputValue,
GraphQLDefaultValueUsage,
GraphQLInputValueConfig,
GraphQLEnumTypeConfig,
GraphQLEnumTypeExtensions,
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -146,6 +146,7 @@ export type {
GraphQLArgument,
GraphQLArgumentConfig,
GraphQLInputValue,
GraphQLDefaultValueUsage,
GraphQLInputValueConfig,
GraphQLEnumTypeConfig,
GraphQLEnumValue,
Expand Down
67 changes: 67 additions & 0 deletions src/type/__tests__/definition-test.js
Expand Up @@ -818,6 +818,73 @@ describe('Type System: Input Objects', () => {
);
});
});

describe('Input Object fields may have default values', () => {
it('accepts an Input Object type with a default value', () => {
const inputObjType = new GraphQLInputObjectType({
name: 'SomeInputObject',
fields: {
f: { type: ScalarType, defaultValue: 3 },
},
});
expect(inputObjType.getFields()).to.deep.equal({
f: {
name: 'f',
description: undefined,
type: ScalarType,
defaultValue: {
value: 3,
literal: undefined,
},
deprecationReason: undefined,
extensions: undefined,
astNode: undefined,
},
});
});

it('accepts an Input Object type with a default value literal', () => {
const inputObjType = new GraphQLInputObjectType({
name: 'SomeInputObject',
fields: {
f: {
type: ScalarType,
defaultValueLiteral: { kind: 'IntValue', value: '3' },
},
},
});
expect(inputObjType.getFields()).to.deep.equal({
f: {
name: 'f',
description: undefined,
type: ScalarType,
defaultValue: {
value: undefined,
literal: { kind: 'IntValue', value: '3' },
},
deprecationReason: undefined,
extensions: undefined,
astNode: undefined,
},
});
});

it('rejects an Input Object type with potentially conflicting default values', () => {
const inputObjType = new GraphQLInputObjectType({
name: 'SomeInputObject',
fields: {
f: {
type: ScalarType,
defaultValue: 3,
defaultValueLiteral: { kind: 'IntValue', value: '3' },
},
},
});
expect(() => inputObjType.getFields()).to.throw(
'f has both a defaultValue and a defaultValueLiteral property, but only one must be provided.',
);
});
});
});

describe('Type System: List', () => {
Expand Down
31 changes: 11 additions & 20 deletions src/type/__tests__/predicate-test.js
Expand Up @@ -69,6 +69,7 @@ import {
assertNamedType,
getNullableType,
getNamedType,
defineInputValue,
} from '../definition';

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

describe('isRequiredInput', () => {
function buildArg(config: {|
function buildArg({
type,
defaultValue,
}: {|
type: GraphQLInputType,
defaultValue?: mixed,
|}): GraphQLArgument {
return {
name: 'someArg',
type: config.type,
description: undefined,
defaultValue: config.defaultValue,
deprecationReason: null,
extensions: undefined,
astNode: undefined,
};
return defineInputValue({ type, defaultValue }, 'someArg');
}

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

function buildInputField(config: {|
function buildInputField({
type,
defaultValue,
}: {|
type: GraphQLInputType,
defaultValue?: mixed,
|}): GraphQLInputField {
return {
name: 'someInputField',
type: config.type,
description: undefined,
defaultValue: config.defaultValue,
deprecationReason: null,
extensions: undefined,
astNode: undefined,
};
return defineInputValue({ type, defaultValue }, 'someInputField');
}

it('returns true for required input field', () => {
Expand Down
9 changes: 9 additions & 0 deletions src/type/defaultValues.d.ts
@@ -0,0 +1,9 @@
import { GraphQLInputType, GraphQLDefaultValueUsage } from './definition';

/**
* @internal
*/
export function getCoercedDefaultValue(
usage: GraphQLDefaultValueUsage,
type: GraphQLInputType,
): unknown;
29 changes: 29 additions & 0 deletions src/type/defaultValues.js
@@ -0,0 +1,29 @@
import { invariant } from '../jsutils/invariant';

import { valueFromAST } from '../utilities/valueFromAST';

import type { GraphQLInputType, GraphQLDefaultValueUsage } from './definition';

/**
* @internal
*/
export function getCoercedDefaultValue(
usage: GraphQLDefaultValueUsage,
type: GraphQLInputType,
): mixed {
if (usage.value !== undefined) {
return usage.value;
}
// Memoize the result of coercing the default value in a hidden field.
let coercedValue = (usage: any)._memoizedCoercedValue;
// istanbul ignore else (memoized case)
if (coercedValue === undefined) {
coercedValue = valueFromAST(usage.literal, type);
invariant(
coercedValue !== undefined,
'Literal cannot be converted to value for this type',
);
(usage: any)._memoizedCoercedValue = coercedValue;
}
return coercedValue;
}
8 changes: 7 additions & 1 deletion src/type/definition.d.ts
Expand Up @@ -23,6 +23,7 @@ import {
FieldNode,
FragmentDefinitionNode,
ValueNode,
ConstValueNode,
ScalarTypeExtensionNode,
UnionTypeExtensionNode,
EnumTypeExtensionNode,
Expand Down Expand Up @@ -575,12 +576,17 @@ export interface GraphQLInputValue<Extensions> {
name: string;
description: Maybe<string>;
type: GraphQLInputType;
defaultValue: unknown;
defaultValue: Maybe<GraphQLDefaultValueUsage>;
deprecationReason: Maybe<string>;
extensions: Maybe<Readonly<Extensions>>;
astNode: Maybe<InputValueDefinitionNode>;
}

export interface GraphQLDefaultValueUsage {
value: unknown;
literal: Maybe<ConstValueNode>;
}

export interface GraphQLInputValueConfig<Extensions> {
description?: Maybe<string>;
type: GraphQLInputType;
Expand Down
25 changes: 22 additions & 3 deletions src/type/definition.js
Expand Up @@ -41,6 +41,7 @@ import type {
FieldNode,
FragmentDefinitionNode,
ValueNode,
ConstValueNode,
} from '../language/ast';

import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
Expand Down Expand Up @@ -971,11 +972,22 @@ export function defineInputValue(
!('resolve' in config),
`${name} has a resolve property, but inputs cannot define resolvers.`,
);
let defaultValue;
if (config.defaultValue !== undefined || config.defaultValueLiteral) {
devAssert(
config.defaultValue === undefined || !config.defaultValueLiteral,
`${name} has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`,
);
defaultValue = {
value: config.defaultValue,
literal: config.defaultValueLiteral,
};
}
return {
name,
description: config.description,
type: config.type,
defaultValue: config.defaultValue,
defaultValue,
deprecationReason: config.deprecationReason,
extensions: config.extensions && toObjMap(config.extensions),
astNode: config.astNode,
Expand All @@ -991,7 +1003,8 @@ export function inputValueToConfig(
return {
description: inputValue.description,
type: inputValue.type,
defaultValue: inputValue.defaultValue,
defaultValue: inputValue.defaultValue?.value,
defaultValueLiteral: inputValue.defaultValue?.literal,
deprecationReason: inputValue.deprecationReason,
extensions: inputValue.extensions,
astNode: inputValue.astNode,
Expand All @@ -1002,16 +1015,22 @@ export type GraphQLInputValue = {|
name: string,
description: ?string,
type: GraphQLInputType,
defaultValue: mixed,
defaultValue: ?GraphQLDefaultValueUsage,
deprecationReason: ?string,
extensions: ?ReadOnlyObjMap<mixed>,
astNode: ?InputValueDefinitionNode,
|};

export type GraphQLDefaultValueUsage = {|
value: mixed,
literal: ?ConstValueNode,
|};

export type GraphQLInputValueConfig = {|
description?: ?string,
type: GraphQLInputType,
defaultValue?: mixed,
defaultValueLiteral?: ?ConstValueNode,
deprecationReason?: ?string,
extensions?: ?ReadOnlyObjMapLike<mixed>,
astNode?: ?InputValueDefinitionNode,
Expand Down
1 change: 1 addition & 0 deletions src/type/index.d.ts
Expand Up @@ -82,6 +82,7 @@ export {
GraphQLArgumentConfig,
GraphQLArgumentExtensions,
GraphQLInputValue,
GraphQLDefaultValueUsage,
GraphQLInputValueConfig,
GraphQLEnumTypeConfig,
GraphQLEnumTypeExtensions,
Expand Down
1 change: 1 addition & 0 deletions src/type/index.js
Expand Up @@ -136,6 +136,7 @@ export type {
GraphQLArgument,
GraphQLArgumentConfig,
GraphQLInputValue,
GraphQLDefaultValueUsage,
GraphQLInputValueConfig,
GraphQLEnumTypeConfig,
GraphQLEnumValue,
Expand Down
11 changes: 9 additions & 2 deletions src/type/introspection.js
Expand Up @@ -384,8 +384,15 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({
'A GraphQL-formatted string representing the default value for this input value.',
resolve(inputValue) {
const { type, defaultValue } = inputValue;
const valueAST = astFromValue(defaultValue, type);
return valueAST ? print(valueAST) : null;
if (!defaultValue) {
return null;
}
let literal = defaultValue.literal;
if (!literal) {
literal = astFromValue(defaultValue.value, type);
invariant(literal, 'Invalid default value');
}
return print(literal);
},
},
isDeprecated: {
Expand Down
6 changes: 2 additions & 4 deletions src/utilities/TypeInfo.js
Expand Up @@ -209,7 +209,7 @@ export class TypeInfo {
}
}
this._argument = argDef;
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
this._defaultValueStack.push(argDef?.defaultValue?.value);
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
break;
}
Expand All @@ -233,9 +233,7 @@ export class TypeInfo {
inputFieldType = inputField.type;
}
}
this._defaultValueStack.push(
inputField ? inputField.defaultValue : undefined,
);
this._defaultValueStack.push(inputField?.defaultValue?.value);
this._inputTypeStack.push(
isInputType(inputFieldType) ? inputFieldType : undefined,
);
Expand Down
2 changes: 2 additions & 0 deletions src/utilities/__tests__/astFromValue-test.js
Expand Up @@ -51,6 +51,8 @@ describe('astFromValue', () => {
kind: 'BooleanValue',
value: false,
});

expect(astFromValue(null, NonNullBoolean)).to.equal(null);
});

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

0 comments on commit 05db9bd

Please sign in to comment.