Skip to content

Commit 364f17f

Browse files
Implement changes for executable descriptions (#4482)
Revives and addresses comments from #4430 This is already in the spec and the impl is lagging behind so we might need to get this in 😅 --------- Co-authored-by: fotoetienne <693596+fotoetienne@users.noreply.github.com>
1 parent 60ae6c4 commit 364f17f

File tree

8 files changed

+437
-22
lines changed

8 files changed

+437
-22
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: 347 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,280 @@ 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+
746+
// Find the operation definition
747+
const opDef = result.definitions.find(
748+
(d) => d.kind === Kind.OPERATION_DEFINITION,
749+
);
750+
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
751+
throw new Error('No operation definition found');
752+
}
753+
754+
expectJSON(opDef).toDeepEqual({
755+
kind: Kind.OPERATION_DEFINITION,
756+
operation: 'query',
757+
description: {
758+
kind: Kind.STRING,
759+
value: 'Operation description',
760+
block: false,
761+
loc: { start: 0, end: 23 },
762+
},
763+
name: {
764+
kind: Kind.NAME,
765+
value: 'myQuery',
766+
loc: { start: 30, end: 37 },
767+
},
768+
variableDefinitions: [
769+
{
770+
kind: Kind.VARIABLE_DEFINITION,
771+
description: {
772+
kind: Kind.STRING,
773+
value: 'Variable a description',
774+
block: false,
775+
loc: { start: 41, end: 65 },
776+
},
777+
variable: {
778+
kind: Kind.VARIABLE,
779+
name: {
780+
kind: Kind.NAME,
781+
value: 'a',
782+
loc: { start: 69, end: 70 },
783+
},
784+
loc: { start: 68, end: 70 },
785+
},
786+
type: {
787+
kind: Kind.NAMED_TYPE,
788+
name: {
789+
kind: Kind.NAME,
790+
value: 'Int',
791+
loc: { start: 72, end: 75 },
792+
},
793+
loc: { start: 72, end: 75 },
794+
},
795+
defaultValue: undefined,
796+
directives: [],
797+
loc: { start: 41, end: 75 },
798+
},
799+
{
800+
kind: Kind.VARIABLE_DEFINITION,
801+
description: {
802+
kind: Kind.STRING,
803+
value: 'Variable b\nmultiline description',
804+
block: true,
805+
loc: { start: 79, end: 117 },
806+
},
807+
variable: {
808+
kind: Kind.VARIABLE,
809+
name: {
810+
kind: Kind.NAME,
811+
value: 'b',
812+
loc: { start: 121, end: 122 },
813+
},
814+
loc: { start: 120, end: 122 },
815+
},
816+
type: {
817+
kind: Kind.NAMED_TYPE,
818+
name: {
819+
kind: Kind.NAME,
820+
value: 'String',
821+
loc: { start: 124, end: 130 },
822+
},
823+
loc: { start: 124, end: 130 },
824+
},
825+
defaultValue: undefined,
826+
directives: [],
827+
loc: { start: 79, end: 130 },
828+
},
829+
],
830+
directives: [],
831+
selectionSet: {
832+
kind: Kind.SELECTION_SET,
833+
selections: [
834+
{
835+
kind: Kind.FIELD,
836+
alias: undefined,
837+
name: {
838+
kind: Kind.NAME,
839+
value: 'field',
840+
loc: { start: 137, end: 142 },
841+
},
842+
arguments: [
843+
{
844+
kind: Kind.ARGUMENT,
845+
name: {
846+
kind: Kind.NAME,
847+
value: 'a',
848+
loc: { start: 143, end: 144 },
849+
},
850+
value: {
851+
kind: Kind.VARIABLE,
852+
name: {
853+
kind: Kind.NAME,
854+
value: 'a',
855+
loc: { start: 147, end: 148 },
856+
},
857+
loc: { start: 146, end: 148 },
858+
},
859+
loc: { start: 143, end: 148 },
860+
},
861+
{
862+
kind: Kind.ARGUMENT,
863+
name: {
864+
kind: Kind.NAME,
865+
value: 'b',
866+
loc: { start: 150, end: 151 },
867+
},
868+
value: {
869+
kind: Kind.VARIABLE,
870+
name: {
871+
kind: Kind.NAME,
872+
value: 'b',
873+
loc: { start: 154, end: 155 },
874+
},
875+
loc: { start: 153, end: 155 },
876+
},
877+
loc: { start: 150, end: 155 },
878+
},
879+
],
880+
directives: [],
881+
selectionSet: undefined,
882+
loc: { start: 137, end: 156 },
883+
},
884+
],
885+
loc: { start: 133, end: 158 },
886+
},
887+
loc: { start: 0, end: 158 },
888+
});
889+
});
890+
891+
it('descriptions on a short-hand query produce a sensible error', () => {
892+
const input = `"""Invalid"""
893+
{ __typename }`;
894+
expect(() => parse(input)).to.throw(
895+
'Syntax Error: Unexpected description, descriptions are not supported on shorthand queries.',
896+
);
897+
});
898+
899+
it('parses variable definition with description, default value, and directives', () => {
900+
const result = parse(dedent`
901+
query (
902+
"desc"
903+
$foo: Int = 42 @dir
904+
) {
905+
field(foo: $foo)
906+
}
907+
`);
908+
const opDef = result.definitions.find(
909+
(d) => d.kind === Kind.OPERATION_DEFINITION,
910+
);
911+
if (!opDef || opDef.kind !== Kind.OPERATION_DEFINITION) {
912+
throw new Error('No operation definition found');
913+
}
914+
const varDef = opDef.variableDefinitions?.[0];
915+
expectJSON(varDef).toDeepEqual({
916+
kind: Kind.VARIABLE_DEFINITION,
917+
defaultValue: {
918+
kind: Kind.INT,
919+
value: '42',
920+
loc: { start: 31, end: 33 },
921+
},
922+
directives: [
923+
{
924+
arguments: [],
925+
kind: Kind.DIRECTIVE,
926+
name: {
927+
kind: Kind.NAME,
928+
value: 'dir',
929+
loc: { start: 35, end: 38 },
930+
},
931+
loc: { start: 34, end: 38 },
932+
},
933+
],
934+
description: {
935+
kind: Kind.STRING,
936+
value: 'desc',
937+
block: false,
938+
loc: { start: 10, end: 16 },
939+
},
940+
variable: {
941+
kind: Kind.VARIABLE,
942+
name: {
943+
kind: Kind.NAME,
944+
value: 'foo',
945+
loc: { start: 20, end: 23 },
946+
},
947+
loc: { start: 19, end: 23 },
948+
},
949+
type: {
950+
kind: Kind.NAMED_TYPE,
951+
name: {
952+
kind: Kind.NAME,
953+
value: 'Int',
954+
loc: { start: 25, end: 28 },
955+
},
956+
loc: { start: 25, end: 28 },
957+
},
958+
loc: { start: 10, end: 38 },
959+
});
960+
});
961+
962+
it('parses fragment with variable description (legacy)', () => {
963+
const result = parse('fragment Foo("desc" $foo: Int) on Bar { baz }', {
964+
allowLegacyFragmentVariables: true,
965+
});
966+
967+
const fragDef = result.definitions.find(
968+
(d) => d.kind === Kind.FRAGMENT_DEFINITION,
969+
);
970+
if (!fragDef || fragDef.kind !== Kind.FRAGMENT_DEFINITION) {
971+
throw new Error('No fragment definition found');
972+
}
973+
const varDef = fragDef.variableDefinitions?.[0];
974+
975+
expectJSON(varDef).toDeepEqual({
976+
kind: Kind.VARIABLE_DEFINITION,
977+
description: {
978+
kind: Kind.STRING,
979+
value: 'desc',
980+
block: false,
981+
loc: { start: 13, end: 19 },
982+
},
983+
variable: {
984+
kind: Kind.VARIABLE,
985+
name: {
986+
kind: Kind.NAME,
987+
value: 'foo',
988+
loc: { start: 21, end: 24 },
989+
},
990+
loc: { start: 20, end: 24 },
991+
},
992+
type: {
993+
kind: Kind.NAMED_TYPE,
994+
name: {
995+
kind: Kind.NAME,
996+
value: 'Int',
997+
loc: { start: 26, end: 29 },
998+
},
999+
loc: { start: 26, end: 29 },
1000+
},
1001+
defaultValue: undefined,
1002+
directives: [],
1003+
loc: { start: 13, end: 29 },
1004+
});
1005+
});
1006+
});
6601007
});

0 commit comments

Comments
 (0)