Skip to content

Commit 1615f12

Browse files
Implement changes for executable descriptions
Co-Authored-By: fotoetienne <693596+fotoetienne@users.noreply.github.com>
1 parent 60ae6c4 commit 1615f12

File tree

8 files changed

+228
-15
lines changed

8 files changed

+228
-15
lines changed

src/__testUtils__/kitchenSinkQuery.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export const kitchenSinkQuery: string = String.raw`
2-
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
2+
"Query description"
3+
query queryName(
4+
"Very complex variable"
5+
$foo: ComplexType,
6+
$site: Site = MOBILE
7+
) @onQuery {
38
whoever123is: node(id: [123, 456]) {
49
id
510
... on User @onInlineFragment {
@@ -44,6 +49,9 @@ subscription StoryLikeSubscription(
4449
}
4550
}
4651
52+
"""
53+
Fragment description
54+
"""
4755
fragment frag on Friend @onFragmentDefinition {
4856
foo(
4957
size: $size

src/language/__tests__/parser-test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ describe('Parser', () => {
258258
definitions: [
259259
{
260260
kind: Kind.OPERATION_DEFINITION,
261+
description: undefined,
261262
loc: { start: 0, end: 40 },
262263
operation: 'query',
263264
name: undefined,
@@ -349,6 +350,7 @@ describe('Parser', () => {
349350
{
350351
kind: Kind.OPERATION_DEFINITION,
351352
loc: { start: 0, end: 29 },
353+
description: undefined,
352354
operation: 'query',
353355
name: undefined,
354356
variableDefinitions: [],
@@ -395,6 +397,75 @@ describe('Parser', () => {
395397
});
396398
});
397399

400+
it('creates ast from nameless query with description', () => {
401+
const result = parse(dedent`
402+
"Description"
403+
query {
404+
node {
405+
id
406+
}
407+
}
408+
`);
409+
410+
expectJSON(result).toDeepEqual({
411+
kind: Kind.DOCUMENT,
412+
loc: { start: 0, end: 43},
413+
definitions: [
414+
{
415+
kind: Kind.OPERATION_DEFINITION,
416+
loc: { start: 0, end: 43 },
417+
description: {
418+
kind: Kind.STRING,
419+
loc: { start: 0, end: 13 },
420+
value: 'Description',
421+
block: false,
422+
},
423+
operation: 'query',
424+
name: undefined,
425+
variableDefinitions: [],
426+
directives: [],
427+
selectionSet: {
428+
kind: Kind.SELECTION_SET,
429+
loc: { start: 20, end: 43 },
430+
selections: [
431+
{
432+
kind: Kind.FIELD,
433+
loc: { start: 24, end: 41 },
434+
alias: undefined,
435+
name: {
436+
kind: Kind.NAME,
437+
loc: { start: 24, end: 28 },
438+
value: 'node',
439+
},
440+
arguments: [],
441+
directives: [],
442+
selectionSet: {
443+
kind: Kind.SELECTION_SET,
444+
loc: { start: 29, end: 41 },
445+
selections: [
446+
{
447+
kind: Kind.FIELD,
448+
loc: { start: 35, end: 37 },
449+
alias: undefined,
450+
name: {
451+
kind: Kind.NAME,
452+
loc: { start: 35, end: 37 },
453+
value: 'id',
454+
},
455+
arguments: [],
456+
directives: [],
457+
selectionSet: undefined,
458+
},
459+
],
460+
},
461+
},
462+
],
463+
},
464+
},
465+
],
466+
});
467+
});
468+
398469
it('allows parsing without source location information', () => {
399470
const result = parse('{ id }', { noLocation: true });
400471
expect('loc' in result).to.equal(false);
@@ -657,4 +728,93 @@ describe('Parser', () => {
657728
});
658729
});
659730
});
731+
732+
describe('operation and variable definition descriptions', () => {
733+
it('parses operation with description and variable descriptions', () => {
734+
const result = parse(dedent`
735+
"Operation description"
736+
query myQuery(
737+
"Variable a description"
738+
$a: Int,
739+
"""Variable b\nmultiline description"""
740+
$b: String
741+
) {
742+
field(a: $a, b: $b)
743+
}
744+
`);
745+
// Find the operation definition
746+
const opDef = result.definitions.find(
747+
(d) => d.kind === Kind.OPERATION_DEFINITION,
748+
);
749+
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
750+
throw new Error('No operation definition found');
751+
}
752+
expect(opDef.description?.value).to.equal('Operation description');
753+
expect(opDef.name?.value).to.equal('myQuery');
754+
expect(opDef.variableDefinitions?.[0].description?.value).to.equal(
755+
'Variable a description',
756+
);
757+
expect(opDef.variableDefinitions?.[0].description?.block).to.equal(false);
758+
expect(opDef.variableDefinitions?.[1].description?.value).to.equal(
759+
'Variable b\nmultiline description',
760+
);
761+
expect(opDef.variableDefinitions?.[1].description?.block).to.equal(true);
762+
expect(opDef.variableDefinitions?.[0].variable.name.value).to.equal('a');
763+
expect(opDef.variableDefinitions?.[1].variable.name.value).to.equal('b');
764+
// Check type names safely
765+
const typeA = opDef.variableDefinitions?.[0].type;
766+
if (typeA && typeA.kind === Kind.NAMED_TYPE) {
767+
expect(typeA.name.value).to.equal('Int');
768+
}
769+
const typeB = opDef.variableDefinitions?.[1].type;
770+
if (typeB && typeB.kind === Kind.NAMED_TYPE) {
771+
expect(typeB.name.value).to.equal('String');
772+
}
773+
});
774+
775+
it('parses variable definition with description, default value, and directives', () => {
776+
const result = parse(dedent`
777+
query (
778+
"desc"
779+
$foo: Int = 42 @dir
780+
) {
781+
field(foo: $foo)
782+
}
783+
`);
784+
const opDef = result.definitions.find(
785+
(d) => d.kind === Kind.OPERATION_DEFINITION,
786+
);
787+
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
788+
throw new Error('No operation definition found');
789+
}
790+
const varDef = opDef.variableDefinitions?.[0];
791+
expect(varDef?.description?.value).to.equal('desc');
792+
expect(varDef?.variable.name.value).to.equal('foo');
793+
if (varDef?.type.kind === Kind.NAMED_TYPE) {
794+
expect(varDef.type.name.value).to.equal('Int');
795+
}
796+
if (varDef?.defaultValue && 'value' in varDef.defaultValue) {
797+
expect(varDef.defaultValue.value).to.equal('42');
798+
}
799+
expect(varDef?.directives?.[0].name.value).to.equal('dir');
800+
});
801+
802+
it('parses fragment with variable description (legacy)', () => {
803+
const result = parse('fragment Foo("desc" $foo: Int) on Bar { baz }', {
804+
allowLegacyFragmentVariables: true,
805+
});
806+
const fragDef = result.definitions.find(
807+
(d) => d.kind === Kind.FRAGMENT_DEFINITION,
808+
);
809+
if (!fragDef || fragDef.kind !== Kind.FRAGMENT_DEFINITION) {
810+
throw new Error('No fragment definition found');
811+
}
812+
const varDef = fragDef.variableDefinitions?.[0];
813+
expect(varDef?.description?.value).to.equal('desc');
814+
expect(varDef?.variable.name.value).to.equal('foo');
815+
if (varDef?.type.kind === Kind.NAMED_TYPE) {
816+
expect(varDef.type.name.value).to.equal('Int');
817+
}
818+
});
819+
});
660820
});

src/language/__tests__/printer-test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,19 @@ describe('Printer: Query document', () => {
138138
`);
139139
});
140140

