Skip to content

Commit ce12bde

Browse files
JoviDeCroockfotoetienne
authored andcommitted
frontport graphql#4482: Implement changes for executable descriptions
Revives and addresses comments from graphql#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 735f0e4 commit ce12bde

File tree

8 files changed

+436
-22
lines changed

8 files changed

+436
-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: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ describe('Parser', () => {
267267
definitions: [
268268
{
269269
kind: Kind.OPERATION_DEFINITION,
270+
description: undefined,
270271
loc: { start: 0, end: 40 },
271272
operation: 'query',
272273
name: undefined,
@@ -357,6 +358,7 @@ describe('Parser', () => {
357358
definitions: [
358359
{
359360
kind: Kind.OPERATION_DEFINITION,
361+
description: undefined,
360362
loc: { start: 0, end: 29 },
361363
operation: 'query',
362364
name: undefined,
@@ -404,6 +406,75 @@ describe('Parser', () => {
404406
});
405407
});
406408

409+
it('creates ast from nameless query with description', () => {
410+
const result = parse(dedent`
411+
"Description"
412+
query {
413+
node {
414+
id
415+
}
416+
}
417+
`);
418+
419+
expectJSON(result).toDeepEqual({
420+
kind: Kind.DOCUMENT,
421+
loc: { start: 0, end: 43 },
422+
definitions: [
423+
{
424+
kind: Kind.OPERATION_DEFINITION,
425+
loc: { start: 0, end: 43 },
426+
description: {
427+
kind: Kind.STRING,
428+
loc: { start: 0, end: 13 },
429+
value: 'Description',
430+
block: false,
431+
},
432+
operation: 'query',
433+
name: undefined,
434+
variableDefinitions: undefined,
435+
directives: undefined,
436+
selectionSet: {
437+
kind: Kind.SELECTION_SET,
438+
loc: { start: 20, end: 43 },
439+
selections: [
440+
{
441+
kind: Kind.FIELD,
442+
loc: { start: 24, end: 41 },
443+
alias: undefined,
444+
name: {
445+
kind: Kind.NAME,
446+
loc: { start: 24, end: 28 },
447+
value: 'node',
448+
},
449+
arguments: undefined,
450+
directives: undefined,
451+
selectionSet: {
452+
kind: Kind.SELECTION_SET,
453+
loc: { start: 29, end: 41 },
454+
selections: [
455+
{
456+
kind: Kind.FIELD,
457+
loc: { start: 35, end: 37 },
458+
alias: undefined,
459+
name: {
460+
kind: Kind.NAME,
461+
loc: { start: 35, end: 37 },
462+
value: 'id',
463+
},
464+
arguments: undefined,
465+
directives: undefined,
466+
selectionSet: undefined,
467+
},
468+
],
469+
},
470+
},
471+
],
472+
},
473+
},
474+
],
475+
});
476+
});
477+
407478
it('allows parsing without source location information', () => {
408479
const result = parse('{ id }', { noLocation: true });
409480
expect('loc' in result).to.equal(false);
@@ -686,6 +757,281 @@ describe('Parser', () => {
686757
});
687758
});
688759

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

0 commit comments

Comments
 (0)