From 83751a9f638470211c7627e715ea9a77e4ee71a4 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 6 May 2021 17:55:52 +0300 Subject: [PATCH 01/16] mapAsyncIterator: refactor async iterator (#3062) --- src/subscription/mapAsyncIterator.js | 52 +++++++++++++--------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/subscription/mapAsyncIterator.js b/src/subscription/mapAsyncIterator.js index 65eeffad5a1..c885450695b 100644 --- a/src/subscription/mapAsyncIterator.js +++ b/src/subscription/mapAsyncIterator.js @@ -11,45 +11,41 @@ export function mapAsyncIterator( // $FlowIssue[incompatible-use] const iterator = iterable[Symbol.asyncIterator](); - async function abruptClose(error: mixed) { - if (typeof iterator.return === 'function') { - try { - await iterator.return(); - } catch (_e) { - /* ignore error */ - } + async function mapResult( + result: IteratorResult, + ): Promise> { + if (result.done) { + return result; } - throw error; - } - async function mapResult(resultPromise: Promise>) { try { - const result = await resultPromise; - - if (result.done) { - return result; - } - return { value: await callback(result.value), done: false }; - } catch (callbackError) { - return abruptClose(callbackError); + } catch (error) { + // istanbul ignore else (FIXME: add test case) + if (typeof iterator.return === 'function') { + try { + await iterator.return(); + } catch (_e) { + /* ignore error */ + } + } + throw error; } } return { - next(): Promise> { - return mapResult(iterator.next()); + async next() { + return mapResult(await iterator.next()); }, - return(): Promise> { + async return(): Promise> { return typeof iterator.return === 'function' - ? mapResult(iterator.return()) - : Promise.resolve({ value: undefined, done: true }); + ? mapResult(await iterator.return()) + : { value: undefined, done: true }; }, - throw(error?: mixed): Promise> { - if (typeof iterator.throw === 'function') { - return mapResult(iterator.throw(error)); - } - return Promise.reject(error).catch(abruptClose); + async throw(error?: mixed) { + return typeof iterator.throw === 'function' + ? mapResult(await iterator.throw(error)) + : Promise.reject(error); }, [Symbol.asyncIterator]() { return this; From 960f207390615588111a4cf0238e62403c9c7eed Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 6 May 2021 19:56:35 +0300 Subject: [PATCH 02/16] mapAsyncIterator: refactor async iterator (#3064) --- src/subscription/mapAsyncIterator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subscription/mapAsyncIterator.js b/src/subscription/mapAsyncIterator.js index c885450695b..30647c30c5f 100644 --- a/src/subscription/mapAsyncIterator.js +++ b/src/subscription/mapAsyncIterator.js @@ -37,7 +37,7 @@ export function mapAsyncIterator( async next() { return mapResult(await iterator.next()); }, - async return(): Promise> { + async return(): Promise> { return typeof iterator.return === 'function' ? mapResult(await iterator.return()) : { value: undefined, done: true }; From 5579180f4a659f9b6fc3083869b765c6630bcc50 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Sat, 8 May 2021 09:14:22 +0300 Subject: [PATCH 03/16] mapAsyncIterator-test: check that return value is passed on early return (#3066) --- .../__tests__/mapAsyncIterator-test.js | 15 ++++++++++----- src/subscription/mapAsyncIterator.js | 12 ++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/subscription/__tests__/mapAsyncIterator-test.js b/src/subscription/__tests__/mapAsyncIterator-test.js index a0540ab3dd1..10e6edffba8 100644 --- a/src/subscription/__tests__/mapAsyncIterator-test.js +++ b/src/subscription/__tests__/mapAsyncIterator-test.js @@ -90,11 +90,16 @@ describe('mapAsyncIterator', () => { it('allows returning early from mapped async generator', async () => { async function* source() { - yield 1; - yield 2; + try { + yield 1; + yield 2; - // istanbul ignore next (Shouldn't be reached) - yield 3; + // istanbul ignore next (Shouldn't be reached) + yield 3; + } finally { + // eslint-disable-next-line no-unsafe-finally + return 'The End'; + } } const doubles = mapAsyncIterator(source(), (x) => x + x); @@ -104,7 +109,7 @@ describe('mapAsyncIterator', () => { // Early return expect(await doubles.return()).to.deep.equal({ - value: undefined, + value: 'The End', done: true, }); diff --git a/src/subscription/mapAsyncIterator.js b/src/subscription/mapAsyncIterator.js index 30647c30c5f..2b447818964 100644 --- a/src/subscription/mapAsyncIterator.js +++ b/src/subscription/mapAsyncIterator.js @@ -4,16 +4,16 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; * Given an AsyncIterable and a callback function, return an AsyncIterator * which produces values mapped via calling the callback function. */ -export function mapAsyncIterator( - iterable: AsyncIterable | AsyncGenerator, +export function mapAsyncIterator( + iterable: AsyncGenerator | AsyncIterable, callback: (T) => PromiseOrValue, -): AsyncGenerator { +): AsyncGenerator { // $FlowIssue[incompatible-use] const iterator = iterable[Symbol.asyncIterator](); async function mapResult( - result: IteratorResult, - ): Promise> { + result: IteratorResult, + ): Promise> { if (result.done) { return result; } @@ -37,7 +37,7 @@ export function mapAsyncIterator( async next() { return mapResult(await iterator.next()); }, - async return(): Promise> { + async return(): Promise> { return typeof iterator.return === 'function' ? mapResult(await iterator.return()) : { value: undefined, done: true }; From 513eacc33ed29e389fa6790e3dba00dbc5f9aa83 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 7 May 2021 23:22:09 -0700 Subject: [PATCH 04/16] Refine parse and AST to represent ConstValue (#3059) This adds: * ConstValueNode - A subtype of ValueNode which recursively excludes Variables * Improved syntax error when encountering variable in a const value * parseConstValue(): ConstValueNode * isConstValue(): ConstValueNode * Various refinements to AST types to use ConstValueNode (or a type which includes it) to better align to the spec. --- src/index.d.ts | 8 ++ src/index.js | 8 ++ src/language/__tests__/parser-test.js | 92 +++++++++++++++++++++- src/language/__tests__/predicates-test.js | 13 ++++ src/language/ast.d.ts | 83 +++++++++++++++----- src/language/ast.js | 83 +++++++++++++++----- src/language/index.d.ts | 15 +++- src/language/index.js | 9 ++- src/language/parser.d.ts | 27 ++++++- src/language/parser.js | 94 +++++++++++++++-------- src/language/predicates.d.ts | 3 + src/language/predicates.js | 11 +++ 12 files changed, 369 insertions(+), 77 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 78afd0ed116..ac935945c5c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -200,6 +200,7 @@ export { // Parse parse, parseValue, + parseConstValue, parseType, // Print print, @@ -215,6 +216,7 @@ export { isExecutableDefinitionNode, isSelectionNode, isValueNode, + isConstValueNode, isTypeNode, isTypeSystemDefinitionNode, isTypeDefinitionNode, @@ -247,10 +249,12 @@ export { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, IntValueNode, FloatValueNode, StringValueNode, @@ -258,9 +262,13 @@ export { NullValueNode, EnumValueNode, ListValueNode, + ConstListValueNode, ObjectValueNode, + ConstObjectValueNode, ObjectFieldNode, + ConstObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, ListTypeNode, diff --git a/src/index.js b/src/index.js index 30d99233d06..1e6b3f247ad 100644 --- a/src/index.js +++ b/src/index.js @@ -187,6 +187,7 @@ export { // Parse parse, parseValue, + parseConstValue, parseType, // Print print, @@ -202,6 +203,7 @@ export { isExecutableDefinitionNode, isSelectionNode, isValueNode, + isConstValueNode, isTypeNode, isTypeSystemDefinitionNode, isTypeDefinitionNode, @@ -234,10 +236,12 @@ export type { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, IntValueNode, FloatValueNode, StringValueNode, @@ -245,9 +249,13 @@ export type { NullValueNode, EnumValueNode, ListValueNode, + ConstListValueNode, ObjectValueNode, + ConstObjectValueNode, ObjectFieldNode, + ConstObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, ListTypeNode, diff --git a/src/language/__tests__/parser-test.js b/src/language/__tests__/parser-test.js index 661378b1d76..d042bec2914 100644 --- a/src/language/__tests__/parser-test.js +++ b/src/language/__tests__/parser-test.js @@ -9,7 +9,7 @@ import { inspect } from '../../jsutils/inspect'; import { Kind } from '../kinds'; import { Source } from '../source'; import { TokenKind } from '../tokenKind'; -import { parse, parseValue, parseType } from '../parser'; +import { parse, parseValue, parseConstValue, parseType } from '../parser'; import { toJSONDeep } from './toJSONDeep'; @@ -95,7 +95,7 @@ describe('Parser', () => { expectSyntaxError( 'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }', ).to.deep.equal({ - message: 'Syntax Error: Unexpected "$".', + message: 'Syntax Error: Unexpected variable "$var" in constant value.', locations: [{ line: 1, column: 37 }], }); }); @@ -447,6 +447,94 @@ describe('Parser', () => { ], }); }); + + it('allows variables', () => { + const result = parseValue('{ field: $var }'); + expect(toJSONDeep(result)).to.deep.equal({ + kind: Kind.OBJECT, + loc: { start: 0, end: 15 }, + fields: [ + { + kind: Kind.OBJECT_FIELD, + loc: { start: 2, end: 13 }, + name: { + kind: Kind.NAME, + loc: { start: 2, end: 7 }, + value: 'field', + }, + value: { + kind: Kind.VARIABLE, + loc: { start: 9, end: 13 }, + name: { + kind: Kind.NAME, + loc: { start: 10, end: 13 }, + value: 'var', + }, + }, + }, + ], + }); + }); + + it('correct message for incomplete variable', () => { + expect(() => parseValue('$')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Expected Name, found .', + locations: [{ line: 1, column: 2 }], + }); + }); + + it('correct message for unexpected token', () => { + expect(() => parseValue(':')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Unexpected ":".', + locations: [{ line: 1, column: 1 }], + }); + }); + }); + + describe('parseConstValue', () => { + it('parses values', () => { + const result = parseConstValue('[123 "abc"]'); + expect(toJSONDeep(result)).to.deep.equal({ + kind: Kind.LIST, + loc: { start: 0, end: 11 }, + values: [ + { + kind: Kind.INT, + loc: { start: 1, end: 4 }, + value: '123', + }, + { + kind: Kind.STRING, + loc: { start: 5, end: 10 }, + value: 'abc', + block: false, + }, + ], + }); + }); + + it('does not allow variables', () => { + expect(() => parseConstValue('{ field: $var }')) + .to.throw() + .to.deep.include({ + message: + 'Syntax Error: Unexpected variable "$var" in constant value.', + locations: [{ line: 1, column: 10 }], + }); + }); + + it('correct message for unexpected token', () => { + expect(() => parseConstValue('$')) + .to.throw() + .to.deep.include({ + message: 'Syntax Error: Unexpected "$".', + locations: [{ line: 1, column: 1 }], + }); + }); }); describe('parseType', () => { diff --git a/src/language/__tests__/predicates-test.js b/src/language/__tests__/predicates-test.js index eb620abd614..4a9f03571e9 100644 --- a/src/language/__tests__/predicates-test.js +++ b/src/language/__tests__/predicates-test.js @@ -3,11 +3,13 @@ import { describe, it } from 'mocha'; import type { ASTNode } from '../ast'; import { Kind } from '../kinds'; +import { parseValue } from '../parser'; import { isDefinitionNode, isExecutableDefinitionNode, isSelectionNode, isValueNode, + isConstValueNode, isTypeNode, isTypeSystemDefinitionNode, isTypeDefinitionNode, @@ -75,6 +77,17 @@ describe('AST node predicates', () => { ]); }); + it('isConstValueNode', () => { + expect(isConstValueNode(parseValue('"value"'))).to.equal(true); + expect(isConstValueNode(parseValue('$var'))).to.equal(false); + + expect(isConstValueNode(parseValue('{ field: "value" }'))).to.equal(true); + expect(isConstValueNode(parseValue('{ field: $var }'))).to.equal(false); + + expect(isConstValueNode(parseValue('[ "value" ]'))).to.equal(true); + expect(isConstValueNode(parseValue('[ $var ]'))).to.equal(false); + }); + it('isTypeNode', () => { expect(filterNodes(isTypeNode)).to.deep.equal([ 'NamedType', diff --git a/src/language/ast.d.ts b/src/language/ast.d.ts index 20ec92f8052..2e2850a423b 100644 --- a/src/language/ast.d.ts +++ b/src/language/ast.d.ts @@ -241,8 +241,8 @@ export interface VariableDefinitionNode { readonly loc?: Location; readonly variable: VariableNode; readonly type: TypeNode; - readonly defaultValue?: ValueNode; - readonly directives?: ReadonlyArray; + readonly defaultValue?: ConstValueNode; + readonly directives?: ReadonlyArray; } export interface VariableNode { @@ -276,6 +276,13 @@ export interface ArgumentNode { readonly value: ValueNode; } +export interface ConstArgumentNode { + readonly kind: 'Argument'; + readonly loc?: Location; + readonly name: NameNode; + readonly value: ConstValueNode; +} + // Fragments export interface FragmentSpreadNode { @@ -317,6 +324,16 @@ export type ValueNode = | ListValueNode | ObjectValueNode; +export type ConstValueNode = + | IntValueNode + | FloatValueNode + | StringValueNode + | BooleanValueNode + | NullValueNode + | EnumValueNode + | ConstListValueNode + | ConstObjectValueNode; + export interface IntValueNode { readonly kind: 'IntValue'; readonly loc?: Location; @@ -359,12 +376,24 @@ export interface ListValueNode { readonly values: ReadonlyArray; } +export interface ConstListValueNode { + readonly kind: 'ListValue'; + readonly loc?: Location; + readonly values: ReadonlyArray; +} + export interface ObjectValueNode { readonly kind: 'ObjectValue'; readonly loc?: Location; readonly fields: ReadonlyArray; } +export interface ConstObjectValueNode { + readonly kind: 'ObjectValue'; + readonly loc?: Location; + readonly fields: ReadonlyArray; +} + export interface ObjectFieldNode { readonly kind: 'ObjectField'; readonly loc?: Location; @@ -372,6 +401,13 @@ export interface ObjectFieldNode { readonly value: ValueNode; } +export interface ConstObjectFieldNode { + readonly kind: 'ObjectField'; + readonly loc?: Location; + readonly name: NameNode; + readonly value: ConstValueNode; +} + // Directives export interface DirectiveNode { @@ -381,6 +417,13 @@ export interface DirectiveNode { readonly arguments?: ReadonlyArray; } +export interface ConstDirectiveNode { + readonly kind: 'Directive'; + readonly loc?: Location; + readonly name: NameNode; + readonly arguments?: ReadonlyArray; +} + // Type Reference export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; @@ -414,7 +457,7 @@ export interface SchemaDefinitionNode { readonly kind: 'SchemaDefinition'; readonly loc?: Location; readonly description?: StringValueNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly operationTypes: ReadonlyArray; } @@ -440,7 +483,7 @@ export interface ScalarTypeDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; } export interface ObjectTypeDefinitionNode { @@ -449,7 +492,7 @@ export interface ObjectTypeDefinitionNode { readonly description?: StringValueNode; readonly name: NameNode; readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -460,7 +503,7 @@ export interface FieldDefinitionNode { readonly name: NameNode; readonly arguments?: ReadonlyArray; readonly type: TypeNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; } export interface InputValueDefinitionNode { @@ -469,8 +512,8 @@ export interface InputValueDefinitionNode { readonly description?: StringValueNode; readonly name: NameNode; readonly type: TypeNode; - readonly defaultValue?: ValueNode; - readonly directives?: ReadonlyArray; + readonly defaultValue?: ConstValueNode; + readonly directives?: ReadonlyArray; } export interface InterfaceTypeDefinitionNode { @@ -479,7 +522,7 @@ export interface InterfaceTypeDefinitionNode { readonly description?: StringValueNode; readonly name: NameNode; readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -488,7 +531,7 @@ export interface UnionTypeDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly types?: ReadonlyArray; } @@ -497,7 +540,7 @@ export interface EnumTypeDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly values?: ReadonlyArray; } @@ -506,7 +549,7 @@ export interface EnumValueDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; } export interface InputObjectTypeDefinitionNode { @@ -514,7 +557,7 @@ export interface InputObjectTypeDefinitionNode { readonly loc?: Location; readonly description?: StringValueNode; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -537,7 +580,7 @@ export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; export interface SchemaExtensionNode { readonly kind: 'SchemaExtension'; readonly loc?: Location; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly operationTypes?: ReadonlyArray; } @@ -555,7 +598,7 @@ export interface ScalarTypeExtensionNode { readonly kind: 'ScalarTypeExtension'; readonly loc?: Location; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; } export interface ObjectTypeExtensionNode { @@ -563,7 +606,7 @@ export interface ObjectTypeExtensionNode { readonly loc?: Location; readonly name: NameNode; readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -572,7 +615,7 @@ export interface InterfaceTypeExtensionNode { readonly loc?: Location; readonly name: NameNode; readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } @@ -580,7 +623,7 @@ export interface UnionTypeExtensionNode { readonly kind: 'UnionTypeExtension'; readonly loc?: Location; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly types?: ReadonlyArray; } @@ -588,7 +631,7 @@ export interface EnumTypeExtensionNode { readonly kind: 'EnumTypeExtension'; readonly loc?: Location; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly values?: ReadonlyArray; } @@ -596,6 +639,6 @@ export interface InputObjectTypeExtensionNode { readonly kind: 'InputObjectTypeExtension'; readonly loc?: Location; readonly name: NameNode; - readonly directives?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly fields?: ReadonlyArray; } diff --git a/src/language/ast.js b/src/language/ast.js index c0a9a615e7e..f4de32054db 100644 --- a/src/language/ast.js +++ b/src/language/ast.js @@ -267,8 +267,8 @@ export type VariableDefinitionNode = {| +loc?: Location, +variable: VariableNode, +type: TypeNode, - +defaultValue?: ValueNode, - +directives?: $ReadOnlyArray, + +defaultValue?: ConstValueNode, + +directives?: $ReadOnlyArray, |}; export type VariableNode = {| @@ -302,6 +302,13 @@ export type ArgumentNode = {| +value: ValueNode, |}; +export type ConstArgumentNode = {| + +kind: 'Argument', + +loc?: Location, + +name: NameNode, + +value: ConstValueNode, +|}; + // Fragments export type FragmentSpreadNode = {| @@ -343,6 +350,16 @@ export type ValueNode = | ListValueNode | ObjectValueNode; +export type ConstValueNode = + | IntValueNode + | FloatValueNode + | StringValueNode + | BooleanValueNode + | NullValueNode + | EnumValueNode + | ConstListValueNode + | ConstObjectValueNode; + export type IntValueNode = {| +kind: 'IntValue', +loc?: Location, @@ -385,12 +402,24 @@ export type ListValueNode = {| +values: $ReadOnlyArray, |}; +export type ConstListValueNode = {| + +kind: 'ListValue', + +loc?: Location, + +values: $ReadOnlyArray, +|}; + export type ObjectValueNode = {| +kind: 'ObjectValue', +loc?: Location, +fields: $ReadOnlyArray, |}; +export type ConstObjectValueNode = {| + +kind: 'ObjectValue', + +loc?: Location, + +fields: $ReadOnlyArray, +|}; + export type ObjectFieldNode = {| +kind: 'ObjectField', +loc?: Location, @@ -398,6 +427,13 @@ export type ObjectFieldNode = {| +value: ValueNode, |}; +export type ConstObjectFieldNode = {| + +kind: 'ObjectField', + +loc?: Location, + +name: NameNode, + +value: ConstValueNode, +|}; + // Directives export type DirectiveNode = {| @@ -407,6 +443,13 @@ export type DirectiveNode = {| +arguments?: $ReadOnlyArray, |}; +export type ConstDirectiveNode = {| + +kind: 'Directive', + +loc?: Location, + +name: NameNode, + +arguments?: $ReadOnlyArray, +|}; + // Type Reference export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; @@ -440,7 +483,7 @@ export type SchemaDefinitionNode = {| +kind: 'SchemaDefinition', +loc?: Location, +description?: StringValueNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +operationTypes: $ReadOnlyArray, |}; @@ -466,7 +509,7 @@ export type ScalarTypeDefinitionNode = {| +loc?: Location, +description?: StringValueNode, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, |}; export type ObjectTypeDefinitionNode = {| @@ -475,7 +518,7 @@ export type ObjectTypeDefinitionNode = {| +description?: StringValueNode, +name: NameNode, +interfaces?: $ReadOnlyArray, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; @@ -486,7 +529,7 @@ export type FieldDefinitionNode = {| +name: NameNode, +arguments?: $ReadOnlyArray, +type: TypeNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, |}; export type InputValueDefinitionNode = {| @@ -495,8 +538,8 @@ export type InputValueDefinitionNode = {| +description?: StringValueNode, +name: NameNode, +type: TypeNode, - +defaultValue?: ValueNode, - +directives?: $ReadOnlyArray, + +defaultValue?: ConstValueNode, + +directives?: $ReadOnlyArray, |}; export type InterfaceTypeDefinitionNode = {| @@ -505,7 +548,7 @@ export type InterfaceTypeDefinitionNode = {| +description?: StringValueNode, +name: NameNode, +interfaces?: $ReadOnlyArray, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; @@ -514,7 +557,7 @@ export type UnionTypeDefinitionNode = {| +loc?: Location, +description?: StringValueNode, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +types?: $ReadOnlyArray, |}; @@ -523,7 +566,7 @@ export type EnumTypeDefinitionNode = {| +loc?: Location, +description?: StringValueNode, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +values?: $ReadOnlyArray, |}; @@ -532,7 +575,7 @@ export type EnumValueDefinitionNode = {| +loc?: Location, +description?: StringValueNode, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, |}; export type InputObjectTypeDefinitionNode = {| @@ -540,7 +583,7 @@ export type InputObjectTypeDefinitionNode = {| +loc?: Location, +description?: StringValueNode, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; @@ -563,7 +606,7 @@ export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; export type SchemaExtensionNode = {| +kind: 'SchemaExtension', +loc?: Location, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +operationTypes?: $ReadOnlyArray, |}; @@ -581,7 +624,7 @@ export type ScalarTypeExtensionNode = {| +kind: 'ScalarTypeExtension', +loc?: Location, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, |}; export type ObjectTypeExtensionNode = {| @@ -589,7 +632,7 @@ export type ObjectTypeExtensionNode = {| +loc?: Location, +name: NameNode, +interfaces?: $ReadOnlyArray, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; @@ -598,7 +641,7 @@ export type InterfaceTypeExtensionNode = {| +loc?: Location, +name: NameNode, +interfaces?: $ReadOnlyArray, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; @@ -606,7 +649,7 @@ export type UnionTypeExtensionNode = {| +kind: 'UnionTypeExtension', +loc?: Location, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +types?: $ReadOnlyArray, |}; @@ -614,7 +657,7 @@ export type EnumTypeExtensionNode = {| +kind: 'EnumTypeExtension', +loc?: Location, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +values?: $ReadOnlyArray, |}; @@ -622,6 +665,6 @@ export type InputObjectTypeExtensionNode = {| +kind: 'InputObjectTypeExtension', +loc?: Location, +name: NameNode, - +directives?: $ReadOnlyArray, + +directives?: $ReadOnlyArray, +fields?: $ReadOnlyArray, |}; diff --git a/src/language/index.d.ts b/src/language/index.d.ts index a5b1157b24a..47e4ecfef79 100644 --- a/src/language/index.d.ts +++ b/src/language/index.d.ts @@ -6,7 +6,13 @@ export { printLocation, printSourceLocation } from './printLocation'; export { Kind, KindEnum } from './kinds'; export { TokenKind, TokenKindEnum } from './tokenKind'; export { Lexer } from './lexer'; -export { parse, parseValue, parseType, ParseOptions } from './parser'; +export { + parse, + parseValue, + parseConstValue, + parseType, + ParseOptions, +} from './parser'; export { print } from './printer'; export { visit, @@ -35,10 +41,12 @@ export { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, IntValueNode, FloatValueNode, StringValueNode, @@ -46,9 +54,13 @@ export { NullValueNode, EnumValueNode, ListValueNode, + ConstListValueNode, ObjectValueNode, + ConstObjectValueNode, ObjectFieldNode, + ConstObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, ListTypeNode, @@ -83,6 +95,7 @@ export { isExecutableDefinitionNode, isSelectionNode, isValueNode, + isConstValueNode, isTypeNode, isTypeSystemDefinitionNode, isTypeDefinitionNode, diff --git a/src/language/index.js b/src/language/index.js index 71cb5f5c226..447c2d0e24a 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -13,7 +13,7 @@ export type { TokenKindEnum } from './tokenKind'; export { Lexer } from './lexer'; -export { parse, parseValue, parseType } from './parser'; +export { parse, parseValue, parseConstValue, parseType } from './parser'; export type { ParseOptions } from './parser'; export { print } from './printer'; @@ -38,10 +38,12 @@ export type { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, IntValueNode, FloatValueNode, StringValueNode, @@ -49,9 +51,13 @@ export type { NullValueNode, EnumValueNode, ListValueNode, + ConstListValueNode, ObjectValueNode, + ConstObjectValueNode, ObjectFieldNode, + ConstObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, ListTypeNode, @@ -86,6 +92,7 @@ export { isExecutableDefinitionNode, isSelectionNode, isValueNode, + isConstValueNode, isTypeNode, isTypeSystemDefinitionNode, isTypeDefinitionNode, diff --git a/src/language/parser.d.ts b/src/language/parser.d.ts index ed7b27c0b48..5790cc38029 100644 --- a/src/language/parser.d.ts +++ b/src/language/parser.d.ts @@ -14,15 +14,21 @@ import { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, StringValueNode, ListValueNode, + ConstListValueNode, ObjectValueNode, + ConstObjectValueNode, ObjectFieldNode, + ConstObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, TypeSystemDefinitionNode, @@ -99,6 +105,15 @@ export function parseValue( options?: ParseOptions, ): ValueNode; +/** + * Similar to parseValue(), but raises a parse error if it encounters a + * variable. The return type will be a constant value. + */ +export function parseConstValue( + source: string | Source, + options?: ParseOptions, +): ConstValueNode; + /** * Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for * that type. @@ -203,14 +218,16 @@ export declare class Parser { /** * Arguments[Const] : ( Argument[?Const]+ ) */ + parseArguments(isConst: true): Array; parseArguments(isConst: boolean): Array; /** * Argument[Const] : Name : Value[?Const] */ - parseArgument(): ArgumentNode; + parseArgument(isConst: true): ConstArgumentNode; + parseArgument(isConst: boolean): ArgumentNode; - parseConstArgument(): ArgumentNode; + parseConstArgument(): ConstArgumentNode; /** * Corresponds to both FragmentSpread and InlineFragment in the spec. @@ -252,6 +269,7 @@ export declare class Parser { * * EnumValue : Name but not `true`, `false` or `null` */ + parseValueLiteral(isConst: true): ConstValueNode; parseValueLiteral(isConst: boolean): ValueNode; parseStringLiteral(): StringValueNode; @@ -261,6 +279,7 @@ export declare class Parser { * - [ ] * - [ Value[?Const]+ ] */ + parseList(isConst: true): ConstListValueNode; parseList(isConst: boolean): ListValueNode; /** @@ -268,21 +287,25 @@ export declare class Parser { * - { } * - { ObjectField[?Const]+ } */ + parseObject(isConst: true): ConstObjectValueNode; parseObject(isConst: boolean): ObjectValueNode; /** * ObjectField[Const] : Name : Value[?Const] */ + parseObjectField(isConst: true): ConstObjectFieldNode; parseObjectField(isConst: boolean): ObjectFieldNode; /** * Directives[Const] : Directive[?Const]+ */ + parseDirectives(isConst: true): Array; parseDirectives(isConst: boolean): Array; /** * Directive[Const] : @ Name Arguments[?Const]? */ + parseDirective(isConst: true): ConstDirectiveNode; parseDirective(isConst: boolean): DirectiveNode; /** diff --git a/src/language/parser.js b/src/language/parser.js index e7c3c51066a..a366ac68950 100644 --- a/src/language/parser.js +++ b/src/language/parser.js @@ -15,15 +15,18 @@ import type { SelectionNode, FieldNode, ArgumentNode, + ConstArgumentNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, StringValueNode, ListValueNode, ObjectValueNode, ObjectFieldNode, DirectiveNode, + ConstDirectiveNode, TypeNode, NamedTypeNode, TypeSystemDefinitionNode, @@ -116,6 +119,21 @@ export function parseValue( return value; } +/** + * Similar to parseValue(), but raises a parse error if it encounters a + * variable. The return type will be a constant value. + */ +export function parseConstValue( + source: string | Source, + options?: ParseOptions, +): ConstValueNode { + const parser = new Parser(source, options); + parser.expectToken(TokenKind.SOF); + const value = parser.parseConstValueLiteral(); + parser.expectToken(TokenKind.EOF); + return value; +} + /** * Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for * that type. @@ -297,9 +315,9 @@ export class Parser { variable: this.parseVariable(), type: (this.expectToken(TokenKind.COLON), this.parseTypeReference()), defaultValue: this.expectOptionalToken(TokenKind.EQUALS) - ? this.parseValueLiteral(true) + ? this.parseConstValueLiteral() : undefined, - directives: this.parseDirectives(true), + directives: this.parseConstDirectives(), }); } @@ -382,7 +400,7 @@ export class Parser { /** * Argument[Const] : Name : Value[?Const] */ - parseArgument(): ArgumentNode { + parseArgument(isConst: boolean = false): ArgumentNode { const start = this._lexer.token; const name = this.parseName(); @@ -390,16 +408,12 @@ export class Parser { return this.node(start, { kind: Kind.ARGUMENT, name, - value: this.parseValueLiteral(false), + value: this.parseValueLiteral(isConst), }); } - parseConstArgument(): ArgumentNode { - return this.node(this._lexer.token, { - kind: Kind.ARGUMENT, - name: this.parseName(), - value: (this.expectToken(TokenKind.COLON), this.parseValueLiteral(true)), - }); + parseConstArgument(): ConstArgumentNode { + return (this.parseArgument(true): any); } // Implements the parsing rules in the Fragments section. @@ -530,14 +544,28 @@ export class Parser { }); } case TokenKind.DOLLAR: - if (!isConst) { - return this.parseVariable(); + if (isConst) { + this.expectToken(TokenKind.DOLLAR); + const varName = this.expectOptionalToken(TokenKind.NAME)?.value; + if (varName != null) { + throw syntaxError( + this._lexer.source, + token.start, + `Unexpected variable "$${varName}" in constant value.`, + ); + } else { + throw this.unexpected(token); + } } - break; + return this.parseVariable(); } throw this.unexpected(); } + parseConstValueLiteral(): ConstValueNode { + return (this.parseValueLiteral(true): any); + } + parseStringLiteral(): StringValueNode { const token = this._lexer.token; this._lexer.advance(); @@ -602,6 +630,10 @@ export class Parser { return directives; } + parseConstDirectives(): Array { + return (this.parseDirectives(true): any); + } + /** * Directive[Const] : @ Name Arguments[?Const]? */ @@ -722,7 +754,7 @@ export class Parser { const start = this._lexer.token; const description = this.parseDescription(); this.expectKeyword('schema'); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const operationTypes = this.many( TokenKind.BRACE_L, this.parseOperationTypeDefinition, @@ -759,7 +791,7 @@ export class Parser { const description = this.parseDescription(); this.expectKeyword('scalar'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); return this.node(start, { kind: Kind.SCALAR_TYPE_DEFINITION, description, @@ -779,7 +811,7 @@ export class Parser { this.expectKeyword('type'); const name = this.parseName(); const interfaces = this.parseImplementsInterfaces(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseFieldsDefinition(); return this.node(start, { kind: Kind.OBJECT_TYPE_DEFINITION, @@ -824,7 +856,7 @@ export class Parser { const args = this.parseArgumentDefs(); this.expectToken(TokenKind.COLON); const type = this.parseTypeReference(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); return this.node(start, { kind: Kind.FIELD_DEFINITION, description, @@ -858,9 +890,9 @@ export class Parser { const type = this.parseTypeReference(); let defaultValue; if (this.expectOptionalToken(TokenKind.EQUALS)) { - defaultValue = this.parseValueLiteral(true); + defaultValue = this.parseConstValueLiteral(); } - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); return this.node(start, { kind: Kind.INPUT_VALUE_DEFINITION, description, @@ -881,7 +913,7 @@ export class Parser { this.expectKeyword('interface'); const name = this.parseName(); const interfaces = this.parseImplementsInterfaces(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseFieldsDefinition(); return this.node(start, { kind: Kind.INTERFACE_TYPE_DEFINITION, @@ -902,7 +934,7 @@ export class Parser { const description = this.parseDescription(); this.expectKeyword('union'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const types = this.parseUnionMemberTypes(); return this.node(start, { kind: Kind.UNION_TYPE_DEFINITION, @@ -933,7 +965,7 @@ export class Parser { const description = this.parseDescription(); this.expectKeyword('enum'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const values = this.parseEnumValuesDefinition(); return this.node(start, { kind: Kind.ENUM_TYPE_DEFINITION, @@ -964,7 +996,7 @@ export class Parser { const start = this._lexer.token; const description = this.parseDescription(); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); return this.node(start, { kind: Kind.ENUM_VALUE_DEFINITION, description, @@ -982,7 +1014,7 @@ export class Parser { const description = this.parseDescription(); this.expectKeyword('input'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseInputFieldsDefinition(); return this.node(start, { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, @@ -1051,7 +1083,7 @@ export class Parser { const start = this._lexer.token; this.expectKeyword('extend'); this.expectKeyword('schema'); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const operationTypes = this.optionalMany( TokenKind.BRACE_L, this.parseOperationTypeDefinition, @@ -1076,7 +1108,7 @@ export class Parser { this.expectKeyword('extend'); this.expectKeyword('scalar'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); if (directives.length === 0) { throw this.unexpected(); } @@ -1099,7 +1131,7 @@ export class Parser { this.expectKeyword('type'); const name = this.parseName(); const interfaces = this.parseImplementsInterfaces(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseFieldsDefinition(); if ( interfaces.length === 0 && @@ -1129,7 +1161,7 @@ export class Parser { this.expectKeyword('interface'); const name = this.parseName(); const interfaces = this.parseImplementsInterfaces(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseFieldsDefinition(); if ( interfaces.length === 0 && @@ -1157,7 +1189,7 @@ export class Parser { this.expectKeyword('extend'); this.expectKeyword('union'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const types = this.parseUnionMemberTypes(); if (directives.length === 0 && types.length === 0) { throw this.unexpected(); @@ -1180,7 +1212,7 @@ export class Parser { this.expectKeyword('extend'); this.expectKeyword('enum'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const values = this.parseEnumValuesDefinition(); if (directives.length === 0 && values.length === 0) { throw this.unexpected(); @@ -1203,7 +1235,7 @@ export class Parser { this.expectKeyword('extend'); this.expectKeyword('input'); const name = this.parseName(); - const directives = this.parseDirectives(true); + const directives = this.parseConstDirectives(); const fields = this.parseInputFieldsDefinition(); if (directives.length === 0 && fields.length === 0) { throw this.unexpected(); diff --git a/src/language/predicates.d.ts b/src/language/predicates.d.ts index cdbe1f9fd69..31116076353 100644 --- a/src/language/predicates.d.ts +++ b/src/language/predicates.d.ts @@ -4,6 +4,7 @@ import { ExecutableDefinitionNode, SelectionNode, ValueNode, + ConstValueNode, TypeNode, TypeSystemDefinitionNode, TypeDefinitionNode, @@ -21,6 +22,8 @@ export function isSelectionNode(node: ASTNode): node is SelectionNode; export function isValueNode(node: ASTNode): node is ValueNode; +export function isConstValueNode(node: ASTNode): node is ConstValueNode; + export function isTypeNode(node: ASTNode): node is TypeNode; export function isTypeSystemDefinitionNode( diff --git a/src/language/predicates.js b/src/language/predicates.js index b9108f87ade..8be57aaa7d5 100644 --- a/src/language/predicates.js +++ b/src/language/predicates.js @@ -38,6 +38,17 @@ export function isValueNode(node: ASTNode): boolean %checks { ); } +export function isConstValueNode(node: ASTNode): boolean %checks { + return ( + isValueNode(node) && + (node.kind === Kind.LIST + ? node.values.some(isConstValueNode) + : node.kind === Kind.OBJECT + ? node.fields.some((field) => isConstValueNode(field.value)) + : node.kind !== Kind.VARIABLE) + ); +} + export function isTypeNode(node: ASTNode): boolean %checks { return ( node.kind === Kind.NAMED_TYPE || From 1af561f355d9cb1429b90701c600815f38caf92c Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 10 May 2021 11:55:25 +0300 Subject: [PATCH 05/16] subscription-test: use separate dummy query type (#3069) --- src/subscription/__tests__/subscribe-test.js | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index cc12994c90d..2891202ee8b 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -143,11 +143,18 @@ async function expectPromise(promise: Promise) { }; } +const DummyQueryType = new GraphQLObjectType({ + name: 'Query', + fields: { + dummy: { type: GraphQLString }, + }, +}); + // Check all error cases when initializing the subscription. describe('Subscription Initialization Phase', () => { it('accepts multiple subscription fields defined in schema', async () => { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -183,7 +190,7 @@ describe('Subscription Initialization Phase', () => { } const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -216,7 +223,7 @@ describe('Subscription Initialization Phase', () => { } const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -255,7 +262,7 @@ describe('Subscription Initialization Phase', () => { let didResolveBar = false; const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -267,7 +274,7 @@ describe('Subscription Initialization Phase', () => { }, }, bar: { - type: EmailEventType, + type: GraphQLString, // istanbul ignore next (Shouldn't be called) subscribe() { didResolveBar = true; @@ -295,7 +302,7 @@ describe('Subscription Initialization Phase', () => { it('throws an error if some of required arguments are missing', async () => { const document = parse('subscription { foo }'); const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -327,7 +334,7 @@ describe('Subscription Initialization Phase', () => { it('resolves to an error for unknown subscription field', async () => { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -350,7 +357,7 @@ describe('Subscription Initialization Phase', () => { it('should pass through unexpected errors thrown in subscribe', async () => { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -365,7 +372,7 @@ describe('Subscription Initialization Phase', () => { it('throws an error if subscribe does not return an iterator', async () => { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -387,7 +394,7 @@ describe('Subscription Initialization Phase', () => { it('resolves to an error for subscription resolver errors', async () => { async function subscribeWithFn(subscribeFn: () => mixed) { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -439,7 +446,7 @@ describe('Subscription Initialization Phase', () => { it('resolves to an error if variables were wrong type', async () => { const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -853,7 +860,7 @@ describe('Subscription Publish Phase', () => { } const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { @@ -914,7 +921,7 @@ describe('Subscription Publish Phase', () => { } const schema = new GraphQLSchema({ - query: QueryType, + query: DummyQueryType, subscription: new GraphQLObjectType({ name: 'Subscription', fields: { From d695f530b3b94e3a37435a3eb5788e8f39b5fe1b Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 10 May 2021 12:43:28 +0300 Subject: [PATCH 06/16] subscription-test: standardize generator names (#3070) --- src/subscription/__tests__/subscribe-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index 2891202ee8b..e9e242fe65d 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -164,14 +164,14 @@ describe('Subscription Initialization Phase', () => { }), }); - async function* fooGen() { + async function* fooGenerator() { yield { foo: 'FooValue' }; } const subscription = await subscribe({ schema, document: parse('subscription { foo }'), - rootValue: { foo: fooGen }, + rootValue: { foo: fooGenerator }, }); invariant(isAsyncIterable(subscription)); From bd5583c6b2e75461dff149e42838f1ba0b543797 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 10 May 2021 15:10:33 +0300 Subject: [PATCH 07/16] Flow: add missing names to arguments of function types (#3072) In preparation to TS convertion --- src/execution/values.js | 2 +- src/jsutils/instanceOf.js | 2 +- src/jsutils/memoize3.js | 2 +- src/jsutils/promiseReduce.js | 2 +- src/language/__tests__/predicates-test.js | 2 +- src/language/visitor.js | 2 +- src/subscription/__tests__/mapAsyncIterator-test.js | 2 +- src/subscription/__tests__/simplePubSub.js | 4 ++-- src/subscription/mapAsyncIterator.js | 2 +- src/type/validate.js | 2 +- src/utilities/lexicographicSortSchema.js | 7 +++++-- src/validation/ValidationContext.js | 6 +++--- 12 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/execution/values.js b/src/execution/values.js index d91633acd43..3b6728254ba 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -74,7 +74,7 @@ function coerceVariableValues( schema: GraphQLSchema, varDefNodes: $ReadOnlyArray, inputs: { +[variable: string]: mixed, ... }, - onError: (GraphQLError) => void, + onError: (error: GraphQLError) => void, ): { [variable: string]: mixed, ... } { const coercedValues = {}; for (const varDefNode of varDefNodes) { diff --git a/src/jsutils/instanceOf.js b/src/jsutils/instanceOf.js index 81b63cbb546..1f2df36707b 100644 --- a/src/jsutils/instanceOf.js +++ b/src/jsutils/instanceOf.js @@ -4,7 +4,7 @@ * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production * See: https://webpack.js.org/guides/production/ */ -export const instanceOf: (mixed, Constructor) => boolean = +export const instanceOf: (value: mixed, constructor: Constructor) => boolean = process.env.NODE_ENV === 'production' ? // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2317') function instanceOf(value: mixed, constructor: Constructor): boolean { diff --git a/src/jsutils/memoize3.js b/src/jsutils/memoize3.js index 925b72d5704..fe7f9a4d79d 100644 --- a/src/jsutils/memoize3.js +++ b/src/jsutils/memoize3.js @@ -6,7 +6,7 @@ export function memoize3< A2: { ... } | $ReadOnlyArray, A3: { ... } | $ReadOnlyArray, R, ->(fn: (A1, A2, A3) => R): (A1, A2, A3) => R { +>(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { let cache0; return function memoized(a1, a2, a3) { diff --git a/src/jsutils/promiseReduce.js b/src/jsutils/promiseReduce.js index 45a1d7964c7..de0ec20169d 100644 --- a/src/jsutils/promiseReduce.js +++ b/src/jsutils/promiseReduce.js @@ -11,7 +11,7 @@ import { isPromise } from './isPromise'; */ export function promiseReduce( values: $ReadOnlyArray, - callback: (U, T) => PromiseOrValue, + callback: (accumulator: U, currentValue: T) => PromiseOrValue, initialValue: PromiseOrValue, ): PromiseOrValue { return values.reduce( diff --git a/src/language/__tests__/predicates-test.js b/src/language/__tests__/predicates-test.js index 4a9f03571e9..308551bf819 100644 --- a/src/language/__tests__/predicates-test.js +++ b/src/language/__tests__/predicates-test.js @@ -21,7 +21,7 @@ const allASTNodes: Array = Object.values(Kind).map( (kind) => ({ kind }: any), ); -function filterNodes(predicate: (ASTNode) => boolean): Array { +function filterNodes(predicate: (node: ASTNode) => boolean): Array { return allASTNodes.filter(predicate).map(({ kind }) => kind); } diff --git a/src/language/visitor.js b/src/language/visitor.js index cafa5103a2a..0c241d5dd51 100644 --- a/src/language/visitor.js +++ b/src/language/visitor.js @@ -11,7 +11,7 @@ export type ASTVisitor = $Shape & KindVisitor>; type KindVisitor = $ObjMap< ASTKindToNode, - (Node) => ASTVisitFn | EnterLeaveVisitor, + (node: Node) => ASTVisitFn | EnterLeaveVisitor, >; type EnterLeaveVisitor = {| diff --git a/src/subscription/__tests__/mapAsyncIterator-test.js b/src/subscription/__tests__/mapAsyncIterator-test.js index 10e6edffba8..b1ff21c1c80 100644 --- a/src/subscription/__tests__/mapAsyncIterator-test.js +++ b/src/subscription/__tests__/mapAsyncIterator-test.js @@ -276,7 +276,7 @@ describe('mapAsyncIterator', () => { .with.property('message', 'Goodbye'); }); - async function testClosesSourceWithMapper(mapper: (number) => T) { + async function testClosesSourceWithMapper(mapper: (value: number) => T) { let didVisitFinally = false; async function* source() { diff --git a/src/subscription/__tests__/simplePubSub.js b/src/subscription/__tests__/simplePubSub.js index 6eb64ad5eaf..f11f9d6b24a 100644 --- a/src/subscription/__tests__/simplePubSub.js +++ b/src/subscription/__tests__/simplePubSub.js @@ -3,7 +3,7 @@ * PubSub system for tests. */ export class SimplePubSub { - _subscribers: Set<(T) => void>; + _subscribers: Set<(value: T) => void>; constructor() { this._subscribers = new Set(); @@ -16,7 +16,7 @@ export class SimplePubSub { return this._subscribers.size > 0; } - getSubscriber(transform: (T) => R): AsyncGenerator { + getSubscriber(transform: (value: T) => R): AsyncGenerator { const pullQueue: Array<(result: IteratorResult) => void> = []; const pushQueue = []; let listening = true; diff --git a/src/subscription/mapAsyncIterator.js b/src/subscription/mapAsyncIterator.js index 2b447818964..1c5c670b8a0 100644 --- a/src/subscription/mapAsyncIterator.js +++ b/src/subscription/mapAsyncIterator.js @@ -6,7 +6,7 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; */ export function mapAsyncIterator( iterable: AsyncGenerator | AsyncIterable, - callback: (T) => PromiseOrValue, + callback: (value: T) => PromiseOrValue, ): AsyncGenerator { // $FlowIssue[incompatible-use] const iterator = iterable[Symbol.asyncIterator](); diff --git a/src/type/validate.js b/src/type/validate.js index bc0763a0f72..9de721adae2 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -557,7 +557,7 @@ function validateInputFields( function createInputObjectCircularRefsValidator( context: SchemaValidationContext, -): (GraphQLInputObjectType) => void { +): (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. diff --git a/src/utilities/lexicographicSortSchema.js b/src/utilities/lexicographicSortSchema.js index dc368426bfc..35bae9bd39c 100644 --- a/src/utilities/lexicographicSortSchema.js +++ b/src/utilities/lexicographicSortSchema.js @@ -156,7 +156,10 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } } -function sortObjMap(map: ObjMap, sortValueFn: (T) => R): ObjMap { +function sortObjMap( + map: ObjMap, + sortValueFn: (value: T) => R, +): ObjMap { const sortedMap = Object.create(null); const sortedEntries = sortBy(Object.entries(map), ([key]) => key); for (const [key, value] of sortedEntries) { @@ -173,7 +176,7 @@ function sortByName( function sortBy( array: $ReadOnlyArray, - mapToKey: (T) => string, + mapToKey: (item: T) => string, ): Array { return array.slice().sort((obj1, obj2) => { const key1 = mapToKey(obj1); diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index c2f602a8cad..eb241054876 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -128,7 +128,7 @@ export class ASTValidationContext { } } -export type ASTValidationRule = (ASTValidationContext) => ASTVisitor; +export type ASTValidationRule = (context: ASTValidationContext) => ASTVisitor; export class SDLValidationContext extends ASTValidationContext { _schema: ?GraphQLSchema; @@ -147,7 +147,7 @@ export class SDLValidationContext extends ASTValidationContext { } } -export type SDLValidationRule = (SDLValidationContext) => ASTVisitor; +export type SDLValidationRule = (context: SDLValidationContext) => ASTVisitor; export class ValidationContext extends ASTValidationContext { _schema: GraphQLSchema; @@ -246,4 +246,4 @@ export class ValidationContext extends ASTValidationContext { } } -export type ValidationRule = (ValidationContext) => ASTVisitor; +export type ValidationRule = (context: ValidationContext) => ASTVisitor; From 3cfb2bef1673aa82b5ee2e25d2a0d183992f1aeb Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Mon, 10 May 2021 15:23:38 +0200 Subject: [PATCH 08/16] Improve grammar in execution error messages (#3068) --- src/execution/__tests__/abstract-test.js | 4 ++-- src/execution/execute.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/execution/__tests__/abstract-test.js b/src/execution/__tests__/abstract-test.js index 519585af0ff..b39167ddce6 100644 --- a/src/execution/__tests__/abstract-test.js +++ b/src/execution/__tests__/abstract-test.js @@ -557,11 +557,11 @@ describe('Execute: Handles execution of abstract types', () => { ); expectError({ forTypeName: 'Human' }).toEqual( - 'Abstract type "Pet" was resolve to a type "Human" that does not exist inside schema.', + 'Abstract type "Pet" was resolved to a type "Human" that does not exist inside the schema.', ); expectError({ forTypeName: 'String' }).toEqual( - 'Abstract type "Pet" was resolve to a non-object type "String".', + 'Abstract type "Pet" was resolved to a non-object type "String".', ); expectError({ forTypeName: '__Schema' }).toEqual( diff --git a/src/execution/execute.js b/src/execution/execute.js index c7c5e379700..f3a88c8c0fd 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -984,14 +984,14 @@ function ensureValidRuntimeType( const runtimeType = exeContext.schema.getType(runtimeTypeName); if (runtimeType == null) { throw new GraphQLError( - `Abstract type "${returnType.name}" was resolve to a type "${runtimeTypeName}" that does not exist inside schema.`, + `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, fieldNodes, ); } if (!isObjectType(runtimeType)) { throw new GraphQLError( - `Abstract type "${returnType.name}" was resolve to a non-object type "${runtimeTypeName}".`, + `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, fieldNodes, ); } From 78d5f832b6f4c11d7f0af8aab68ff4229ba4f46f Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Mon, 10 May 2021 21:08:55 +0300 Subject: [PATCH 09/16] Improve naming 'err => error' and 'arr => array' (#3073) --- src/utilities/lexicographicSortSchema.js | 4 ++-- src/validation/ValidationContext.d.ts | 6 +++--- src/validation/ValidationContext.js | 8 ++++---- src/validation/__tests__/validation-test.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utilities/lexicographicSortSchema.js b/src/utilities/lexicographicSortSchema.js index 35bae9bd39c..30c0e44807d 100644 --- a/src/utilities/lexicographicSortSchema.js +++ b/src/utilities/lexicographicSortSchema.js @@ -104,8 +104,8 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { })); } - function sortTypes(arr: $ReadOnlyArray): Array { - return sortByName(arr).map(replaceNamedType); + function sortTypes(array: $ReadOnlyArray): Array { + return sortByName(array).map(replaceNamedType); } function sortNamedType(type: GraphQLNamedType): GraphQLNamedType { diff --git a/src/validation/ValidationContext.d.ts b/src/validation/ValidationContext.d.ts index a09b42bcb79..0424c14da48 100644 --- a/src/validation/ValidationContext.d.ts +++ b/src/validation/ValidationContext.d.ts @@ -35,7 +35,7 @@ interface VariableUsage { * validation rule. */ export class ASTValidationContext { - constructor(ast: DocumentNode, onError: (err: GraphQLError) => void); + constructor(ast: DocumentNode, onError: (error: GraphQLError) => void); reportError(error: GraphQLError): undefined; @@ -54,7 +54,7 @@ export class SDLValidationContext extends ASTValidationContext { constructor( ast: DocumentNode, schema: Maybe, - onError: (err: GraphQLError) => void, + onError: (error: GraphQLError) => void, ); getSchema(): Maybe; @@ -67,7 +67,7 @@ export class ValidationContext extends ASTValidationContext { schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, - onError: (err: GraphQLError) => void, + onError: (error: GraphQLError) => void, ); getSchema(): GraphQLSchema; diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index eb241054876..6db25ecc5b5 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -42,7 +42,7 @@ type VariableUsage = {| */ export class ASTValidationContext { _ast: DocumentNode; - _onError: (err: GraphQLError) => void; + _onError: (error: GraphQLError) => void; _fragments: ?ObjMap; _fragmentSpreads: Map>; _recursivelyReferencedFragments: Map< @@ -50,7 +50,7 @@ export class ASTValidationContext { $ReadOnlyArray, >; - constructor(ast: DocumentNode, onError: (err: GraphQLError) => void) { + constructor(ast: DocumentNode, onError: (error: GraphQLError) => void) { this._ast = ast; this._fragments = undefined; this._fragmentSpreads = new Map(); @@ -136,7 +136,7 @@ export class SDLValidationContext extends ASTValidationContext { constructor( ast: DocumentNode, schema: ?GraphQLSchema, - onError: (err: GraphQLError) => void, + onError: (error: GraphQLError) => void, ) { super(ast, onError); this._schema = schema; @@ -162,7 +162,7 @@ export class ValidationContext extends ASTValidationContext { schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, - onError: (err: GraphQLError) => void, + onError: (error: GraphQLError) => void, ) { super(ast, onError); this._schema = schema; diff --git a/src/validation/__tests__/validation-test.js b/src/validation/__tests__/validation-test.js index 1b2d7f8bcdb..2abdf6b2482 100644 --- a/src/validation/__tests__/validation-test.js +++ b/src/validation/__tests__/validation-test.js @@ -71,7 +71,7 @@ describe('Validate: Supports full validation', () => { `); const errors = validate(testSchema, doc, undefined, undefined, typeInfo); - const errorMessages = errors.map((err) => err.message); + const errorMessages = errors.map((error) => error.message); expect(errorMessages).to.deep.equal([ 'Cannot query field "catOrDog" on type "QueryRoot". Did you mean "catOrDog"?', From 98dcac3e6a84156658601bfe303575f448e5fa14 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 10 May 2021 15:24:05 -0700 Subject: [PATCH 10/16] Simplify printSchema directive printing (#3075) --- src/utilities/printSchema.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/utilities/printSchema.js b/src/utilities/printSchema.js index f20b8edb426..cc41725368a 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -18,7 +18,7 @@ import type { GraphQLInputObjectType, } from '../type/definition'; import { isIntrospectionType } from '../type/introspection'; -import { GraphQLString, isSpecifiedScalarType } from '../type/scalars'; +import { isSpecifiedScalarType } from '../type/scalars'; import { DEFAULT_DEPRECATION_REASON, isSpecifiedDirective, @@ -282,9 +282,9 @@ function printDeprecated(reason: ?string): string { if (reason == null) { return ''; } - const reasonAST = astFromValue(reason, GraphQLString); - if (reasonAST && reason !== DEFAULT_DEPRECATION_REASON) { - return ' @deprecated(reason: ' + print(reasonAST) + ')'; + if (reason !== DEFAULT_DEPRECATION_REASON) { + const astValue = print({ kind: 'StringValue', value: reason }); + return ` @deprecated(reason: ${astValue})`; } return ' @deprecated'; } @@ -293,13 +293,8 @@ function printSpecifiedByURL(scalar: GraphQLScalarType): string { if (scalar.specifiedByURL == null) { return ''; } - const url = scalar.specifiedByURL; - const urlAST = astFromValue(url, GraphQLString); - invariant( - urlAST, - 'Unexpected null value returned from `astFromValue` for specifiedByURL', - ); - return ' @specifiedBy(url: ' + print(urlAST) + ')'; + const astValue = print({ kind: 'StringValue', value: scalar.specifiedByURL }); + return ` @specifiedBy(url: ${astValue})`; } function printDescription( From 13c3957fb07bcd57cc1b82ed3fbb6c5538fbf184 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 7 May 2021 12:39:24 -0700 Subject: [PATCH 11/16] Unify InputValueConfig in schema definition Defines a central `GraphQLInputValueConfig` and `GraphQLInputValue` as well as single definitions for converting between them, unifying this common functionality between input values and arguments. This is a pre-req for #3049 --- src/index.d.ts | 4 + src/index.js | 4 + src/type/definition.d.ts | 59 ++++++--------- src/type/definition.js | 154 +++++++++++++++++---------------------- src/type/directives.d.ts | 26 ++++++- src/type/directives.js | 34 ++++++--- src/type/index.d.ts | 4 + src/type/index.js | 8 +- 8 files changed, 151 insertions(+), 142 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index ac935945c5c..348ae20f1e8 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -145,9 +145,13 @@ export { GraphQLSchemaExtensions, GraphQLDirectiveConfig, GraphQLDirectiveExtensions, + GraphQLDirectiveArgument, + GraphQLDirectiveArgumentConfig, GraphQLArgument, GraphQLArgumentConfig, GraphQLArgumentExtensions, + GraphQLInputValue, + GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, GraphQLEnumValue, diff --git a/src/index.js b/src/index.js index 1e6b3f247ad..4e64568ffbf 100644 --- a/src/index.js +++ b/src/index.js @@ -144,6 +144,10 @@ export type { GraphQLDirectiveConfig, GraphQLArgument, GraphQLArgumentConfig, + GraphQLDirectiveArgument, + GraphQLDirectiveArgumentConfig, + GraphQLInputValue, + GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, GraphQLEnumValueConfig, diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index c520340b0e8..d651b2e40a6 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -441,10 +441,6 @@ export class GraphQLObjectType { get [Symbol.toStringTag](): string; } -export function argsToArgsConfig( - args: ReadonlyArray, -): GraphQLFieldConfigArgumentMap; - export interface GraphQLObjectTypeConfig { name: string; description?: Maybe; @@ -545,14 +541,7 @@ export interface GraphQLArgumentExtensions { [attributeName: string]: unknown; } -export interface GraphQLArgumentConfig { - description?: Maybe; - type: GraphQLInputType; - defaultValue?: unknown; - deprecationReason?: Maybe; - extensions?: Maybe>; - astNode?: Maybe; -} +export type GraphQLArgumentConfig = GraphQLInputValueConfig; export type GraphQLFieldConfigMap = ObjMap< GraphQLFieldConfig @@ -574,21 +563,32 @@ export interface GraphQLField< astNode?: Maybe; } -export interface GraphQLArgument { +export type GraphQLArgument = GraphQLInputValue; + +export function isRequiredArgument(arg: GraphQLArgument): boolean; + +export type GraphQLFieldMap = ObjMap< + GraphQLField +>; + +export interface GraphQLInputValue { name: string; description: Maybe; type: GraphQLInputType; defaultValue: unknown; deprecationReason: Maybe; - extensions: Maybe>; + extensions: Maybe>; astNode: Maybe; } -export function isRequiredArgument(arg: GraphQLArgument): boolean; - -export type GraphQLFieldMap = ObjMap< - GraphQLField ->; +export interface GraphQLInputValueConfig { + description?: Maybe; + type: GraphQLInputType; + defaultValue?: unknown; + deprecationReason?: Maybe; + extensions?: Maybe>; + astNode?: Maybe; +} /** * Custom extensions @@ -913,29 +913,14 @@ export interface GraphQLInputObjectTypeConfig { * an object which can contain all the values you need. */ export interface GraphQLInputFieldExtensions { - [attributeName: string]: any; + [attributeName: string]: unknown; } -export interface GraphQLInputFieldConfig { - description?: Maybe; - type: GraphQLInputType; - defaultValue?: unknown; - deprecationReason?: Maybe; - extensions?: Maybe>; - astNode?: Maybe; -} +export type GraphQLInputFieldConfig = GraphQLInputValueConfig; export type GraphQLInputFieldConfigMap = ObjMap; -export interface GraphQLInputField { - name: string; - description?: Maybe; - type: GraphQLInputType; - defaultValue?: unknown; - deprecationReason: Maybe; - extensions: Maybe>; - astNode?: Maybe; -} +export type GraphQLInputField = GraphQLInputValue; export function isRequiredInputField(field: GraphQLInputField): boolean; diff --git a/src/type/definition.js b/src/type/definition.js index 7abdc25b0ab..7e2d00ecd1b 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -821,7 +821,9 @@ function defineFieldMap( name: fieldName, description: fieldConfig.description, type: fieldConfig.type, - args: defineArguments(argsConfig), + args: Object.entries(argsConfig).map(([argName, argConfig]) => + defineInputValue(argConfig, argName), + ), resolve: fieldConfig.resolve, subscribe: fieldConfig.subscribe, deprecationReason: fieldConfig.deprecationReason, @@ -831,20 +833,6 @@ function defineFieldMap( }); } -export function defineArguments( - config: GraphQLFieldConfigArgumentMap, -): $ReadOnlyArray { - return Object.entries(config).map(([argName, argConfig]) => ({ - name: argName, - description: argConfig.description, - type: argConfig.type, - defaultValue: argConfig.defaultValue, - deprecationReason: argConfig.deprecationReason, - extensions: argConfig.extensions && toObjMap(argConfig.extensions), - astNode: argConfig.astNode, - })); -} - function isPlainObj(obj: mixed): boolean { return isObjectLike(obj) && !Array.isArray(obj); } @@ -855,7 +843,7 @@ function fieldsToFieldsConfig( return mapValue(fields, (field) => ({ description: field.description, type: field.type, - args: argsToArgsConfig(field.args), + args: keyValMap(field.args, (arg) => arg.name, inputValueToConfig), resolve: field.resolve, subscribe: field.subscribe, deprecationReason: field.deprecationReason, @@ -864,26 +852,6 @@ function fieldsToFieldsConfig( })); } -/** - * @internal - */ -export function argsToArgsConfig( - args: $ReadOnlyArray, -): GraphQLFieldConfigArgumentMap { - return keyValMap( - args, - (arg) => arg.name, - (arg) => ({ - description: arg.description, - type: arg.type, - defaultValue: arg.defaultValue, - deprecationReason: arg.deprecationReason, - extensions: arg.extensions, - astNode: arg.astNode, - }), - ); -} - export type GraphQLObjectTypeConfig = {| name: string, description?: ?string, @@ -960,14 +928,7 @@ export type GraphQLFieldConfig< export type GraphQLFieldConfigArgumentMap = ObjMap; -export type GraphQLArgumentConfig = {| - description?: ?string, - type: GraphQLInputType, - defaultValue?: mixed, - extensions?: ?ReadOnlyObjMapLike, - deprecationReason?: ?string, - astNode?: ?InputValueDefinitionNode, -|}; +export type GraphQLArgumentConfig = GraphQLInputValueConfig; export type GraphQLFieldConfigMap = ObjMap< GraphQLFieldConfig, @@ -989,7 +950,55 @@ export type GraphQLField< astNode: ?FieldDefinitionNode, |}; -export type GraphQLArgument = {| +export type GraphQLArgument = GraphQLInputValue; + +export function isRequiredArgument(arg: GraphQLArgument): boolean %checks { + return isNonNullType(arg.type) && arg.defaultValue === undefined; +} + +export type GraphQLFieldMap = ObjMap< + GraphQLField, +>; + +/** + * @internal + */ +export function defineInputValue( + config: GraphQLInputValueConfig, + name: string, +): GraphQLInputValue { + devAssert( + !('resolve' in config), + `${name} has a resolve property, but inputs cannot define resolvers.`, + ); + return { + name, + description: config.description, + type: config.type, + defaultValue: config.defaultValue, + deprecationReason: config.deprecationReason, + extensions: config.extensions && toObjMap(config.extensions), + astNode: config.astNode, + }; +} + +/** + * @internal + */ +export function inputValueToConfig( + inputValue: GraphQLInputValue, +): GraphQLInputValueConfig { + return { + description: inputValue.description, + type: inputValue.type, + defaultValue: inputValue.defaultValue, + deprecationReason: inputValue.deprecationReason, + extensions: inputValue.extensions, + astNode: inputValue.astNode, + }; +} + +export type GraphQLInputValue = {| name: string, description: ?string, type: GraphQLInputType, @@ -999,13 +1008,14 @@ export type GraphQLArgument = {| astNode: ?InputValueDefinitionNode, |}; -export function isRequiredArgument(arg: GraphQLArgument): boolean %checks { - return isNonNullType(arg.type) && arg.defaultValue === undefined; -} - -export type GraphQLFieldMap = ObjMap< - GraphQLField, ->; +export type GraphQLInputValueConfig = {| + description?: ?string, + type: GraphQLInputType, + defaultValue?: mixed, + deprecationReason?: ?string, + extensions?: ?ReadOnlyObjMapLike, + astNode?: ?InputValueDefinitionNode, +|}; /** * Interface Type Definition @@ -1497,18 +1507,10 @@ export class GraphQLInputObjectType { } toConfig(): GraphQLInputObjectTypeNormalizedConfig { - const fields = mapValue(this.getFields(), (field) => ({ - description: field.description, - type: field.type, - defaultValue: field.defaultValue, - extensions: field.extensions, - astNode: field.astNode, - })); - return { name: this.name, description: this.description, - fields, + fields: mapValue(this.getFields(), inputValueToConfig), extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, @@ -1542,16 +1544,7 @@ function defineInputFieldMap( !('resolve' in fieldConfig), `${config.name}.${fieldName} field has a resolve property, but Input Types cannot define resolvers.`, ); - - return { - name: fieldName, - description: fieldConfig.description, - type: fieldConfig.type, - defaultValue: fieldConfig.defaultValue, - deprecationReason: fieldConfig.deprecationReason, - extensions: fieldConfig.extensions && toObjMap(fieldConfig.extensions), - astNode: fieldConfig.astNode, - }; + return defineInputValue(fieldConfig, fieldName); }); } @@ -1571,26 +1564,11 @@ type GraphQLInputObjectTypeNormalizedConfig = {| extensionASTNodes: $ReadOnlyArray, |}; -export type GraphQLInputFieldConfig = {| - description?: ?string, - type: GraphQLInputType, - defaultValue?: mixed, - deprecationReason?: ?string, - extensions?: ?ReadOnlyObjMapLike, - astNode?: ?InputValueDefinitionNode, -|}; +export type GraphQLInputFieldConfig = GraphQLInputValueConfig; export type GraphQLInputFieldConfigMap = ObjMap; -export type GraphQLInputField = {| - name: string, - description: ?string, - type: GraphQLInputType, - defaultValue: mixed, - deprecationReason: ?string, - extensions: ?ReadOnlyObjMap, - astNode: ?InputValueDefinitionNode, -|}; +export type GraphQLInputField = GraphQLInputValue; export function isRequiredInputField( field: GraphQLInputField, diff --git a/src/type/directives.d.ts b/src/type/directives.d.ts index 66415d879a2..2ba312e8539 100644 --- a/src/type/directives.d.ts +++ b/src/type/directives.d.ts @@ -2,11 +2,12 @@ /* eslint-disable import/no-cycle */ import { Maybe } from '../jsutils/Maybe'; +import { ObjMap } from '../jsutils/ObjMap'; import { DirectiveDefinitionNode } from '../language/ast'; import { DirectiveLocationEnum } from '../language/directiveLocation'; -import { GraphQLFieldConfigArgumentMap, GraphQLArgument } from './definition'; +import { GraphQLInputValue, GraphQLInputValueConfig } from './definition'; /** * Test if the given value is a GraphQL directive. @@ -36,14 +37,14 @@ export class GraphQLDirective { description: Maybe; locations: Array; isRepeatable: boolean; - args: Array; + args: Array; extensions: Maybe>; astNode: Maybe; constructor(config: Readonly); toConfig(): GraphQLDirectiveConfig & { - args: GraphQLFieldConfigArgumentMap; + args: ObjMap; isRepeatable: boolean; extensions: Maybe>; }; @@ -58,12 +59,29 @@ export interface GraphQLDirectiveConfig { name: string; description?: Maybe; locations: Array; - args?: Maybe; + args?: Maybe>; isRepeatable?: Maybe; extensions?: Maybe>; astNode?: Maybe; } +/** + * Custom extensions + * + * @remarks + * Use a unique identifier name for your extension, for example the name of + * your library or project. Do not use a shortened identifier as this increases + * the risk of conflicts. We recommend you add at most one extension field, + * an object which can contain all the values you need. + */ +export interface GraphQLDirectiveArgumentExtensions { + [attributeName: string]: unknown; +} + +export type GraphQLDirectiveArgument = GraphQLInputValue; + +export type GraphQLDirectiveArgumentConfig = GraphQLInputValueConfig; + /** * Used to conditionally include fields or fragments. */ diff --git a/src/type/directives.js b/src/type/directives.js index a6ca5005c98..cefb44ce22d 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -1,5 +1,10 @@ -import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from '../jsutils/ObjMap'; +import type { + ObjMap, + ReadOnlyObjMap, + ReadOnlyObjMapLike, +} from '../jsutils/ObjMap'; import { inspect } from '../jsutils/inspect'; +import { keyValMap } from '../jsutils/keyValMap'; import { toObjMap } from '../jsutils/toObjMap'; import { devAssert } from '../jsutils/devAssert'; import { instanceOf } from '../jsutils/instanceOf'; @@ -9,14 +14,11 @@ import type { DirectiveDefinitionNode } from '../language/ast'; import type { DirectiveLocationEnum } from '../language/directiveLocation'; import { DirectiveLocation } from '../language/directiveLocation'; -import type { - GraphQLArgument, - GraphQLFieldConfigArgumentMap, -} from './definition'; +import type { GraphQLInputValue, GraphQLInputValueConfig } from './definition'; import { GraphQLString, GraphQLBoolean } from './scalars'; import { - defineArguments, - argsToArgsConfig, + defineInputValue, + inputValueToConfig, GraphQLNonNull, } from './definition'; @@ -48,7 +50,7 @@ export class GraphQLDirective { name: string; description: ?string; locations: Array; - args: $ReadOnlyArray; + args: $ReadOnlyArray; isRepeatable: boolean; extensions: ?ReadOnlyObjMap; astNode: ?DirectiveDefinitionNode; @@ -73,7 +75,9 @@ export class GraphQLDirective { `@${config.name} args must be an object with argument names as keys.`, ); - this.args = defineArguments(args); + this.args = Object.entries(args).map(([argName, argConfig]) => + defineInputValue(argConfig, argName), + ); } toConfig(): GraphQLDirectiveNormalizedConfig { @@ -81,7 +85,7 @@ export class GraphQLDirective { name: this.name, description: this.description, locations: this.locations, - args: argsToArgsConfig(this.args), + args: keyValMap(this.args, (arg) => arg.name, inputValueToConfig), isRepeatable: this.isRepeatable, extensions: this.extensions, astNode: this.astNode, @@ -102,11 +106,13 @@ export class GraphQLDirective { } } +export type GraphQLDirectiveArgument = GraphQLInputValue; + export type GraphQLDirectiveConfig = {| name: string, description?: ?string, locations: Array, - args?: ?GraphQLFieldConfigArgumentMap, + args?: ?GraphQLDirectiveConfigArgumentMap, isRepeatable?: ?boolean, extensions?: ?ReadOnlyObjMapLike, astNode?: ?DirectiveDefinitionNode, @@ -114,11 +120,15 @@ export type GraphQLDirectiveConfig = {| type GraphQLDirectiveNormalizedConfig = {| ...GraphQLDirectiveConfig, - args: GraphQLFieldConfigArgumentMap, + args: GraphQLDirectiveConfigArgumentMap, isRepeatable: boolean, extensions: ?ReadOnlyObjMap, |}; +type GraphQLDirectiveConfigArgumentMap = ObjMap; + +export type GraphQLDirectiveArgumentConfig = GraphQLInputValueConfig; + /** * Used to conditionally include fields or fragments. */ diff --git a/src/type/index.d.ts b/src/type/index.d.ts index 253fcf21139..f645f4527a2 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -80,6 +80,8 @@ export { GraphQLArgument, GraphQLArgumentConfig, GraphQLArgumentExtensions, + GraphQLInputValue, + GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, GraphQLEnumValue, @@ -123,6 +125,7 @@ export { assertDirective, // Directives Definition GraphQLDirective, + GraphQLDirectiveArgument, // Built-in Directives defined by the Spec isSpecifiedDirective, specifiedDirectives, @@ -134,6 +137,7 @@ export { DEFAULT_DEPRECATION_REASON, // type GraphQLDirectiveConfig, + GraphQLDirectiveArgumentConfig, GraphQLDirectiveExtensions, } from './directives'; diff --git a/src/type/index.js b/src/type/index.js index bf0b17973e6..39523812642 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -71,6 +71,7 @@ export { assertDirective, // Directives Definition GraphQLDirective, + GraphQLDirectiveArgument, // Built-in Directives defined by the Spec isSpecifiedDirective, specifiedDirectives, @@ -82,7 +83,10 @@ export { DEFAULT_DEPRECATION_REASON, } from './directives'; -export type { GraphQLDirectiveConfig } from './directives'; +export type { + GraphQLDirectiveConfig, + GraphQLDirectiveArgumentConfig, +} from './directives'; // Common built-in scalar instances. export { @@ -134,6 +138,8 @@ export type { ThunkObjMap, GraphQLArgument, GraphQLArgumentConfig, + GraphQLInputValue, + GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, GraphQLEnumValueConfig, From 13ef6fed219b4ec2414db3278304e11bad082fa9 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Fri, 7 May 2021 13:03:15 -0700 Subject: [PATCH 12/16] Also add isRequiredInput() --- src/index.d.ts | 1 + src/index.js | 1 + src/type/__tests__/predicate-test.js | 27 +++++++++---------- src/type/definition.d.ts | 2 ++ src/type/definition.js | 14 +++++----- src/type/index.d.ts | 1 + src/type/index.js | 1 + src/type/validate.js | 11 ++++---- src/utilities/findBreakingChanges.js | 9 +++---- .../rules/ProvidedRequiredArgumentsRule.js | 10 +++---- .../rules/ValuesOfCorrectTypeRule.js | 4 +-- 11 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 348ae20f1e8..123415ba469 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -94,6 +94,7 @@ export { isWrappingType, isNullableType, isNamedType, + isRequiredInput, isRequiredArgument, isRequiredInputField, isSpecifiedScalarType, diff --git a/src/index.js b/src/index.js index 4e64568ffbf..d0fce51a1d7 100644 --- a/src/index.js +++ b/src/index.js @@ -93,6 +93,7 @@ export { isWrappingType, isNullableType, isNamedType, + isRequiredInput, isRequiredArgument, isRequiredInputField, isSpecifiedScalarType, diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index f94bed10302..899752864cd 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -49,8 +49,7 @@ import { isWrappingType, isNullableType, isNamedType, - isRequiredArgument, - isRequiredInputField, + isRequiredInput, assertType, assertScalarType, assertObjectType, @@ -562,7 +561,7 @@ describe('Type predicates', () => { }); }); - describe('isRequiredArgument', () => { + describe('isRequiredInput', () => { function buildArg(config: {| type: GraphQLInputType, defaultValue?: mixed, @@ -582,35 +581,33 @@ describe('Type predicates', () => { const requiredArg = buildArg({ type: new GraphQLNonNull(GraphQLString), }); - expect(isRequiredArgument(requiredArg)).to.equal(true); + expect(isRequiredInput(requiredArg)).to.equal(true); }); it('returns false for optional arguments', () => { const optArg1 = buildArg({ type: GraphQLString, }); - expect(isRequiredArgument(optArg1)).to.equal(false); + expect(isRequiredInput(optArg1)).to.equal(false); const optArg2 = buildArg({ type: GraphQLString, defaultValue: null, }); - expect(isRequiredArgument(optArg2)).to.equal(false); + expect(isRequiredInput(optArg2)).to.equal(false); const optArg3 = buildArg({ type: new GraphQLList(new GraphQLNonNull(GraphQLString)), }); - expect(isRequiredArgument(optArg3)).to.equal(false); + expect(isRequiredInput(optArg3)).to.equal(false); const optArg4 = buildArg({ type: new GraphQLNonNull(GraphQLString), defaultValue: 'default', }); - expect(isRequiredArgument(optArg4)).to.equal(false); + expect(isRequiredInput(optArg4)).to.equal(false); }); - }); - describe('isRequiredInputField', () => { function buildInputField(config: {| type: GraphQLInputType, defaultValue?: mixed, @@ -630,31 +627,31 @@ describe('Type predicates', () => { const requiredField = buildInputField({ type: new GraphQLNonNull(GraphQLString), }); - expect(isRequiredInputField(requiredField)).to.equal(true); + expect(isRequiredInput(requiredField)).to.equal(true); }); it('returns false for optional input field', () => { const optField1 = buildInputField({ type: GraphQLString, }); - expect(isRequiredInputField(optField1)).to.equal(false); + expect(isRequiredInput(optField1)).to.equal(false); const optField2 = buildInputField({ type: GraphQLString, defaultValue: null, }); - expect(isRequiredInputField(optField2)).to.equal(false); + expect(isRequiredInput(optField2)).to.equal(false); const optField3 = buildInputField({ type: new GraphQLList(new GraphQLNonNull(GraphQLString)), }); - expect(isRequiredInputField(optField3)).to.equal(false); + expect(isRequiredInput(optField3)).to.equal(false); const optField4 = buildInputField({ type: new GraphQLNonNull(GraphQLString), defaultValue: 'default', }); - expect(isRequiredInputField(optField4)).to.equal(false); + expect(isRequiredInput(optField4)).to.equal(false); }); }); }); diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index d651b2e40a6..3614db5b1bb 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -571,6 +571,8 @@ export type GraphQLFieldMap = ObjMap< GraphQLField >; +export function isRequiredInput(input: GraphQLInputValue): boolean; + export interface GraphQLInputValue { name: string; description: Maybe; diff --git a/src/type/definition.js b/src/type/definition.js index 7e2d00ecd1b..8a7f0a72b14 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -952,14 +952,16 @@ export type GraphQLField< export type GraphQLArgument = GraphQLInputValue; -export function isRequiredArgument(arg: GraphQLArgument): boolean %checks { - return isNonNullType(arg.type) && arg.defaultValue === undefined; -} +export const isRequiredArgument = isRequiredInput; export type GraphQLFieldMap = ObjMap< GraphQLField, >; +export function isRequiredInput(input: GraphQLInputValue): boolean %checks { + return isNonNullType(input.type) && input.defaultValue === undefined; +} + /** * @internal */ @@ -1570,10 +1572,6 @@ export type GraphQLInputFieldConfigMap = ObjMap; export type GraphQLInputField = GraphQLInputValue; -export function isRequiredInputField( - field: GraphQLInputField, -): boolean %checks { - return isNonNullType(field.type) && field.defaultValue === undefined; -} +export const isRequiredInputField = isRequiredInput; export type GraphQLInputFieldMap = ObjMap; diff --git a/src/type/index.d.ts b/src/type/index.d.ts index f645f4527a2..f6f1acf3d59 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -30,6 +30,7 @@ export { isWrappingType, isNullableType, isNamedType, + isRequiredInput, isRequiredArgument, isRequiredInputField, // Assertions diff --git a/src/type/index.js b/src/type/index.js index 39523812642..0545fb2c35d 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -29,6 +29,7 @@ export { isWrappingType, isNullableType, isNamedType, + isRequiredInput, isRequiredArgument, isRequiredInputField, // Assertions diff --git a/src/type/validate.js b/src/type/validate.js index 9de721adae2..3aec0a94ee3 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -34,8 +34,7 @@ import { isNonNullType, isInputType, isOutputType, - isRequiredArgument, - isRequiredInputField, + isRequiredInput, } from './definition'; /** @@ -180,7 +179,7 @@ function validateDirectives(context: SchemaValidationContext): void { ); } - if (isRequiredArgument(arg) && arg.deprecationReason != null) { + if (isRequiredInput(arg) && arg.deprecationReason != null) { context.reportError( `Required argument @${directive.name}(${arg.name}:) cannot be deprecated.`, [ @@ -296,7 +295,7 @@ function validateFields( ); } - if (isRequiredArgument(arg) && arg.deprecationReason != null) { + if (isRequiredInput(arg) && arg.deprecationReason != null) { context.reportError( `Required argument ${type.name}.${field.name}(${argName}:) cannot be deprecated.`, [ @@ -424,7 +423,7 @@ function validateTypeImplementsInterface( for (const typeArg of typeField.args) { const argName = typeArg.name; const ifaceArg = ifaceField.args.find((arg) => arg.name === argName); - if (!ifaceArg && isRequiredArgument(typeArg)) { + if (!ifaceArg && isRequiredInput(typeArg)) { context.reportError( `Object field ${type.name}.${fieldName} includes required argument ${argName} that is missing from the Interface field ${iface.name}.${fieldName}.`, [typeArg.astNode, ifaceField.astNode], @@ -542,7 +541,7 @@ function validateInputFields( ); } - if (isRequiredInputField(field) && field.deprecationReason != null) { + if (isRequiredInput(field) && field.deprecationReason != null) { context.reportError( `Required input field ${inputObj.name}.${field.name} cannot be deprecated.`, [ diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index 2a13d4655b6..08caec042e7 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -29,8 +29,7 @@ import { isNonNullType, isListType, isNamedType, - isRequiredArgument, - isRequiredInputField, + isRequiredInput, } from '../type/definition'; import { astFromValue } from './astFromValue'; @@ -133,7 +132,7 @@ function findDirectiveChanges( const argsDiff = diff(oldDirective.args, newDirective.args); for (const newArg of argsDiff.added) { - if (isRequiredArgument(newArg)) { + if (isRequiredInput(newArg)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, description: `A required arg ${newArg.name} on directive ${oldDirective.name} was added.`, @@ -229,7 +228,7 @@ function findInputObjectTypeChanges( ); for (const newField of fieldsDiff.added) { - if (isRequiredInputField(newField)) { + if (isRequiredInput(newField)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED, description: `A required field ${newField.name} on input type ${oldType.name} was added.`, @@ -427,7 +426,7 @@ function findArgChanges( } for (const newArg of argsDiff.added) { - if (isRequiredArgument(newArg)) { + if (isRequiredInput(newArg)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_ARG_ADDED, description: `A required arg ${newArg.name} on ${oldType.name}.${oldField.name} was added.`, diff --git a/src/validation/rules/ProvidedRequiredArgumentsRule.js b/src/validation/rules/ProvidedRequiredArgumentsRule.js index 0ddc8f784dc..d67443d1042 100644 --- a/src/validation/rules/ProvidedRequiredArgumentsRule.js +++ b/src/validation/rules/ProvidedRequiredArgumentsRule.js @@ -9,7 +9,7 @@ import { Kind } from '../../language/kinds'; import { print } from '../../language/printer'; import { specifiedDirectives } from '../../type/directives'; -import { isType, isRequiredArgument } from '../../type/definition'; +import { isType, isRequiredInput } from '../../type/definition'; import type { ValidationContext, @@ -41,7 +41,7 @@ export function ProvidedRequiredArgumentsRule( fieldNode.arguments?.map((arg) => arg.name.value), ); for (const argDef of fieldDef.args) { - if (!providedArgs.has(argDef.name) && isRequiredArgument(argDef)) { + if (!providedArgs.has(argDef.name) && isRequiredInput(argDef)) { const argTypeStr = inspect(argDef.type); context.reportError( new GraphQLError( @@ -68,7 +68,7 @@ export function ProvidedRequiredArgumentsOnDirectivesRule( const definedDirectives = schema?.getDirectives() ?? specifiedDirectives; for (const directive of definedDirectives) { requiredArgsMap[directive.name] = keyMap( - directive.args.filter(isRequiredArgument), + directive.args.filter(isRequiredInput), (arg) => arg.name, ); } @@ -80,7 +80,7 @@ export function ProvidedRequiredArgumentsOnDirectivesRule( const argNodes = def.arguments ?? []; requiredArgsMap[def.name.value] = keyMap( - argNodes.filter(isRequiredArgumentNode), + argNodes.filter(isRequiredInputNode), (arg) => arg.name.value, ); } @@ -115,6 +115,6 @@ export function ProvidedRequiredArgumentsOnDirectivesRule( }; } -function isRequiredArgumentNode(arg: InputValueDefinitionNode): boolean { +function isRequiredInputNode(arg: InputValueDefinitionNode): boolean { return arg.type.kind === Kind.NON_NULL_TYPE && arg.defaultValue == null; } diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.js b/src/validation/rules/ValuesOfCorrectTypeRule.js index 6d5dc5c1ca8..9153b1a5edf 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.js +++ b/src/validation/rules/ValuesOfCorrectTypeRule.js @@ -14,7 +14,7 @@ import { isInputObjectType, isListType, isNonNullType, - isRequiredInputField, + isRequiredInput, getNullableType, getNamedType, } from '../../type/definition'; @@ -50,7 +50,7 @@ export function ValuesOfCorrectTypeRule( const fieldNodeMap = keyMap(node.fields, (field) => field.name.value); for (const fieldDef of Object.values(type.getFields())) { const fieldNode = fieldNodeMap[fieldDef.name]; - if (!fieldNode && isRequiredInputField(fieldDef)) { + if (!fieldNode && isRequiredInput(fieldDef)) { const typeStr = inspect(fieldDef.type); context.reportError( new GraphQLError( From 5d0c2f3e241a105c986cb7d171fde4c3f66b66fb Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 10 May 2021 09:54:51 -0700 Subject: [PATCH 13/16] Revert DirectiveArgument --- src/index.d.ts | 2 -- src/index.js | 2 -- src/type/directives.d.ts | 25 ++++--------------------- src/type/directives.js | 14 ++++---------- src/type/index.d.ts | 2 -- src/type/index.js | 6 +----- 6 files changed, 9 insertions(+), 42 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 123415ba469..ddf092347fb 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -146,8 +146,6 @@ export { GraphQLSchemaExtensions, GraphQLDirectiveConfig, GraphQLDirectiveExtensions, - GraphQLDirectiveArgument, - GraphQLDirectiveArgumentConfig, GraphQLArgument, GraphQLArgumentConfig, GraphQLArgumentExtensions, diff --git a/src/index.js b/src/index.js index d0fce51a1d7..a9864a384f4 100644 --- a/src/index.js +++ b/src/index.js @@ -145,8 +145,6 @@ export type { GraphQLDirectiveConfig, GraphQLArgument, GraphQLArgumentConfig, - GraphQLDirectiveArgument, - GraphQLDirectiveArgumentConfig, GraphQLInputValue, GraphQLInputValueConfig, GraphQLEnumTypeConfig, diff --git a/src/type/directives.d.ts b/src/type/directives.d.ts index 2ba312e8539..69f86563350 100644 --- a/src/type/directives.d.ts +++ b/src/type/directives.d.ts @@ -7,7 +7,7 @@ import { ObjMap } from '../jsutils/ObjMap'; import { DirectiveDefinitionNode } from '../language/ast'; import { DirectiveLocationEnum } from '../language/directiveLocation'; -import { GraphQLInputValue, GraphQLInputValueConfig } from './definition'; +import { GraphQLArgument, GraphQLArgumentConfig } from './definition'; /** * Test if the given value is a GraphQL directive. @@ -37,14 +37,14 @@ export class GraphQLDirective { description: Maybe; locations: Array; isRepeatable: boolean; - args: Array; + args: Array; extensions: Maybe>; astNode: Maybe; constructor(config: Readonly); toConfig(): GraphQLDirectiveConfig & { - args: ObjMap; + args: ObjMap; isRepeatable: boolean; extensions: Maybe>; }; @@ -59,29 +59,12 @@ export interface GraphQLDirectiveConfig { name: string; description?: Maybe; locations: Array; - args?: Maybe>; + args?: Maybe>; isRepeatable?: Maybe; extensions?: Maybe>; astNode?: Maybe; } -/** - * Custom extensions - * - * @remarks - * Use a unique identifier name for your extension, for example the name of - * your library or project. Do not use a shortened identifier as this increases - * the risk of conflicts. We recommend you add at most one extension field, - * an object which can contain all the values you need. - */ -export interface GraphQLDirectiveArgumentExtensions { - [attributeName: string]: unknown; -} - -export type GraphQLDirectiveArgument = GraphQLInputValue; - -export type GraphQLDirectiveArgumentConfig = GraphQLInputValueConfig; - /** * Used to conditionally include fields or fragments. */ diff --git a/src/type/directives.js b/src/type/directives.js index cefb44ce22d..702abebb1e1 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -14,7 +14,7 @@ import type { DirectiveDefinitionNode } from '../language/ast'; import type { DirectiveLocationEnum } from '../language/directiveLocation'; import { DirectiveLocation } from '../language/directiveLocation'; -import type { GraphQLInputValue, GraphQLInputValueConfig } from './definition'; +import type { GraphQLArgument, GraphQLArgumentConfig } from './definition'; import { GraphQLString, GraphQLBoolean } from './scalars'; import { defineInputValue, @@ -50,7 +50,7 @@ export class GraphQLDirective { name: string; description: ?string; locations: Array; - args: $ReadOnlyArray; + args: $ReadOnlyArray; isRepeatable: boolean; extensions: ?ReadOnlyObjMap; astNode: ?DirectiveDefinitionNode; @@ -106,13 +106,11 @@ export class GraphQLDirective { } } -export type GraphQLDirectiveArgument = GraphQLInputValue; - export type GraphQLDirectiveConfig = {| name: string, description?: ?string, locations: Array, - args?: ?GraphQLDirectiveConfigArgumentMap, + args?: ?ObjMap, isRepeatable?: ?boolean, extensions?: ?ReadOnlyObjMapLike, astNode?: ?DirectiveDefinitionNode, @@ -120,15 +118,11 @@ export type GraphQLDirectiveConfig = {| type GraphQLDirectiveNormalizedConfig = {| ...GraphQLDirectiveConfig, - args: GraphQLDirectiveConfigArgumentMap, + args: ObjMap, isRepeatable: boolean, extensions: ?ReadOnlyObjMap, |}; -type GraphQLDirectiveConfigArgumentMap = ObjMap; - -export type GraphQLDirectiveArgumentConfig = GraphQLInputValueConfig; - /** * Used to conditionally include fields or fragments. */ diff --git a/src/type/index.d.ts b/src/type/index.d.ts index f6f1acf3d59..fc7c22ead89 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -126,7 +126,6 @@ export { assertDirective, // Directives Definition GraphQLDirective, - GraphQLDirectiveArgument, // Built-in Directives defined by the Spec isSpecifiedDirective, specifiedDirectives, @@ -138,7 +137,6 @@ export { DEFAULT_DEPRECATION_REASON, // type GraphQLDirectiveConfig, - GraphQLDirectiveArgumentConfig, GraphQLDirectiveExtensions, } from './directives'; diff --git a/src/type/index.js b/src/type/index.js index 0545fb2c35d..851d9a192d8 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -72,7 +72,6 @@ export { assertDirective, // Directives Definition GraphQLDirective, - GraphQLDirectiveArgument, // Built-in Directives defined by the Spec isSpecifiedDirective, specifiedDirectives, @@ -84,10 +83,7 @@ export { DEFAULT_DEPRECATION_REASON, } from './directives'; -export type { - GraphQLDirectiveConfig, - GraphQLDirectiveArgumentConfig, -} from './directives'; +export type { GraphQLDirectiveConfig } from './directives'; // Common built-in scalar instances. export { From 1d87c029bb6edb15902b025809d832b92520218c Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 10 May 2021 10:27:14 -0700 Subject: [PATCH 14/16] Update other duplicated callsites in extendSchema and lexicographicSortSchema --- src/index.d.ts | 2 - src/index.js | 2 - src/type/definition.d.ts | 8 +--- src/type/definition.js | 8 +--- src/type/index.d.ts | 2 - src/type/index.js | 2 - src/utilities/extendSchema.js | 56 +++++++++--------------- src/utilities/lexicographicSortSchema.js | 24 ++++------ 8 files changed, 32 insertions(+), 72 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index ddf092347fb..317602293e5 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -160,14 +160,12 @@ export { GraphQLField, GraphQLFieldConfig, GraphQLFieldExtensions, - GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldMap, GraphQLFieldResolver, GraphQLInputField, GraphQLInputFieldConfig, GraphQLInputFieldExtensions, - GraphQLInputFieldConfigMap, GraphQLInputFieldMap, GraphQLInputObjectTypeConfig, GraphQLInputObjectTypeExtensions, diff --git a/src/index.js b/src/index.js index a9864a384f4..b46f10cd3a0 100644 --- a/src/index.js +++ b/src/index.js @@ -153,13 +153,11 @@ export type { GraphQLEnumValueConfigMap, GraphQLField, GraphQLFieldConfig, - GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldMap, GraphQLFieldResolver, GraphQLInputField, GraphQLInputFieldConfig, - GraphQLInputFieldConfigMap, GraphQLInputFieldMap, GraphQLInputObjectTypeConfig, GraphQLInterfaceTypeConfig, diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 3614db5b1bb..f07202d91af 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -516,7 +516,7 @@ export interface GraphQLFieldConfig< > { description?: Maybe; type: GraphQLOutputType; - args?: GraphQLFieldConfigArgumentMap; + args?: ObjMap; resolve?: GraphQLFieldResolver; subscribe?: GraphQLFieldResolver; deprecationReason?: Maybe; @@ -526,8 +526,6 @@ export interface GraphQLFieldConfig< astNode?: Maybe; } -export type GraphQLFieldConfigArgumentMap = ObjMap; - /** * Custom extensions * @@ -885,7 +883,7 @@ export class GraphQLInputObjectType { getFields(): GraphQLInputFieldMap; toConfig(): GraphQLInputObjectTypeConfig & { - fields: GraphQLInputFieldConfigMap; + fields: ObjMap; extensions: Maybe>; extensionASTNodes: ReadonlyArray; }; @@ -920,8 +918,6 @@ export interface GraphQLInputFieldExtensions { export type GraphQLInputFieldConfig = GraphQLInputValueConfig; -export type GraphQLInputFieldConfigMap = ObjMap; - export type GraphQLInputField = GraphQLInputValue; export function isRequiredInputField(field: GraphQLInputField): boolean; diff --git a/src/type/definition.js b/src/type/definition.js index 8a7f0a72b14..c16433d5b31 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -918,7 +918,7 @@ export type GraphQLFieldConfig< > = {| description?: ?string, type: GraphQLOutputType, - args?: GraphQLFieldConfigArgumentMap, + args?: ObjMap, resolve?: GraphQLFieldResolver, subscribe?: GraphQLFieldResolver, deprecationReason?: ?string, @@ -926,8 +926,6 @@ export type GraphQLFieldConfig< astNode?: ?FieldDefinitionNode, |}; -export type GraphQLFieldConfigArgumentMap = ObjMap; - export type GraphQLArgumentConfig = GraphQLInputValueConfig; export type GraphQLFieldConfigMap = ObjMap< @@ -1561,15 +1559,13 @@ export type GraphQLInputObjectTypeConfig = {| type GraphQLInputObjectTypeNormalizedConfig = {| ...GraphQLInputObjectTypeConfig, - fields: GraphQLInputFieldConfigMap, + fields: ObjMap, extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |}; export type GraphQLInputFieldConfig = GraphQLInputValueConfig; -export type GraphQLInputFieldConfigMap = ObjMap; - export type GraphQLInputField = GraphQLInputValue; export const isRequiredInputField = isRequiredInput; diff --git a/src/type/index.d.ts b/src/type/index.d.ts index fc7c22ead89..365256dfd1d 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -91,14 +91,12 @@ export { GraphQLEnumValueExtensions, GraphQLField, GraphQLFieldConfig, - GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldExtensions, GraphQLFieldMap, GraphQLFieldResolver, GraphQLInputField, GraphQLInputFieldConfig, - GraphQLInputFieldConfigMap, GraphQLInputFieldExtensions, GraphQLInputFieldMap, GraphQLInputObjectTypeConfig, diff --git a/src/type/index.js b/src/type/index.js index 851d9a192d8..6bf0943f228 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -143,13 +143,11 @@ export type { GraphQLEnumValueConfigMap, GraphQLField, GraphQLFieldConfig, - GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldMap, GraphQLFieldResolver, GraphQLInputField, GraphQLInputFieldConfig, - GraphQLInputFieldConfigMap, GraphQLInputFieldMap, GraphQLInputObjectTypeConfig, GraphQLInterfaceTypeConfig, diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 71be143ca8e..a824f0fb97f 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -1,3 +1,4 @@ +import type { ObjMap } from '../jsutils/ObjMap'; import { keyMap } from '../jsutils/keyMap'; import { inspect } from '../jsutils/inspect'; import { mapValue } from '../jsutils/mapValue'; @@ -48,10 +49,9 @@ import type { GraphQLNamedType, GraphQLFieldConfig, GraphQLFieldConfigMap, + GraphQLInputValueConfig, GraphQLArgumentConfig, - GraphQLFieldConfigArgumentMap, GraphQLEnumValueConfigMap, - GraphQLInputFieldConfigMap, } from '../type/definition'; import { assertSchema, GraphQLSchema } from '../type/schema'; import { specifiedScalarTypes, isSpecifiedScalarType } from '../type/scalars'; @@ -446,7 +446,7 @@ export function extendSchemaImpl( description: node.description?.value, locations, isRepeatable: node.repeatable, - args: buildArgumentMap(node.arguments), + args: buildInputValueMap(node.arguments), astNode: node, }); } @@ -471,7 +471,7 @@ export function extendSchemaImpl( // with validateSchema() will produce more actionable results. type: (getWrappedType(field.type): any), description: field.description?.value, - args: buildArgumentMap(field.arguments), + args: buildInputValueMap(field.arguments), deprecationReason: getDeprecationReason(field), astNode: field, }; @@ -480,54 +480,38 @@ export function extendSchemaImpl( return fieldConfigMap; } - function buildArgumentMap( - args: ?$ReadOnlyArray, - ): GraphQLFieldConfigArgumentMap { + function buildInputValueMap( + nodes: ?$ReadOnlyArray, + configMap: ObjMap = Object.create(null), + ): ObjMap { // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') - const argsNodes = args ?? []; + const inputNodes = nodes ?? []; - const argConfigMap = Object.create(null); - for (const arg of argsNodes) { + for (const node of inputNodes) { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - const type: any = getWrappedType(arg.type); + const type: any = getWrappedType(node.type); - argConfigMap[arg.name.value] = { + configMap[node.name.value] = { type, - description: arg.description?.value, - defaultValue: valueFromAST(arg.defaultValue, type), - deprecationReason: getDeprecationReason(arg), - astNode: arg, + description: node.description?.value, + defaultValue: valueFromAST(node.defaultValue, type), + deprecationReason: getDeprecationReason(node), + astNode: node, }; } - return argConfigMap; + return configMap; } function buildInputFieldMap( nodes: $ReadOnlyArray< InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, >, - ): GraphQLInputFieldConfigMap { - const inputFieldMap = Object.create(null); + ): ObjMap { + let inputFieldMap = Object.create(null); for (const node of nodes) { - // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') - const fieldsNodes = node.fields ?? []; - - for (const field of fieldsNodes) { - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - const type: any = getWrappedType(field.type); - - inputFieldMap[field.name.value] = { - type, - description: field.description?.value, - defaultValue: valueFromAST(field.defaultValue, type), - deprecationReason: getDeprecationReason(field), - astNode: field, - }; - } + inputFieldMap = buildInputValueMap(node.fields, inputFieldMap); } return inputFieldMap; } diff --git a/src/utilities/lexicographicSortSchema.js b/src/utilities/lexicographicSortSchema.js index 30c0e44807d..67e1b0edc06 100644 --- a/src/utilities/lexicographicSortSchema.js +++ b/src/utilities/lexicographicSortSchema.js @@ -8,8 +8,7 @@ import type { GraphQLType, GraphQLNamedType, GraphQLFieldConfigMap, - GraphQLFieldConfigArgumentMap, - GraphQLInputFieldConfigMap, + GraphQLInputValueConfig, } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import { GraphQLDirective } from '../type/directives'; @@ -78,14 +77,14 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { return new GraphQLDirective({ ...config, locations: sortBy(config.locations, (x) => x), - args: sortArgs(config.args), + args: sortInputs(config.args), }); } - function sortArgs(args: GraphQLFieldConfigArgumentMap) { - return sortObjMap(args, (arg) => ({ - ...arg, - type: replaceType(arg.type), + function sortInputs(inputs: ObjMap) { + return sortObjMap(inputs, (input) => ({ + ...input, + type: replaceType(input.type), })); } @@ -93,14 +92,7 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { return sortObjMap(fieldsMap, (field) => ({ ...field, type: replaceType(field.type), - args: sortArgs(field.args), - })); - } - - function sortInputFields(fieldsMap: GraphQLInputFieldConfigMap) { - return sortObjMap(fieldsMap, (field) => ({ - ...field, - type: replaceType(field.type), + args: sortInputs(field.args), })); } @@ -147,7 +139,7 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { const config = type.toConfig(); return new GraphQLInputObjectType({ ...config, - fields: () => sortInputFields(config.fields), + fields: () => sortInputs(config.fields), }); } From 0a91e63342700fd00e3bf86f40f11f88f530fb12 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 10 May 2021 11:44:12 -0700 Subject: [PATCH 15/16] Preserve defaultValue literals Fixes #3051 --- src/execution/values.js | 15 ++++++--- src/index.d.ts | 4 +++ src/index.js | 4 +++ src/type/__tests__/predicate-test.js | 31 +++++++------------ src/type/defaultValues.d.ts | 13 ++++++++ src/type/defaultValues.js | 29 +++++++++++++++++ src/type/definition.d.ts | 8 ++++- src/type/definition.js | 21 +++++++++++-- src/type/index.d.ts | 6 ++++ src/type/index.js | 6 ++++ src/type/introspection.js | 16 ++++++---- src/utilities/TypeInfo.js | 6 ++-- src/utilities/__tests__/astFromValue-test.js | 2 ++ .../__tests__/buildClientSchema-test.js | 22 +++++++++++++ src/utilities/__tests__/valueFromAST-test.js | 15 +++++++++ src/utilities/astFromValue.js | 12 ++++--- src/utilities/buildClientSchema.js | 12 +++---- src/utilities/coerceInputValue.js | 8 +++-- src/utilities/extendSchema.js | 4 +-- src/utilities/findBreakingChanges.js | 23 +++++++------- src/utilities/printSchema.js | 10 +++--- src/utilities/valueFromAST.js | 9 ++++-- 22 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 src/type/defaultValues.d.ts create mode 100644 src/type/defaultValues.js diff --git a/src/execution/values.js b/src/execution/values.js index 3b6728254ba..173f04c65ae 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -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'; @@ -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)}" ` + @@ -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)}" ` + diff --git a/src/index.d.ts b/src/index.d.ts index 317602293e5..db8241b04e2 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -126,6 +126,9 @@ export { // Validate GraphQL schema. validateSchema, assertValidSchema, + // Operate on default values. + getCoercedDefaultValue, + getLiteralDefaultValue, } from './type/index'; export { @@ -150,6 +153,7 @@ export { GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, diff --git a/src/index.js b/src/index.js index b46f10cd3a0..71dbddf0fa3 100644 --- a/src/index.js +++ b/src/index.js @@ -125,6 +125,9 @@ export { // Validate GraphQL schema. validateSchema, assertValidSchema, + // Operate on default values. + getCoercedDefaultValue, + getLiteralDefaultValue, } from './type/index'; export type { @@ -146,6 +149,7 @@ export type { GraphQLArgument, GraphQLArgumentConfig, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index 899752864cd..2ac688b4c8b 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -69,6 +69,7 @@ import { assertNamedType, getNullableType, getNamedType, + defineInputValue, } from '../definition'; const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} }); @@ -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', () => { @@ -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', () => { diff --git a/src/type/defaultValues.d.ts b/src/type/defaultValues.d.ts new file mode 100644 index 00000000000..363e1534862 --- /dev/null +++ b/src/type/defaultValues.d.ts @@ -0,0 +1,13 @@ +import { ConstValueNode } from '../language/ast'; + +import { GraphQLInputType, GraphQLDefaultValueUsage } from './definition'; + +export function getLiteralDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): ConstValueNode; + +export function getCoercedDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): unknown; diff --git a/src/type/defaultValues.js b/src/type/defaultValues.js new file mode 100644 index 00000000000..f5d1401e15c --- /dev/null +++ b/src/type/defaultValues.js @@ -0,0 +1,29 @@ +import { invariant } from '../jsutils/invariant'; +import type { ConstValueNode } from '../language/ast'; + +import { astFromValue } from '../utilities/astFromValue'; +import { valueFromAST } from '../utilities/valueFromAST'; + +import type { GraphQLInputType, GraphQLDefaultValueUsage } from './definition'; + +export function getLiteralDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): ConstValueNode { + if (!usage.literal) { + const literal = astFromValue(usage.value, type); + invariant(literal, 'Value cannot be converted to literal for this type'); + usage.literal = literal; + } + return usage.literal; +} + +export function getCoercedDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): mixed { + if (usage.value === undefined) { + usage.value = valueFromAST(usage.literal, type); + } + return usage.value; +} diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index f07202d91af..0acd9b3d4e7 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -23,6 +23,7 @@ import { FieldNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, ScalarTypeExtensionNode, UnionTypeExtensionNode, EnumTypeExtensionNode, @@ -575,12 +576,17 @@ export interface GraphQLInputValue { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: Maybe; deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; } +export interface GraphQLDefaultValueUsage { + value: unknown; + literal: Maybe; +} + export interface GraphQLInputValueConfig { description?: Maybe; type: GraphQLInputType; diff --git a/src/type/definition.js b/src/type/definition.js index c16433d5b31..1242c3d183c 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -41,6 +41,7 @@ import type { FieldNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, } from '../language/ast'; import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; @@ -971,11 +972,18 @@ export function defineInputValue( !('resolve' in config), `${name} has a resolve property, but inputs cannot define resolvers.`, ); + let defaultValue; + if (config.defaultValue !== undefined || config.defaultValueLiteral) { + 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, @@ -991,7 +999,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, @@ -1002,16 +1011,22 @@ export type GraphQLInputValue = {| name: string, description: ?string, type: GraphQLInputType, - defaultValue: mixed, + defaultValue: ?GraphQLDefaultValueUsage, deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; +export type GraphQLDefaultValueUsage = {| + value: mixed, + literal: ?ConstValueNode, +|}; + export type GraphQLInputValueConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + defaultValueLiteral?: ?ConstValueNode, deprecationReason?: ?string, extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, diff --git a/src/type/index.d.ts b/src/type/index.d.ts index 365256dfd1d..cd919929ad9 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -82,6 +82,7 @@ export { GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, @@ -117,6 +118,11 @@ export { GraphQLScalarLiteralParser, } from './definition'; +export { + getCoercedDefaultValue, + getLiteralDefaultValue, +} from './defaultValues'; + export { // Predicate isDirective, diff --git a/src/type/index.js b/src/type/index.js index 6bf0943f228..af5d0b553c9 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -136,6 +136,7 @@ export type { GraphQLArgument, GraphQLArgumentConfig, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, @@ -162,5 +163,10 @@ export type { GraphQLScalarLiteralParser, } from './definition'; +export { + getCoercedDefaultValue, + getLiteralDefaultValue, +} from './defaultValues'; + // Validate GraphQL schema. export { validateSchema, assertValidSchema } from './validate'; diff --git a/src/type/introspection.js b/src/type/introspection.js index f0bce5838a0..a47f0260722 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -3,7 +3,6 @@ import { invariant } from '../jsutils/invariant'; import { print } from '../language/printer'; import { DirectiveLocation } from '../language/directiveLocation'; -import { astFromValue } from '../utilities/astFromValue'; import type { GraphQLSchema } from './schema'; import type { GraphQLDirective } from './directives'; @@ -31,6 +30,7 @@ import { isNonNullType, isAbstractType, } from './definition'; +import { getLiteralDefaultValue } from './defaultValues'; export const __Schema: GraphQLObjectType = new GraphQLObjectType({ name: '__Schema', @@ -382,11 +382,15 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ type: GraphQLString, description: '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; - }, + resolve: (inputValue) => + inputValue.defaultValue + ? print( + getLiteralDefaultValue( + inputValue.defaultValue, + inputValue.type, + ), + ) + : null, }, isDeprecated: { type: new GraphQLNonNull(GraphQLBoolean), diff --git a/src/utilities/TypeInfo.js b/src/utilities/TypeInfo.js index 10225d17549..2455309867e 100644 --- a/src/utilities/TypeInfo.js +++ b/src/utilities/TypeInfo.js @@ -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; } @@ -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, ); diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 3641f00227e..99dc3fdb5ae 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -51,6 +51,8 @@ describe('astFromValue', () => { kind: 'BooleanValue', value: false, }); + + expect(astFromValue(null, NonNullBoolean)).to.equal(null); }); it('converts Int values to Int ASTs', () => { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index bc03e3245b6..9dcf0ab0fe4 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -438,6 +438,7 @@ describe('Type System: build schema from introspection', () => { } type Query { + defaultID(intArg: ID = "123"): String defaultInt(intArg: Int = 30): String defaultList(listArg: [Int] = [1, 2, 3]): String defaultObject(objArg: Geo = {lat: 37.485, lon: -122.148}): String @@ -592,6 +593,27 @@ describe('Type System: build schema from introspection', () => { expect(result.data).to.deep.equal({ foo: 'bar' }); }); + it('can use client schema for execution if resolvers are added', () => { + const schema = buildSchema(` + type Query { + foo(bar: String = "abc"): String + } + `); + + const introspection = introspectionFromSchema(schema); + const clientSchema = buildClientSchema(introspection); + + const QueryType: GraphQLObjectType = (clientSchema.getType('Query'): any); + QueryType.getFields().foo.resolve = (_value, args) => args.bar; + + const result = graphqlSync({ + schema: clientSchema, + source: '{ foo }', + }); + + expect(result.data).to.deep.equal({ foo: 'abc' }); + }); + it('can build invalid schema', () => { const schema = buildSchema('type Query', { assumeValid: true }); diff --git a/src/utilities/__tests__/valueFromAST-test.js b/src/utilities/__tests__/valueFromAST-test.js index a4443927770..187472d0d60 100644 --- a/src/utilities/__tests__/valueFromAST-test.js +++ b/src/utilities/__tests__/valueFromAST-test.js @@ -188,6 +188,21 @@ describe('valueFromAST', () => { ); }); + it('uses default values for unprovided fields', () => { + const type = new GraphQLInputObjectType({ + name: 'TestInput', + fields: { + int: { type: GraphQLInt, defaultValue: 42 }, + float: { + type: GraphQLFloat, + defaultValueLiteral: { kind: 'FloatValue', value: '3.14' }, + }, + }, + }); + + expectValueFrom('{}', type).to.deep.equal({ int: 42, float: 3.14 }); + }); + const testInputObj = new GraphQLInputObjectType({ name: 'TestInput', fields: { diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index 5621659f73a..af9b9c74a6d 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -3,7 +3,7 @@ import { invariant } from '../jsutils/invariant'; import { isObjectLike } from '../jsutils/isObjectLike'; import { isIterableObject } from '../jsutils/isIterableObject'; -import type { ValueNode } from '../language/ast'; +import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; import type { GraphQLInputType } from '../type/definition'; @@ -37,13 +37,15 @@ import { * | null | NullValue | * */ -export function astFromValue(value: mixed, type: GraphQLInputType): ?ValueNode { +export function astFromValue( + value: mixed, + type: GraphQLInputType, +): ?ConstValueNode { if (isNonNullType(type)) { - const astValue = astFromValue(value, type.ofType); - if (astValue?.kind === Kind.NULL) { + if (value === null) { return null; } - return astValue; + return astFromValue(value, type.ofType); } // only explicit null, not undefined, NaN diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 487ee6d16e2..29daeb282cb 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -3,7 +3,7 @@ import { devAssert } from '../jsutils/devAssert'; import { keyValMap } from '../jsutils/keyValMap'; import { isObjectLike } from '../jsutils/isObjectLike'; -import { parseValue } from '../language/parser'; +import { parseConstValue } from '../language/parser'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; import type { @@ -47,7 +47,6 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { valueFromAST } from './valueFromAST'; /** * Build a GraphQLSchema for use by client tools. @@ -367,14 +366,13 @@ export function buildClientSchema( ); } - const defaultValue = - inputValueIntrospection.defaultValue != null - ? valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) - : undefined; return { description: inputValueIntrospection.description, type, - defaultValue, + defaultValueLiteral: + inputValueIntrospection.defaultValue != null + ? parseConstValue(inputValueIntrospection.defaultValue) + : undefined, deprecationReason: inputValueIntrospection.deprecationReason, }; } diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js index 9c9546fbc2e..31896064f50 100644 --- a/src/utilities/coerceInputValue.js +++ b/src/utilities/coerceInputValue.js @@ -17,6 +17,7 @@ import { isListType, isNonNullType, } from '../type/definition'; +import { getCoercedDefaultValue } from '../type/defaultValues'; type OnErrorCB = ( path: $ReadOnlyArray, @@ -102,8 +103,11 @@ function coerceInputValueImpl( const fieldValue = inputValue[field.name]; if (fieldValue === undefined) { - if (field.defaultValue !== undefined) { - coercedValue[field.name] = field.defaultValue; + if (field.defaultValue) { + coercedValue[field.name] = getCoercedDefaultValue( + field.defaultValue, + field.type, + ); } else if (isNonNullType(field.type)) { const typeStr = inspect(field.type); onError( diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index a824f0fb97f..23c85774fa1 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -80,8 +80,6 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { valueFromAST } from './valueFromAST'; - type Options = {| ...GraphQLSchemaValidationOptions, @@ -496,7 +494,7 @@ export function extendSchemaImpl( configMap[node.name.value] = { type, description: node.description?.value, - defaultValue: valueFromAST(node.defaultValue, type), + defaultValueLiteral: node.defaultValue, deprecationReason: getDeprecationReason(node), astNode: node, }; diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index 08caec042e7..8e70b9a23ec 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -3,6 +3,7 @@ import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { naturalCompare } from '../jsutils/naturalCompare'; +import type { ConstValueNode } from '../language/ast'; import { print } from '../language/printer'; import { visit } from '../language/visitor'; @@ -10,7 +11,6 @@ import type { GraphQLSchema } from '../type/schema'; import type { GraphQLField, GraphQLType, - GraphQLInputType, GraphQLNamedType, GraphQLEnumType, GraphQLUnionType, @@ -31,8 +31,7 @@ import { isNamedType, isRequiredInput, } from '../type/definition'; - -import { astFromValue } from './astFromValue'; +import { getLiteralDefaultValue } from '../type/defaultValues'; export const BreakingChangeType = Object.freeze({ TYPE_REMOVED: 'TYPE_REMOVED', @@ -402,18 +401,23 @@ function findArgChanges( `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed type from ` + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); - } else if (oldArg.defaultValue !== undefined) { - if (newArg.defaultValue === undefined) { + } else if (oldArg.defaultValue) { + if (!newArg.defaultValue) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, description: `${oldType.name}.${oldField.name} arg ${oldArg.name} defaultValue was removed.`, }); } else { + const newArgDefaultValue = newArg.defaultValue; // 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); + const oldValueStr = stringifyValue( + getLiteralDefaultValue(oldArg.defaultValue, oldArg.type), + ); + const newValueStr = stringifyValue( + getLiteralDefaultValue(newArgDefaultValue, newArg.type), + ); if (oldValueStr !== newValueStr) { schemaChanges.push({ @@ -533,10 +537,7 @@ function typeKindName(type: GraphQLNamedType): string { invariant(false, 'Unexpected type: ' + inspect((type: empty))); } -function stringifyValue(value: mixed, type: GraphQLInputType): string { - const ast = astFromValue(value, type); - invariant(ast != null); - +function stringifyValue(ast: ConstValueNode): string { const sortedAST = visit(ast, { ObjectValue(objectNode) { // Make a copy since sort mutates array diff --git a/src/utilities/printSchema.js b/src/utilities/printSchema.js index cc41725368a..0e7e8cfaade 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -31,8 +31,7 @@ import { isEnumType, isInputObjectType, } from '../type/definition'; - -import { astFromValue } from './astFromValue'; +import { getLiteralDefaultValue } from '../type/defaultValues'; export function printSchema(schema: GraphQLSchema): string { return printFilteredSchema( @@ -258,10 +257,11 @@ function printArgs( } function printInputValue(arg: GraphQLInputField): string { - const defaultAST = astFromValue(arg.defaultValue, arg.type); let argDecl = arg.name + ': ' + String(arg.type); - if (defaultAST) { - argDecl += ` = ${print(defaultAST)}`; + if (arg.defaultValue) { + argDecl += ` = ${print( + getLiteralDefaultValue(arg.defaultValue, arg.type), + )}`; } return argDecl + printDeprecated(arg.deprecationReason); } diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 258976462b9..19081ea96e0 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -111,8 +111,13 @@ export function valueFromAST( for (const field of Object.values(type.getFields())) { const fieldNode = fieldNodes[field.name]; if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { - if (field.defaultValue !== undefined) { - coercedObj[field.name] = field.defaultValue; + if (field.defaultValue) { + // NOTE: This is an inlined version of "getCoercedDefaultValue" which + // cannot be used directly since it would cause a circular dependency. + coercedObj[field.name] = + field.defaultValue.value !== undefined + ? field.defaultValue.value + : valueFromAST(field.defaultValue.literal, field.type); } else if (isNonNullType(field.type)) { return; // Invalid: intentionally return no value. } From 4171fc64a8650e234d213d12e072f78311212759 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Thu, 6 May 2021 00:47:39 -0700 Subject: [PATCH 16/16] Add valueToLiteral() and literalToValue() * Adds `valueToLiteral()` which takes an external value and translates it to a literal, allowing for custom scalars to define this behavior. * Adds `literalToValue()` which does the same in reverse, also allowing custom scalars to define it. * Deprecates `valueFromASTUntyped()` in favor of `literalToValue()`, replacing all use of it in the codebase and adding a printed warning (via a new `deprecationWarning` method). This also adds important changes to Input Coercion, especially for custom scalars: * The value provided to `parseLiteral` is now `ConstValueNode` and the second `variables` argument has been removed. For all built-in scalars this has no effect, but any custom scalars which use complex literals no longer need to do variable reconciliation manually (in fact most do not -- this has been an easy subtle bug to miss). This behavior is possible with the addition of `replaceASTVariables` * The `parseLiteral` function is no longer filled with a default implementation. Callsites need to check for it before calling it. This untangles what would otherwise be a circular dependency. Both callsites are updated. --- src/index.d.ts | 7 + src/index.js | 7 + src/jsutils/deprecationWarning.js | 15 + src/type/__tests__/definition-test.js | 23 -- src/type/__tests__/scalars-test.js | 32 +- src/type/definition.d.ts | 29 +- src/type/definition.js | 44 ++- src/type/scalars.js | 16 + src/utilities/__tests__/expectWarning.js | 17 ++ .../__tests__/literalToValue-test.js | 146 +++++++++ .../__tests__/replaceASTVariables-test.js | 52 ++++ src/utilities/__tests__/valueFromAST-test.js | 14 + .../__tests__/valueFromASTUntyped-test.js | 10 + .../__tests__/valueToLiteral-test.js | 283 ++++++++++++++++++ src/utilities/index.d.ts | 10 + src/utilities/index.js | 10 + src/utilities/literalToValue.d.ts | 24 ++ src/utilities/literalToValue.js | 79 +++++ src/utilities/replaceASTVariables.d.ts | 16 + src/utilities/replaceASTVariables.js | 36 +++ src/utilities/valueFromAST.js | 9 +- src/utilities/valueFromASTUntyped.d.ts | 1 + src/utilities/valueFromASTUntyped.js | 4 + src/utilities/valueToLiteral.d.ts | 28 ++ src/utilities/valueToLiteral.js | 97 ++++++ .../rules/ValuesOfCorrectTypeRule.js | 11 +- 26 files changed, 951 insertions(+), 69 deletions(-) create mode 100644 src/jsutils/deprecationWarning.js create mode 100644 src/utilities/__tests__/expectWarning.js create mode 100644 src/utilities/__tests__/literalToValue-test.js create mode 100644 src/utilities/__tests__/replaceASTVariables-test.js create mode 100644 src/utilities/__tests__/valueToLiteral-test.js create mode 100644 src/utilities/literalToValue.d.ts create mode 100644 src/utilities/literalToValue.js create mode 100644 src/utilities/replaceASTVariables.d.ts create mode 100644 src/utilities/replaceASTVariables.js create mode 100644 src/utilities/valueToLiteral.d.ts create mode 100644 src/utilities/valueToLiteral.js diff --git a/src/index.d.ts b/src/index.d.ts index db8241b04e2..2f42a26ffda 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -413,6 +413,7 @@ export { // Create a JavaScript value from a GraphQL language AST with a Type. valueFromAST, // Create a JavaScript value from a GraphQL language AST without a Type. + // DEPRECATED: use literalToValue valueFromASTUntyped, // Create a GraphQL language AST from a JavaScript value. astFromValue, @@ -420,6 +421,12 @@ export { // the GraphQL type system. TypeInfo, visitWithTypeInfo, + // Converts a value to a const value by replacing variables. + replaceASTVariables, + // Create a GraphQL Literal AST from a JavaScript input value. + valueToLiteral, + // Create a JavaScript input value from a GraphQL Literal AST. + literalToValue, // Coerces a JavaScript value to a GraphQL type, or produces errors. coerceInputValue, // Concatenates multiple AST together. diff --git a/src/index.js b/src/index.js index 71dbddf0fa3..365bf332acc 100644 --- a/src/index.js +++ b/src/index.js @@ -402,6 +402,7 @@ export { // Create a JavaScript value from a GraphQL language AST with a Type. valueFromAST, // Create a JavaScript value from a GraphQL language AST without a Type. + // DEPRECATED: use literalToValue valueFromASTUntyped, // Create a GraphQL language AST from a JavaScript value. astFromValue, @@ -409,6 +410,12 @@ export { // the GraphQL type system. TypeInfo, visitWithTypeInfo, + // Converts a value to a const value by replacing variables. + replaceASTVariables, + // Create a GraphQL Literal AST from a JavaScript input value. + valueToLiteral, + // Create a JavaScript input value from a GraphQL Literal AST. + literalToValue, // Coerces a JavaScript value to a GraphQL type, or produces errors. coerceInputValue, // Concatenates multiple AST together. diff --git a/src/jsutils/deprecationWarning.js b/src/jsutils/deprecationWarning.js new file mode 100644 index 00000000000..f0c87bfc1b8 --- /dev/null +++ b/src/jsutils/deprecationWarning.js @@ -0,0 +1,15 @@ +/* eslint-disable no-console */ +const canWarn = console && console.warn; +const hasIssuedWarning = {}; + +export function deprecationWarning( + deprecatedFunction: string, + resolution: string, +): void { + if (canWarn && !hasIssuedWarning[deprecatedFunction]) { + hasIssuedWarning[deprecatedFunction] = true; + console.warn( + `DEPRECATION WARNING: The function "${deprecatedFunction}" is deprecated and may be removed in a future version. ${resolution}`, + ); + } +} diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 809e7007d34..5cd563c8e69 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -1,11 +1,8 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { inspect } from '../../jsutils/inspect'; import { identityFunc } from '../../jsutils/identityFunc'; -import { parseValue } from '../../language/parser'; - import type { GraphQLType, GraphQLNullableType } from '../definition'; import { GraphQLList, @@ -72,26 +69,6 @@ describe('Type System: Scalars', () => { expect(scalar.serialize).to.equal(identityFunc); expect(scalar.parseValue).to.equal(identityFunc); - expect(scalar.parseLiteral).to.be.a('function'); - }); - - it('use parseValue for parsing literals if parseLiteral omitted', () => { - const scalar = new GraphQLScalarType({ - name: 'Foo', - parseValue(value) { - return 'parseValue: ' + inspect(value); - }, - }); - - expect(scalar.parseLiteral(parseValue('null'))).to.equal( - 'parseValue: null', - ); - expect(scalar.parseLiteral(parseValue('{ foo: "bar" }'))).to.equal( - 'parseValue: { foo: "bar" }', - ); - expect( - scalar.parseLiteral(parseValue('{ foo: { bar: $var } }'), { var: 'baz' }), - ).to.equal('parseValue: { foo: { bar: "baz" } }'); }); it('rejects a Scalar type without name', () => { diff --git a/src/type/__tests__/scalars-test.js b/src/type/__tests__/scalars-test.js index 8eac0f6266e..e3a154a9403 100644 --- a/src/type/__tests__/scalars-test.js +++ b/src/type/__tests__/scalars-test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { parseValue as parseValueToAST } from '../../language/parser'; +import { parseConstValue } from '../../language/parser'; import { GraphQLID, @@ -66,7 +66,8 @@ describe('Type System: Specified scalar types', () => { it('parseLiteral', () => { function parseLiteral(str: string) { - return GraphQLInt.parseLiteral(parseValueToAST(str), undefined); + // $FlowExpectedError[not-a-function] + return GraphQLInt.parseLiteral(parseConstValue(str)); } expect(parseLiteral('1')).to.equal(1); @@ -104,9 +105,6 @@ describe('Type System: Specified scalar types', () => { expect(() => parseLiteral('ENUM_VALUE')).to.throw( 'Int cannot represent non-integer value: ENUM_VALUE', ); - expect(() => parseLiteral('$var')).to.throw( - 'Int cannot represent non-integer value: $var', - ); }); it('serialize', () => { @@ -231,7 +229,8 @@ describe('Type System: Specified scalar types', () => { it('parseLiteral', () => { function parseLiteral(str: string) { - return GraphQLFloat.parseLiteral(parseValueToAST(str), undefined); + // $FlowExpectedError[not-a-function] + return GraphQLFloat.parseLiteral(parseConstValue(str)); } expect(parseLiteral('1')).to.equal(1); @@ -264,9 +263,6 @@ describe('Type System: Specified scalar types', () => { expect(() => parseLiteral('ENUM_VALUE')).to.throw( 'Float cannot represent non numeric value: ENUM_VALUE', ); - expect(() => parseLiteral('$var')).to.throw( - 'Float cannot represent non numeric value: $var', - ); }); it('serialize', () => { @@ -344,7 +340,8 @@ describe('Type System: Specified scalar types', () => { it('parseLiteral', () => { function parseLiteral(str: string) { - return GraphQLString.parseLiteral(parseValueToAST(str), undefined); + // $FlowExpectedError[not-a-function] + return GraphQLString.parseLiteral(parseConstValue(str)); } expect(parseLiteral('"foo"')).to.equal('foo'); @@ -371,9 +368,6 @@ describe('Type System: Specified scalar types', () => { expect(() => parseLiteral('ENUM_VALUE')).to.throw( 'String cannot represent a non string value: ENUM_VALUE', ); - expect(() => parseLiteral('$var')).to.throw( - 'String cannot represent a non string value: $var', - ); }); it('serialize', () => { @@ -456,7 +450,8 @@ describe('Type System: Specified scalar types', () => { it('parseLiteral', () => { function parseLiteral(str: string) { - return GraphQLBoolean.parseLiteral(parseValueToAST(str), undefined); + // $FlowExpectedError[not-a-function] + return GraphQLBoolean.parseLiteral(parseConstValue(str)); } expect(parseLiteral('true')).to.equal(true); @@ -489,9 +484,6 @@ describe('Type System: Specified scalar types', () => { expect(() => parseLiteral('ENUM_VALUE')).to.throw( 'Boolean cannot represent a non boolean value: ENUM_VALUE', ); - expect(() => parseLiteral('$var')).to.throw( - 'Boolean cannot represent a non boolean value: $var', - ); }); it('serialize', () => { @@ -571,7 +563,8 @@ describe('Type System: Specified scalar types', () => { it('parseLiteral', () => { function parseLiteral(str: string) { - return GraphQLID.parseLiteral(parseValueToAST(str), undefined); + // $FlowExpectedError[not-a-function] + return GraphQLID.parseLiteral(parseConstValue(str)); } expect(parseLiteral('""')).to.equal(''); @@ -604,9 +597,6 @@ describe('Type System: Specified scalar types', () => { expect(() => parseLiteral('ENUM_VALUE')).to.throw( 'ID cannot represent a non-string and non-integer value: ENUM_VALUE', ); - expect(() => parseLiteral('$var')).to.throw( - 'ID cannot represent a non-string and non-integer value: $var', - ); }); it('serialize', () => { diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 0acd9b3d4e7..924ade4d28a 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -22,7 +22,6 @@ import { OperationDefinitionNode, FieldNode, FragmentDefinitionNode, - ValueNode, ConstValueNode, ScalarTypeExtensionNode, UnionTypeExtensionNode, @@ -316,7 +315,9 @@ export class GraphQLScalarType { specifiedByURL: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: Maybe>; + valueToLiteral: Maybe; + literalToValue: Maybe; extensions: Maybe>; astNode: Maybe; extensionASTNodes: ReadonlyArray; @@ -327,7 +328,9 @@ export class GraphQLScalarType { specifiedByURL: Maybe; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: Maybe>; + valueToLiteral: Maybe; + literalToValue: Maybe; extensions: Maybe>; extensionASTNodes: ReadonlyArray; }; @@ -344,9 +347,14 @@ export type GraphQLScalarValueParser = ( value: unknown, ) => Maybe; export type GraphQLScalarLiteralParser = ( - valueNode: ValueNode, - variables: Maybe>, + valueNode: ConstValueNode, ) => Maybe; +export type GraphQLScalarValueToLiteral = ( + inputValue: unknown, +) => Maybe; +export type GraphQLScalarLiteralToValue = ( + valueNode: ConstValueNode, +) => unknown; export interface GraphQLScalarTypeConfig { name: string; @@ -358,6 +366,10 @@ export interface GraphQLScalarTypeConfig { parseValue?: GraphQLScalarValueParser; // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser; + // Translates an external input value to a literal (AST). + valueToLiteral?: Maybe; + // Translates a literal (AST) to external input value. + literalToValue?: Maybe; extensions?: Maybe>; astNode?: Maybe; extensionASTNodes?: Maybe>; @@ -788,10 +800,9 @@ export class GraphQLEnumType { getValue(name: string): Maybe; serialize(value: unknown): Maybe; parseValue(value: unknown): Maybe; - parseLiteral( - valueNode: ValueNode, - _variables: Maybe>, - ): Maybe; + parseLiteral(valueNode: ConstValueNode): Maybe; + valueToLiteral(value: unknown): Maybe; + literalToValue(valueNode: ConstValueNode): unknown; toConfig(): GraphQLEnumTypeConfig & { extensions: Maybe>; diff --git a/src/type/definition.js b/src/type/definition.js index 1242c3d183c..68c4c2baaac 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -40,12 +40,9 @@ import type { OperationDefinitionNode, FieldNode, FragmentDefinitionNode, - ValueNode, ConstValueNode, } from '../language/ast'; -import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; - import type { GraphQLSchema } from './schema'; // Predicates & Assertions @@ -561,7 +558,9 @@ export class GraphQLScalarType { specifiedByURL: ?string; serialize: GraphQLScalarSerializer; parseValue: GraphQLScalarValueParser; - parseLiteral: GraphQLScalarLiteralParser; + parseLiteral: ?GraphQLScalarLiteralParser; + valueToLiteral: ?GraphQLScalarValueToLiteral; + literalToValue: ?GraphQLScalarLiteralToValue; extensions: ?ReadOnlyObjMap; astNode: ?ScalarTypeDefinitionNode; extensionASTNodes: $ReadOnlyArray; @@ -573,9 +572,9 @@ export class GraphQLScalarType { this.specifiedByURL = config.specifiedByURL; this.serialize = config.serialize ?? identityFunc; this.parseValue = parseValue; - this.parseLiteral = - config.parseLiteral ?? - ((node, variables) => parseValue(valueFromASTUntyped(node, variables))); + this.parseLiteral = config.parseLiteral; + this.valueToLiteral = config.valueToLiteral; + this.literalToValue = config.literalToValue; this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; @@ -611,6 +610,8 @@ export class GraphQLScalarType { serialize: this.serialize, parseValue: this.parseValue, parseLiteral: this.parseLiteral, + valueToLiteral: this.valueToLiteral, + literalToValue: this.literalToValue, extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, @@ -640,10 +641,15 @@ export type GraphQLScalarValueParser = ( ) => ?TInternal; export type GraphQLScalarLiteralParser = ( - valueNode: ValueNode, - variables: ?ObjMap, + valueNode: ConstValueNode, ) => ?TInternal; +export type GraphQLScalarValueToLiteral = ( + inputValue: mixed, +) => ?ConstValueNode; + +export type GraphQLScalarLiteralToValue = (valueNode: ConstValueNode) => mixed; + export type GraphQLScalarTypeConfig = {| name: string, description?: ?string, @@ -653,7 +659,11 @@ export type GraphQLScalarTypeConfig = {| // Parses an externally provided value to use as an input. parseValue?: GraphQLScalarValueParser, // Parses an externally provided literal value to use as an input. - parseLiteral?: GraphQLScalarLiteralParser, + parseLiteral?: ?GraphQLScalarLiteralParser, + // Translates an external input value to a literal (AST). + valueToLiteral?: ?GraphQLScalarValueToLiteral, + // Translates a literal (AST) to external input value. + literalToValue?: ?GraphQLScalarLiteralToValue, extensions?: ?ReadOnlyObjMapLike, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, @@ -663,7 +673,6 @@ type GraphQLScalarTypeNormalizedConfig = {| ...GraphQLScalarTypeConfig, serialize: GraphQLScalarSerializer, parseValue: GraphQLScalarValueParser, - parseLiteral: GraphQLScalarLiteralParser, extensions: ?ReadOnlyObjMap, extensionASTNodes: $ReadOnlyArray, |}; @@ -1345,8 +1354,7 @@ export class GraphQLEnumType /* */ { return enumValue.value; } - parseLiteral(valueNode: ValueNode, _variables: ?ObjMap): ?any /* T */ { - // Note: variables will be resolved to a value before calling this function. + parseLiteral(valueNode: ConstValueNode): ?any /* T */ { if (valueNode.kind !== Kind.ENUM) { const valueStr = print(valueNode); throw new GraphQLError( @@ -1368,6 +1376,16 @@ export class GraphQLEnumType /* */ { return enumValue.value; } + valueToLiteral(value: mixed): ?ConstValueNode { + if (typeof value === 'string') { + // https://spec.graphql.org/draft/#Name + if (/^[_a-zA-Z][_a-zA-Z0-9]*$/.test(value)) { + return { kind: Kind.ENUM, value }; + } + return { kind: Kind.STRING, value }; + } + } + toConfig(): GraphQLEnumTypeNormalizedConfig { const values = keyValMap( this.getValues(), diff --git a/src/type/scalars.js b/src/type/scalars.js index 0d9bdc955b8..1286e2ce84a 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -1,6 +1,7 @@ import { inspect } from '../jsutils/inspect'; import { isObjectLike } from '../jsutils/isObjectLike'; +import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; import { print } from '../language/printer'; @@ -268,6 +269,21 @@ export const GraphQLID: GraphQLScalarType = new GraphQLScalarType({ } return valueNode.value; }, + valueToLiteral(value: mixed): ?ConstValueNode { + // ID types can use Int literals. + if (typeof value === 'string') { + if (/^-?(?:0|[1-9][0-9]*)$/.test(value)) { + return { kind: Kind.INT, value }; + } + return { kind: Kind.STRING, value }; + } + }, + literalToValue(valueNode: ConstValueNode): mixed { + // ID Int literals are represented as string values. + if (valueNode.kind === Kind.INT || valueNode.kind === Kind.STRING) { + return valueNode.value; + } + }, }); export const specifiedScalarTypes: $ReadOnlyArray = Object.freeze( diff --git a/src/utilities/__tests__/expectWarning.js b/src/utilities/__tests__/expectWarning.js new file mode 100644 index 00000000000..f581eed41f1 --- /dev/null +++ b/src/utilities/__tests__/expectWarning.js @@ -0,0 +1,17 @@ +import { expect } from 'chai'; + +export function expectWarning(callback: () => any): any { + const _console = console; + try { + let warnCallArg; + global.console = { + warn(arg) { + warnCallArg = arg; + }, + }; + callback(); + return expect(warnCallArg); + } finally { + global.console = _console; + } +} diff --git a/src/utilities/__tests__/literalToValue-test.js b/src/utilities/__tests__/literalToValue-test.js new file mode 100644 index 00000000000..8f6f60c37c4 --- /dev/null +++ b/src/utilities/__tests__/literalToValue-test.js @@ -0,0 +1,146 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { + GraphQLID, + GraphQLInt, + GraphQLFloat, + GraphQLBoolean, +} from '../../type/scalars'; +import { + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../../type/definition'; + +import { parseValue, parseConstValue } from '../../language/parser'; +import { literalToValue } from '../literalToValue'; +import { Kind } from '../../language/kinds'; + +describe('literalToValue', () => { + it('does not allow variables', () => { + // $FlowExpectedError[incompatible-call] + expect(() => literalToValue(parseValue('$var'))).to.throw('Unexpected'); + }); + + it('converts null ASTs to values', () => { + expect(literalToValue(parseConstValue('null'))).to.equal(null); + }); + + it('converts boolean ASTs to values', () => { + expect(literalToValue(parseConstValue('true'))).to.equal(true); + expect(literalToValue(parseConstValue('false'))).to.equal(false); + }); + + it('converts Int ASTs to Int values', () => { + expect(literalToValue(parseConstValue('0'))).to.equal(0); + expect(literalToValue(parseConstValue('-1'))).to.equal(-1); + expect(literalToValue(parseConstValue('1000'))).to.equal(1000); + }); + + it('converts Float ASTs to Float values', () => { + expect(literalToValue(parseConstValue('123.5'))).to.equal(123.5); + expect(literalToValue(parseConstValue('2e40'))).to.equal(2e40); + }); + + it('converts String ASTs to String values', () => { + expect(literalToValue(parseConstValue('"hello world"'))).to.equal( + 'hello world', + ); + expect(literalToValue(parseConstValue('"NAME"'))).to.equal('NAME'); + }); + + it('does not do type checking or coercion', () => { + expect(literalToValue(parseConstValue('0'), GraphQLBoolean)).to.equal(0); + expect(literalToValue(parseConstValue('1.23'), GraphQLInt)).to.equal(1.23); + expect(literalToValue(parseConstValue('"123"'), GraphQLInt)).to.equal( + '123', + ); + const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); + expect(literalToValue(parseConstValue('null'), NonNullBoolean)).to.equal( + null, + ); + }); + + it('converts ID Int/String ASTs to string values', () => { + expect( + literalToValue(parseConstValue('"hello world"'), GraphQLID), + ).to.equal('hello world'); + expect(literalToValue(parseConstValue('"NAME"'), GraphQLID)).to.equal( + 'NAME', + ); + expect(literalToValue(parseConstValue('123'), GraphQLID)).to.equal('123'); + expect(literalToValue(parseConstValue('"123"'), GraphQLID)).to.equal('123'); + expect(literalToValue(parseConstValue('123.0'), GraphQLID)).to.equal(123); + }); + + const myEnum = new GraphQLEnumType({ + name: 'MyEnum', + values: { + HELLO: {}, + COMPLEX: { value: { someArbitrary: 'complexValue' } }, + }, + }); + + it('converts Enum ASTs to string values', () => { + expect(literalToValue(parseConstValue('HELLO'))).to.equal('HELLO'); + expect(literalToValue(parseConstValue('HELLO'), myEnum)).to.equal('HELLO'); + expect(literalToValue(parseConstValue('COMPLEX'), myEnum)).to.equal( + 'COMPLEX', + ); + expect(literalToValue(parseConstValue('GOODBYE'), myEnum)).to.equal( + 'GOODBYE', + ); + }); + + it('converts List ASTs to array values', () => { + expect(literalToValue(parseConstValue('["FOO", BAR]'))).to.deep.equal([ + 'FOO', + 'BAR', + ]); + + expect( + literalToValue( + parseConstValue('["123", 123]'), + new GraphQLList(GraphQLID), + ), + ).to.deep.equal(['123', '123']); + }); + + const inputObj = new GraphQLInputObjectType({ + name: 'MyInputObj', + fields: { + foo: { type: GraphQLFloat }, + bar: { type: GraphQLID }, + }, + }); + + it('converts input objects', () => { + expect( + literalToValue(parseConstValue('{ foo: 3, bar: 3 }'), inputObj), + ).to.deep.equal({ + foo: 3, + bar: '3', + }); + }); + + it('custom scalar types may define literalToValue', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + literalToValue(value) { + if (value.kind === Kind.ENUM) { + return '#' + value.value; + } + }, + }); + + expect(literalToValue(parseConstValue('FOO'), customScalar)).to.equal( + '#FOO', + ); + expect(literalToValue(parseConstValue('"FOO"'), customScalar)).to.equal( + 'FOO', + ); + }); +}); diff --git a/src/utilities/__tests__/replaceASTVariables-test.js b/src/utilities/__tests__/replaceASTVariables-test.js new file mode 100644 index 00000000000..7b620e34fe1 --- /dev/null +++ b/src/utilities/__tests__/replaceASTVariables-test.js @@ -0,0 +1,52 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import type { ValueNode } from '../../language/ast'; +import { parseValue as _parseValue } from '../../language/parser'; +import { replaceASTVariables } from '../replaceASTVariables'; + +function parseValue(ast: string): ValueNode { + return _parseValue(ast, { noLocation: true }); +} + +describe('replaceASTVariables', () => { + it('does not change simple AST', () => { + const ast = parseValue('null'); + expect(replaceASTVariables(ast, undefined)).to.equal(ast); + }); + + it('replaces simple Variables', () => { + const ast = parseValue('$var'); + expect(replaceASTVariables(ast, { var: 123 })).to.deep.equal( + parseValue('123'), + ); + }); + + it('replaces nested Variables', () => { + const ast = parseValue('{ foo: [ $var ], bar: $var }'); + expect(replaceASTVariables(ast, { var: 123 })).to.deep.equal( + parseValue('{ foo: [ 123 ], bar: 123 }'), + ); + }); + + it('replaces missing Variables with null', () => { + const ast = parseValue('$var'); + expect(replaceASTVariables(ast, undefined)).to.deep.equal( + parseValue('null'), + ); + }); + + it('replaces missing Variables in lists with null', () => { + const ast = parseValue('[1, $var]'); + expect(replaceASTVariables(ast, undefined)).to.deep.equal( + parseValue('[1, null]'), + ); + }); + + it('omits missing Variables from objects', () => { + const ast = parseValue('{ foo: 1, bar: $var }'); + expect(replaceASTVariables(ast, undefined)).to.deep.equal( + parseValue('{ foo: 1 }'), + ); + }); +}); diff --git a/src/utilities/__tests__/valueFromAST-test.js b/src/utilities/__tests__/valueFromAST-test.js index 187472d0d60..e88638eaaa3 100644 --- a/src/utilities/__tests__/valueFromAST-test.js +++ b/src/utilities/__tests__/valueFromAST-test.js @@ -5,6 +5,7 @@ import type { ObjMap } from '../../jsutils/ObjMap'; import { invariant } from '../../jsutils/invariant'; import { identityFunc } from '../../jsutils/identityFunc'; +import { print } from '../../language/printer'; import { parseValue } from '../../language/parser'; import type { GraphQLInputType } from '../../type/definition'; @@ -74,6 +75,19 @@ describe('valueFromAST', () => { expectValueFrom('"value"', passthroughScalar).to.equal('value'); + const graphqlScalar = new GraphQLScalarType({ + name: 'GraphQLScalar', + parseLiteral(node) { + return print(node); + }, + parseValue: identityFunc, + }); + + expectValueFrom('"value"', graphqlScalar).to.equal('"value"'); + expectValueFrom('{field: $var}', graphqlScalar, { var: 'value' }).to.equal( + '{field: "value"}', + ); + const throwScalar = new GraphQLScalarType({ name: 'ThrowScalar', parseLiteral() { diff --git a/src/utilities/__tests__/valueFromASTUntyped-test.js b/src/utilities/__tests__/valueFromASTUntyped-test.js index 5e971a43f61..5c210cf3683 100644 --- a/src/utilities/__tests__/valueFromASTUntyped-test.js +++ b/src/utilities/__tests__/valueFromASTUntyped-test.js @@ -1,3 +1,5 @@ +/* eslint-disable import/no-deprecated */ + import { expect } from 'chai'; import { describe, it } from 'mocha'; @@ -7,6 +9,8 @@ import { parseValue } from '../../language/parser'; import { valueFromASTUntyped } from '../valueFromASTUntyped'; +import { expectWarning } from './expectWarning'; + describe('valueFromASTUntyped', () => { function expectValueFrom(valueText: string, variables?: ?ObjMap) { const ast = parseValue(valueText); @@ -14,6 +18,12 @@ describe('valueFromASTUntyped', () => { return expect(value); } + it('warns about deprecation', () => { + expectWarning(() => valueFromASTUntyped(parseValue('true'))).to.equal( + 'DEPRECATION WARNING: The function "valueFromASTUntyped" is deprecated and may be removed in a future version. Use "literalToValue".', + ); + }); + it('parses simple values', () => { expectValueFrom('null').to.equal(null); expectValueFrom('true').to.equal(true); diff --git a/src/utilities/__tests__/valueToLiteral-test.js b/src/utilities/__tests__/valueToLiteral-test.js new file mode 100644 index 00000000000..f565f85f014 --- /dev/null +++ b/src/utilities/__tests__/valueToLiteral-test.js @@ -0,0 +1,283 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { + GraphQLID, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLBoolean, +} from '../../type/scalars'; +import { + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../../type/definition'; + +import { valueToLiteral } from '../valueToLiteral'; + +describe('valueToLiteral', () => { + it('does not convert some value types', () => { + expect(() => valueToLiteral(Symbol('test'))).to.throw( + 'Cannot convert value to AST: Symbol(test).', + ); + }); + + it('converts null values to ASTs', () => { + expect(valueToLiteral(null)).to.deep.equal({ + kind: 'NullValue', + }); + + // Note: undefined values are represented as null. + expect(valueToLiteral(undefined)).to.deep.equal({ + kind: 'NullValue', + }); + }); + + it('converts boolean values to ASTs', () => { + expect(valueToLiteral(true)).to.deep.equal({ + kind: 'BooleanValue', + value: true, + }); + + expect(valueToLiteral(false)).to.deep.equal({ + kind: 'BooleanValue', + value: false, + }); + }); + + it('converts non-finite values to null', () => { + expect(valueToLiteral(NaN)).to.deep.equal({ + kind: 'NullValue', + }); + + expect(valueToLiteral(Infinity)).to.deep.equal({ + kind: 'NullValue', + }); + }); + + it('converts Int values to Int ASTs', () => { + expect(valueToLiteral(-1)).to.deep.equal({ + kind: 'IntValue', + value: '-1', + }); + + expect(valueToLiteral(123.0)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); + + expect(valueToLiteral(1e4)).to.deep.equal({ + kind: 'IntValue', + value: '10000', + }); + }); + + it('converts Float values to Int/Float ASTs', () => { + expect(valueToLiteral(123.5)).to.deep.equal({ + kind: 'FloatValue', + value: '123.5', + }); + + expect(valueToLiteral(1e40)).to.deep.equal({ + kind: 'FloatValue', + value: '1e+40', + }); + }); + + it('converts String values to String ASTs', () => { + expect(valueToLiteral('hello world')).to.deep.equal({ + kind: 'StringValue', + value: 'hello world', + }); + + expect(valueToLiteral('NAME')).to.deep.equal({ + kind: 'StringValue', + value: 'NAME', + }); + }); + + it('converts ID values to Int/String ASTs', () => { + expect(valueToLiteral('hello world', GraphQLID)).to.deep.equal({ + kind: 'StringValue', + value: 'hello world', + }); + + expect(valueToLiteral('NAME', GraphQLID)).to.deep.equal({ + kind: 'StringValue', + value: 'NAME', + }); + + expect(valueToLiteral(123, GraphQLID)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); + + expect(valueToLiteral('123', GraphQLID)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); + + expect(valueToLiteral('123.5', GraphQLID)).to.deep.equal({ + kind: 'StringValue', + value: '123.5', + }); + + expect(valueToLiteral('001', GraphQLID)).to.deep.equal({ + kind: 'StringValue', + value: '001', + }); + }); + + const myEnum = new GraphQLEnumType({ + name: 'MyEnum', + values: { + HELLO: {}, + COMPLEX: { value: { someArbitrary: 'complexValue' } }, + }, + }); + + it('converts string values to Enum ASTs if possible', () => { + expect(valueToLiteral('HELLO', myEnum)).to.deep.equal({ + kind: 'EnumValue', + value: 'HELLO', + }); + + expect(valueToLiteral('COMPLEX', myEnum)).to.deep.equal({ + kind: 'EnumValue', + value: 'COMPLEX', + }); + + expect(valueToLiteral('GOODBYE', myEnum)).to.deep.equal({ + kind: 'EnumValue', + value: 'GOODBYE', + }); + + // Non-names are string value + expect(valueToLiteral('hello friend', myEnum)).to.deep.equal({ + kind: 'StringValue', + value: 'hello friend', + }); + }); + + it('does not do type checking or coercion', () => { + expect(valueToLiteral(0, GraphQLBoolean)).to.deep.equal({ + kind: 'IntValue', + value: '0', + }); + + expect(valueToLiteral(1.23, GraphQLInt)).to.deep.equal({ + kind: 'FloatValue', + value: '1.23', + }); + + expect(valueToLiteral('123', GraphQLInt)).to.deep.equal({ + kind: 'StringValue', + value: '123', + }); + + const NonNullBoolean = new GraphQLNonNull(GraphQLBoolean); + expect(valueToLiteral(null, NonNullBoolean)).to.deep.equal({ + kind: 'NullValue', + }); + + expect(valueToLiteral(123, myEnum)).to.deep.equal({ + kind: 'IntValue', + value: '123', + }); + }); + + it('converts array values to List ASTs', () => { + expect( + valueToLiteral(['FOO', 'BAR'], new GraphQLList(GraphQLString)), + ).to.deep.equal({ + kind: 'ListValue', + values: [ + { kind: 'StringValue', value: 'FOO' }, + { kind: 'StringValue', value: 'BAR' }, + ], + }); + + expect( + valueToLiteral(['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( + valueToLiteral(listGenerator(), new GraphQLList(GraphQLInt)), + ).to.deep.equal({ + kind: 'ListValue', + values: [ + { kind: 'IntValue', value: '1' }, + { kind: 'IntValue', value: '2' }, + { kind: 'IntValue', value: '3' }, + ], + }); + }); + + const inputObj = new GraphQLInputObjectType({ + name: 'MyInputObj', + fields: { + foo: { type: GraphQLFloat }, + bar: { type: myEnum }, + }, + }); + + it('converts input objects', () => { + expect(valueToLiteral({ 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, omitting undefined', () => { + expect(valueToLiteral({ foo: null, bar: undefined })).to.deep.equal({ + kind: 'ObjectValue', + fields: [ + { + kind: 'ObjectField', + name: { kind: 'Name', value: 'foo' }, + value: { kind: 'NullValue' }, + }, + ], + }); + }); + + it('custom scalar types may define valueToLiteral', () => { + const customScalar = new GraphQLScalarType({ + name: 'CustomScalar', + valueToLiteral(value) { + return { kind: 'StringValue', value: String(value) }; + }, + }); + + expect(valueToLiteral(123, customScalar)).to.deep.equal({ + kind: 'StringValue', + value: '123', + }); + }); +}); diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index a690e640e95..4328864c888 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -65,6 +65,7 @@ export { typeFromAST } from './typeFromAST'; export { valueFromAST } from './valueFromAST'; // Create a JavaScript value from a GraphQL language AST without a type. +// DEPRECATED: use literalToValue export { valueFromASTUntyped } from './valueFromASTUntyped'; // Create a GraphQL language AST from a JavaScript value. @@ -74,6 +75,15 @@ export { astFromValue } from './astFromValue'; // the GraphQL type system. export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; +// Converts a value to a const value by replacing variables. +export { replaceASTVariables } from './replaceASTVariables'; + +// Create a GraphQL Literal AST from a JavaScript input value. +export { valueToLiteral } from './valueToLiteral'; + +// Create a JavaScript input value from a GraphQL Literal AST. +export { literalToValue } from './literalToValue'; + // Coerces a JavaScript value to a GraphQL type, or produces errors. export { coerceInputValue } from './coerceInputValue'; diff --git a/src/utilities/index.js b/src/utilities/index.js index b4c8372f04e..fcdc68941db 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -63,6 +63,7 @@ export { typeFromAST } from './typeFromAST'; export { valueFromAST } from './valueFromAST'; // Create a JavaScript value from a GraphQL language AST without a type. +// DEPRECATED: use literalToValue export { valueFromASTUntyped } from './valueFromASTUntyped'; // Create a GraphQL language AST from a JavaScript value. @@ -72,6 +73,15 @@ export { astFromValue } from './astFromValue'; // the GraphQL type system. export { TypeInfo, visitWithTypeInfo } from './TypeInfo'; +// Converts a value to a const value by replacing variables. +export { replaceASTVariables } from './replaceASTVariables'; + +// Create a GraphQL Literal AST from a JavaScript input value. +export { valueToLiteral } from './valueToLiteral'; + +// Create a JavaScript input value from a GraphQL Literal AST. +export { literalToValue } from './literalToValue'; + // Coerces a JavaScript value to a GraphQL type, or produces errors. export { coerceInputValue } from './coerceInputValue'; diff --git a/src/utilities/literalToValue.d.ts b/src/utilities/literalToValue.d.ts new file mode 100644 index 00000000000..bd52e6cdcac --- /dev/null +++ b/src/utilities/literalToValue.d.ts @@ -0,0 +1,24 @@ +import { ConstValueNode } from '../language/ast'; +import { GraphQLInputType } from '../type/definition'; + +/** + * Produces a JavaScript value given a GraphQL Value AST. + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `literalToValue` method. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + * Note: This function does not perform any type validation or coercion. + */ +export function literalToValue( + valueNode: ConstValueNode, + type?: GraphQLInputType, +): unknown; diff --git a/src/utilities/literalToValue.js b/src/utilities/literalToValue.js new file mode 100644 index 00000000000..3a2bd58a339 --- /dev/null +++ b/src/utilities/literalToValue.js @@ -0,0 +1,79 @@ +import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; +import { keyValMap } from '../jsutils/keyValMap'; + +import { Kind } from '../language/kinds'; +import type { ConstValueNode } from '../language/ast'; +import type { GraphQLInputType } from '../type/definition'; +import { + getNamedType, + isScalarType, + isInputObjectType, +} from '../type/definition'; + +/** + * Produces a JavaScript value given a GraphQL Value AST. + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `literalToValue` method. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + * Note: This function does not perform any type validation or coercion. + */ +export function literalToValue( + valueNode: ConstValueNode, + type?: GraphQLInputType, +): mixed { + if (valueNode.kind === Kind.NULL) { + return null; + } + + const namedType = type && getNamedType(type); + + if (valueNode.kind === Kind.LIST) { + return valueNode.values.map((node) => literalToValue(node, namedType)); + } + + // Does this type (if provided) define `literalToValue` which returns a value? + if (isScalarType(namedType) && namedType.literalToValue != null) { + const literal = namedType.literalToValue(valueNode); + if (literal !== undefined) { + return literal; + } + } + + switch (valueNode.kind) { + case Kind.BOOLEAN: + case Kind.STRING: + case Kind.ENUM: + return valueNode.value; + case Kind.INT: + return parseInt(valueNode.value, 10); + case Kind.FLOAT: + return parseFloat(valueNode.value); + case Kind.OBJECT: { + const fieldDefs = isInputObjectType(namedType) + ? namedType.getFields() + : undefined; + return keyValMap( + valueNode.fields, + (field) => field.name.value, + (field) => { + const fieldDef = fieldDefs && fieldDefs[field.name.value]; + return literalToValue(field.value, fieldDef && fieldDef.type); + }, + ); + } + } + + // istanbul ignore next (Not reachable. All possible const value nodes have been considered) + invariant(false, 'Unexpected: ' + inspect((valueNode: empty))); +} diff --git a/src/utilities/replaceASTVariables.d.ts b/src/utilities/replaceASTVariables.d.ts new file mode 100644 index 00000000000..f07c5f30408 --- /dev/null +++ b/src/utilities/replaceASTVariables.d.ts @@ -0,0 +1,16 @@ +import { Maybe } from '../jsutils/Maybe'; +import { ObjMap } from '../jsutils/ObjMap'; + +import { ValueNode, ConstValueNode } from '../language/ast'; + +/** + * Replaces any Variables found within an AST Value literal with literals + * supplied from a map of variable values, returning a constant value. + * + * Used primarily to ensure only complete constant values are used during input + * coercion of custom scalars which accept complex literals. + */ +export function replaceASTVariables( + valueNode: ValueNode, + variables: Maybe>, +): ConstValueNode; diff --git a/src/utilities/replaceASTVariables.js b/src/utilities/replaceASTVariables.js new file mode 100644 index 00000000000..5e4fb3feb3e --- /dev/null +++ b/src/utilities/replaceASTVariables.js @@ -0,0 +1,36 @@ +import type { ObjMap } from '../jsutils/ObjMap'; + +import type { ValueNode, ConstValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; +import { visit } from '../language/visitor'; + +import { valueToLiteral } from './valueToLiteral'; + +/** + * Replaces any Variables found within an AST Value literal with literals + * supplied from a map of variable values, returning a constant value. + * + * Used primarily to ensure only complete constant values are used during input + * coercion of custom scalars which accept complex literals. + */ +export function replaceASTVariables( + valueNode: ValueNode, + variables: ?ObjMap, +): ConstValueNode { + return visit(valueNode, { + Variable(node) { + return valueToLiteral(variables?.[node.name.value]); + }, + ObjectValue(node) { + return { + ...node, + // Filter out any fields with a missing variable. + fields: node.fields.filter( + (field) => + field.value.kind !== Kind.VARIABLE || + variables?.[field.value.name.value] !== undefined, + ), + }; + }, + }); +} diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 19081ea96e0..07d6f50368c 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -14,6 +14,9 @@ import { isNonNullType, } from '../type/definition'; +import { literalToValue } from './literalToValue'; +import { replaceASTVariables } from './replaceASTVariables'; + /** * Produces a JavaScript value given a GraphQL Value AST. * @@ -134,12 +137,16 @@ export function valueFromAST( // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') if (isLeafType(type)) { + const constValueNode = replaceASTVariables(valueNode, variables); + // Scalars and Enums fulfill parsing a literal value via parseLiteral(). // Invalid values represent a failure to parse correctly, in which case // no value is returned. let result; try { - result = type.parseLiteral(valueNode, variables); + result = type.parseLiteral + ? type.parseLiteral(constValueNode) + : type.parseValue(literalToValue(constValueNode, type)); } catch (_error) { return; // Invalid: intentionally return no value. } diff --git a/src/utilities/valueFromASTUntyped.d.ts b/src/utilities/valueFromASTUntyped.d.ts index 495e2797234..c51fbf98381 100644 --- a/src/utilities/valueFromASTUntyped.d.ts +++ b/src/utilities/valueFromASTUntyped.d.ts @@ -18,6 +18,7 @@ import { ValueNode } from '../language/ast'; * | Int / Float | Number | * | Null | null | * + * @deprecated use literalToValue */ export function valueFromASTUntyped( valueNode: ValueNode, diff --git a/src/utilities/valueFromASTUntyped.js b/src/utilities/valueFromASTUntyped.js index 05f5db71b2e..bac8d5c9354 100644 --- a/src/utilities/valueFromASTUntyped.js +++ b/src/utilities/valueFromASTUntyped.js @@ -1,4 +1,5 @@ import type { ObjMap } from '../jsutils/ObjMap'; +import { deprecationWarning } from '../jsutils/deprecationWarning'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { keyValMap } from '../jsutils/keyValMap'; @@ -21,11 +22,14 @@ import type { ValueNode } from '../language/ast'; * | Int / Float | Number | * | Null | null | * + * @deprecated use literalToValue */ export function valueFromASTUntyped( valueNode: ValueNode, variables?: ?ObjMap, ): mixed { + deprecationWarning('valueFromASTUntyped', 'Use "literalToValue".'); + switch (valueNode.kind) { case Kind.NULL: return null; diff --git a/src/utilities/valueToLiteral.d.ts b/src/utilities/valueToLiteral.d.ts new file mode 100644 index 00000000000..1872f31453c --- /dev/null +++ b/src/utilities/valueToLiteral.d.ts @@ -0,0 +1,28 @@ +import { ConstValueNode } from '../language/ast'; +import { GraphQLInputType } from '../type/definition'; + +/** + * Produces a GraphQL Value AST given a JavaScript object. + * Function will match JavaScript values to GraphQL AST schema format + * by using suggested GraphQLInputType. For example: + * + * valueToLiteral("value", GraphQLString) + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `valueToLiteral` method. + * + * | JavaScript Value | GraphQL Value | + * | ----------------- | -------------------- | + * | Object | Input Object | + * | Array | List | + * | Boolean | Boolean | + * | String | String Value | + * | Number | Int / Float | + * | null / undefined | NullValue | + * + * Note: This function does not perform any type validation or coercion. + */ +export function valueToLiteral( + value: unknown, + type?: GraphQLInputType, +): ConstValueNode; diff --git a/src/utilities/valueToLiteral.js b/src/utilities/valueToLiteral.js new file mode 100644 index 00000000000..a4c13f18884 --- /dev/null +++ b/src/utilities/valueToLiteral.js @@ -0,0 +1,97 @@ +import { inspect } from '../jsutils/inspect'; +import { isIterableObject } from '../jsutils/isIterableObject'; + +import type { ConstValueNode } from '../language/ast'; +import { Kind } from '../language/kinds'; + +import type { GraphQLInputType } from '../type/definition'; +import { + getNamedType, + isLeafType, + isInputObjectType, +} from '../type/definition'; + +/** + * Produces a GraphQL Value AST given a JavaScript value. + * + * A GraphQL type may be provided, which will be used to interpret different + * JavaScript values if it defines a `valueToLiteral` method. + * + * | JavaScript Value | GraphQL Value | + * | ----------------- | -------------------- | + * | Object | Input Object | + * | Array | List | + * | Boolean | Boolean | + * | String | String | + * | Number | Int / Float | + * | null / undefined | Null | + * + * Note: This function does not perform any type validation or coercion. + */ +export function valueToLiteral( + value: mixed, + type?: GraphQLInputType, +): ConstValueNode { + // Like JSON, a null literal is produced for null and undefined. + if (value == null) { + return { kind: Kind.NULL }; + } + + const namedType = type && getNamedType(type); + + // Convert JavaScript array to GraphQL list. + if (isIterableObject(value)) { + return { + kind: Kind.LIST, + values: Array.from(value, (item) => valueToLiteral(item, namedType)), + }; + } + + // Does this type (if provided) define `valueToLiteral` which returns a value? + if (isLeafType(namedType) && namedType.valueToLiteral != null) { + const literal = namedType.valueToLiteral(value); + if (literal) { + return literal; + } + } + + // Otherwise, perform a JS-to-Literal default equivalency. + switch (typeof value) { + case 'boolean': + return { kind: Kind.BOOLEAN, value }; + case 'string': + return { kind: Kind.STRING, value }; + case 'number': { + if (!Number.isFinite(value)) { + // Like JSON, a null literal is produced for non-finite values. + return { kind: Kind.NULL }; + } + const stringNum = String(value); + // Use Int literals for integer numbers. + return /^-?(?:0|[1-9][0-9]*)$/.test(stringNum) + ? { kind: Kind.INT, value: stringNum } + : { kind: Kind.FLOAT, value: stringNum }; + } + case 'object': { + const fields = []; + const fieldDefs = isInputObjectType(namedType) + ? namedType.getFields() + : undefined; + for (const fieldName of Object.keys(value)) { + const fieldValue = value[fieldName]; + // Like JSON, undefined fields are not included in the literal result. + if (fieldValue !== undefined) { + const fieldDef = fieldDefs && fieldDefs[fieldName]; + fields.push({ + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: fieldName }, + value: valueToLiteral(value[fieldName], fieldDef && fieldDef.type), + }); + } + } + return { kind: Kind.OBJECT, fields }; + } + } + + throw new TypeError(`Cannot convert value to AST: ${inspect(value)}.`); +} diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.js b/src/validation/rules/ValuesOfCorrectTypeRule.js index 9153b1a5edf..5b55b275483 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.js +++ b/src/validation/rules/ValuesOfCorrectTypeRule.js @@ -20,6 +20,8 @@ import { } from '../../type/definition'; import type { ValidationContext } from '../ValidationContext'; +import { literalToValue } from '../../utilities/literalToValue'; +import { replaceASTVariables } from '../../utilities/replaceASTVariables'; /** * Value literals of correct type @@ -121,10 +123,15 @@ function isValidValueNode(context: ValidationContext, node: ValueNode): void { return; } + const constValueNode = replaceASTVariables(node, undefined /* variables */); + // Scalars and Enums determine if a literal value is valid via parseLiteral(), - // which may throw or return an invalid value to indicate failure. + // or parseValue() which may throw or return an invalid value to indicate + // failure. try { - const parseResult = type.parseLiteral(node, undefined /* variables */); + const parseResult = type.parseLiteral + ? type.parseLiteral(constValueNode) + : type.parseValue(literalToValue(constValueNode, type)); if (parseResult === undefined) { const typeStr = inspect(locationType); context.reportError(