141+
it('prints fragment', () => {
142+
const printed = print(
143+
parse('"Fragment description" fragment Foo on Bar { baz }'),
144+
);
145+
146+
expect(printed).to.equal(dedent`
147+
"Fragment description"
148+
fragment Foo on Bar {
149+
baz
150+
}
151+
`);
152+
});
153+
141154
it('prints kitchen sink without altering ast', () => {
142155
const ast = parse(kitchenSinkQuery, { noLocation: true });
143156

@@ -150,7 +163,12 @@ describe('Printer: Query document', () => {
150163

151164
expect(printed).to.equal(
152165
dedentString(String.raw`
153-
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
166+
"Query description"
167+
query queryName(
168+
"Very complex variable"
169+
$foo: ComplexType
170+
$site: Site = MOBILE
171+
) @onQuery {
154172
whoever123is: node(id: [123, 456]) {
155173
id
156174
... on User @onInlineFragment {
@@ -192,6 +210,7 @@ describe('Printer: Query document', () => {
192210
}
193211
}
194212
213+
"""Fragment description"""
195214
fragment frag on Friend @onFragmentDefinition {
196215
foo(
197216
size: $size

src/language/__tests__/schema-parser-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ describe('Schema Parser', () => {
331331
}
332332
`).to.deep.equal({
333333
message:
334-
'Syntax Error: Unexpected description, descriptions are supported only on type definitions.',
334+
'Syntax Error: Unexpected description, descriptions are not supported on type extensions and shorthand queries.',
335335
locations: [{ line: 2, column: 7 }],
336336
});
337337

@@ -353,7 +353,7 @@ describe('Schema Parser', () => {
353353
}
354354
`).to.deep.equal({
355355
message:
356-
'Syntax Error: Unexpected description, descriptions are supported only on type definitions.',
356+
'Syntax Error: Unexpected description, descriptions are not supported on type extensions and shorthand queries.',
357357
locations: [{ line: 2, column: 7 }],
358358
});
359359

src/language/__tests__/visitor-test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,13 @@ describe('Visitor', () => {
539539
expect(visited).to.deep.equal([
540540
['enter', 'Document', undefined, undefined],
541541
['enter', 'OperationDefinition', 0, undefined],
542+
['enter', 'StringValue', 'description', 'OperationDefinition'],
543+
['leave', 'StringValue', 'description', 'OperationDefinition'],
542544
['enter', 'Name', 'name', 'OperationDefinition'],
543545
['leave', 'Name', 'name', 'OperationDefinition'],
544546
['enter', 'VariableDefinition', 0, undefined],
547+
['enter', 'StringValue', 'description', 'VariableDefinition'],
548+
['leave', 'StringValue', 'description', 'VariableDefinition'],
545549
['enter', 'Variable', 'variable', 'VariableDefinition'],
546550
['enter', 'Name', 'name', 'Variable'],
547551
['leave', 'Name', 'name', 'Variable'],
@@ -793,6 +797,8 @@ describe('Visitor', () => {
793797
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
794798
['leave', 'OperationDefinition', 2, undefined],
795799
['enter', 'FragmentDefinition', 3, undefined],
800+
['enter', 'StringValue', 'description', 'FragmentDefinition'],
801+
['leave', 'StringValue', 'description', 'FragmentDefinition'],
796802
['enter', 'Name', 'name', 'FragmentDefinition'],
797803
['leave', 'Name', 'name', 'FragmentDefinition'],
798804
['enter', 'NamedType', 'typeCondition', 'FragmentDefinition'],

src/language/ast.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,13 @@ export const QueryDocumentKeys: {
198198

199199
Document: ['definitions'],
200200
OperationDefinition: [
201+
'description',
201202
'name',
202203
'variableDefinitions',
203204
'directives',
204205
'selectionSet',
205206
],
206-
VariableDefinition: ['variable', 'type', 'defaultValue', 'directives'],
207+
VariableDefinition: ['description', 'variable', 'type', 'defaultValue', 'directives'],
207208
Variable: ['name'],
208209
SelectionSet: ['selections'],
209210
Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
@@ -212,6 +213,7 @@ export const QueryDocumentKeys: {
212213
FragmentSpread: ['name', 'directives'],
213214
InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
214215
FragmentDefinition: [
216+
'description',
215217
'name',
216218
// Note: fragment variable definitions are deprecated and will removed in v17.0.0
217219
'variableDefinitions',
@@ -316,6 +318,7 @@ export type ExecutableDefinitionNode =
316318

317319
export interface OperationDefinitionNode {
318320
readonly kind: Kind.OPERATION_DEFINITION;
321+
readonly description?: StringValueNode;
319322
readonly loc?: Location;
320323
readonly operation: OperationTypeNode;
321324
readonly name?: NameNode;
@@ -333,6 +336,7 @@ export { OperationTypeNode };
333336

334337
export interface VariableDefinitionNode {
335338
readonly kind: Kind.VARIABLE_DEFINITION;
339+
readonly description?: StringValueNode;
336340
readonly loc?: Location;
337341
readonly variable: VariableNode;
338342
readonly type: TypeNode;
@@ -397,6 +401,7 @@ export interface InlineFragmentNode {
397401

398402
export interface FragmentDefinitionNode {
399403
readonly kind: Kind.FRAGMENT_DEFINITION;
404+
readonly description?: StringValueNode;
400405
readonly loc?: Location;
401406
readonly name: NameNode;
402407
/** @deprecated variableDefinitions will be removed in v17.0.0 */

0 commit comments

Comments
 (0)