Skip to content

Commit 2c03c75

Browse files
authored
Add support for executable descriptions (#59)
* Add support for executable descriptions * Add changeset * Cleanup
1 parent 58290cf commit 2c03c75

File tree

5 files changed

+335
-16
lines changed

5 files changed

+335
-16
lines changed

.changeset/gold-apricots-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@0no-co/graphql.web": minor
3+
---
4+
5+
Add support for executable definitions as defined in https://github.com/graphql/graphql-spec/pull/1170

src/__tests__/description.test.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { parse } from '../parser';
3+
import { print } from '../printer';
4+
import type {
5+
OperationDefinitionNode,
6+
VariableDefinitionNode,
7+
FragmentDefinitionNode,
8+
} from '../ast';
9+
10+
describe('GraphQL descriptions', () => {
11+
describe('OperationDefinition descriptions', () => {
12+
it('parses operation with description', () => {
13+
const source = `
14+
"""
15+
Request the current status of a time machine and its operator.
16+
"""
17+
query GetTimeMachineStatus {
18+
timeMachine {
19+
id
20+
status
21+
}
22+
}
23+
`;
24+
25+
const doc = parse(source, { noLocation: true });
26+
const operation = doc.definitions[0] as OperationDefinitionNode;
27+
28+
expect(operation.description).toBeDefined();
29+
expect(operation.description?.value).toBe(
30+
'Request the current status of a time machine and its operator.'
31+
);
32+
expect(operation.description?.block).toBe(true);
33+
});
34+
35+
it('parses operation with single-line description', () => {
36+
const source = `
37+
"Simple query description"
38+
query SimpleQuery {
39+
field
40+
}
41+
`;
42+
43+
const doc = parse(source, { noLocation: true });
44+
const operation = doc.definitions[0] as OperationDefinitionNode;
45+
46+
expect(operation.description).toBeDefined();
47+
expect(operation.description?.value).toBe('Simple query description');
48+
expect(operation.description?.block).toBe(false);
49+
});
50+
51+
it('does not allow description on anonymous operations', () => {
52+
const source = `
53+
"This should fail"
54+
{
55+
field
56+
}
57+
`;
58+
59+
expect(() => parse(source)).toThrow();
60+
});
61+
62+
it('parses mutation with description', () => {
63+
const source = `
64+
"""
65+
Create a new time machine entry.
66+
"""
67+
mutation CreateTimeMachine($input: TimeMachineInput!) {
68+
createTimeMachine(input: $input) {
69+
id
70+
}
71+
}
72+
`;
73+
74+
const doc = parse(source, { noLocation: true });
75+
const operation = doc.definitions[0] as OperationDefinitionNode;
76+
77+
expect(operation.description).toBeDefined();
78+
expect(operation.description?.value).toBe('Create a new time machine entry.');
79+
});
80+
});
81+
82+
describe('VariableDefinition descriptions', () => {
83+
it('parses variable with description', () => {
84+
const source = `
85+
query GetTimeMachineStatus(
86+
"The unique serial number of the time machine to inspect."
87+
$machineId: ID!
88+
89+
"""
90+
The year to check the status for.
91+
**Warning:** certain years may trigger an anomaly in the space-time continuum.
92+
"""
93+
$year: Int
94+
) {
95+
timeMachine(id: $machineId) {
96+
status(year: $year)
97+
}
98+
}
99+
`;
100+
101+
const doc = parse(source, { noLocation: true });
102+
const operation = doc.definitions[0] as OperationDefinitionNode;
103+
const variables = operation.variableDefinitions as VariableDefinitionNode[];
104+
105+
expect(variables[0].description).toBeDefined();
106+
expect(variables[0].description?.value).toBe(
107+
'The unique serial number of the time machine to inspect.'
108+
);
109+
expect(variables[0].description?.block).toBe(false);
110+
111+
expect(variables[1].description).toBeDefined();
112+
expect(variables[1].description?.value).toBe(
113+
'The year to check the status for.\n**Warning:** certain years may trigger an anomaly in the space-time continuum.'
114+
);
115+
expect(variables[1].description?.block).toBe(true);
116+
});
117+
118+
it('parses mixed variables with and without descriptions', () => {
119+
const source = `
120+
query Mixed(
121+
"Described variable"
122+
$described: String
123+
$undescribed: Int
124+
) {
125+
field
126+
}
127+
`;
128+
129+
const doc = parse(source, { noLocation: true });
130+
const operation = doc.definitions[0] as OperationDefinitionNode;
131+
const variables = operation.variableDefinitions as VariableDefinitionNode[];
132+
133+
expect(variables[0].description).toBeDefined();
134+
expect(variables[0].description?.value).toBe('Described variable');
135+
expect(variables[1].description).toBeUndefined();
136+
});
137+
});
138+
139+
describe('FragmentDefinition descriptions', () => {
140+
it('parses fragment with description', () => {
141+
const source = `
142+
"Time machine details."
143+
fragment TimeMachineDetails on TimeMachine {
144+
id
145+
model
146+
lastMaintenance
147+
}
148+
`;
149+
150+
const doc = parse(source, { noLocation: true });
151+
const fragment = doc.definitions[0] as FragmentDefinitionNode;
152+
153+
expect(fragment.description).toBeDefined();
154+
expect(fragment.description?.value).toBe('Time machine details.');
155+
expect(fragment.description?.block).toBe(false);
156+
});
157+
158+
it('parses fragment with block description', () => {
159+
const source = `
160+
"""
161+
Comprehensive time machine information
162+
including maintenance history and operational status.
163+
"""
164+
fragment FullTimeMachineInfo on TimeMachine {
165+
id
166+
model
167+
lastMaintenance
168+
operationalStatus
169+
}
170+
`;
171+
172+
const doc = parse(source, { noLocation: true });
173+
const fragment = doc.definitions[0] as FragmentDefinitionNode;
174+
175+
expect(fragment.description).toBeDefined();
176+
expect(fragment.description?.value).toBe(
177+
'Comprehensive time machine information\nincluding maintenance history and operational status.'
178+
);
179+
expect(fragment.description?.block).toBe(true);
180+
});
181+
});
182+
183+
describe('print with descriptions', () => {
184+
it('prints operation description correctly', () => {
185+
const source = `"""
186+
Request the current status of a time machine and its operator.
187+
"""
188+
query GetTimeMachineStatus {
189+
timeMachine {
190+
id
191+
}
192+
}`;
193+
194+
const doc = parse(source, { noLocation: true });
195+
const printed = print(doc);
196+
197+
expect(printed).toContain('"""');
198+
expect(printed).toContain('Request the current status of a time machine and its operator.');
199+
});
200+
201+
it('prints variable descriptions correctly', () => {
202+
const source = `query GetStatus(
203+
"Machine ID"
204+
$id: ID!
205+
) {
206+
field
207+
}`;
208+
209+
const doc = parse(source, { noLocation: true });
210+
const printed = print(doc);
211+
212+
expect(printed).toContain('"Machine ID"');
213+
});
214+
215+
it('prints fragment description correctly', () => {
216+
const source = `"Details fragment"
217+
fragment Details on Type {
218+
field
219+
}`;
220+
221+
const doc = parse(source, { noLocation: true });
222+
const printed = print(doc);
223+
224+
expect(printed).toContain('"Details fragment"');
225+
});
226+
});
227+
228+
describe('roundtrip parsing and printing', () => {
229+
it('maintains descriptions through parse and print cycle', () => {
230+
const source = `"""
231+
Request the current status of a time machine and its operator.
232+
"""
233+
query GetTimeMachineStatus(
234+
"The unique serial number of the time machine to inspect."
235+
$machineId: ID!
236+
237+
"""
238+
The year to check the status for.
239+
**Warning:** certain years may trigger an anomaly in the space-time continuum.
240+
"""
241+
$year: Int
242+
) {
243+
timeMachine(id: $machineId) {
244+
...TimeMachineDetails
245+
operator {
246+
name
247+
licenseLevel
248+
}
249+
status(year: $year)
250+
}
251+
}
252+
253+
"Time machine details."
254+
fragment TimeMachineDetails on TimeMachine {
255+
id
256+
model
257+
lastMaintenance
258+
}`;
259+
260+
const doc = parse(source, { noLocation: true });
261+
const printed = print(doc);
262+
const reparsed = parse(printed, { noLocation: true });
263+
264+
const operation = doc.definitions[0] as OperationDefinitionNode;
265+
const reparsedOperation = reparsed.definitions[0] as OperationDefinitionNode;
266+
267+
// The printed/reparsed cycle may have slightly different formatting but same content
268+
expect(reparsedOperation.description?.value?.trim()).toBe(
269+
operation.description?.value?.trim()
270+
);
271+
272+
const fragment = doc.definitions[1] as FragmentDefinitionNode;
273+
const reparsedFragment = reparsed.definitions[1] as FragmentDefinitionNode;
274+
275+
expect(reparsedFragment.description?.value).toBe(fragment.description?.value);
276+
});
277+
});
278+
});

src/ast.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ export type ExecutableDefinitionNode = Or<
103103
>;
104104

105105
export type OperationDefinitionNode = Or<
106-
GraphQL.OperationDefinitionNode,
106+
GraphQL.OperationDefinitionNode & { description?: StringValueNode },
107107
{
108108
readonly kind: Kind.OPERATION_DEFINITION;
109109
readonly operation: OperationTypeNode;
110110
readonly name?: NameNode;
111+
readonly description?: StringValueNode;
111112
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
112113
readonly directives?: ReadonlyArray<DirectiveNode>;
113114
readonly selectionSet: SelectionSetNode;
@@ -116,12 +117,13 @@ export type OperationDefinitionNode = Or<
116117
>;
117118

118119
export type VariableDefinitionNode = Or<
119-
GraphQL.VariableDefinitionNode,
120+
GraphQL.VariableDefinitionNode & { description?: StringValueNode },
120121
{
121122
readonly kind: Kind.VARIABLE_DEFINITION;
122123
readonly variable: VariableNode;
123124
readonly type: TypeNode;
124125
readonly defaultValue?: ConstValueNode;
126+
readonly description?: StringValueNode;
125127
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
126128
readonly loc?: Location;
127129
}
@@ -205,10 +207,11 @@ export type InlineFragmentNode = Or<
205207
>;
206208

207209
export type FragmentDefinitionNode = Or<
208-
GraphQL.FragmentDefinitionNode,
210+
GraphQL.FragmentDefinitionNode & { description?: StringValueNode },
209211
{
210212
readonly kind: Kind.FRAGMENT_DEFINITION;
211213
readonly name: NameNode;
214+
readonly description?: StringValueNode;
212215
readonly typeCondition: NamedTypeNode;
213216
readonly directives?: ReadonlyArray<DirectiveNode>;
214217
readonly selectionSet: SelectionSetNode;

0 commit comments

Comments
 (0)