Skip to content

Commit 5b35c72

Browse files
committed
Add support for fragment arguments
1 parent 58290cf commit 5b35c72

File tree

7 files changed

+216
-21
lines changed

7 files changed

+216
-21
lines changed

src/__tests__/__snapshots__/parser.test.ts.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
185185
"selectionSet": undefined,
186186
},
187187
{
188+
"arguments": undefined,
188189
"directives": [
189190
{
190191
"arguments": undefined,
@@ -644,7 +645,8 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
644645
"value": {
645646
"block": true,
646647
"kind": "StringValue",
647-
"value": "block string uses """",
648+
"value": "block string uses """
649+
",
648650
},
649651
},
650652
],
@@ -669,6 +671,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
669671
"value": "Friend",
670672
},
671673
},
674+
"variableDefinitions": undefined,
672675
},
673676
{
674677
"directives": undefined,

src/__tests__/parser.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,90 @@ describe('parse', () => {
166166
expect(() => parse('fragment Name on Type { field }')).not.toThrow();
167167
});
168168

169+
it('parses fragment variable definitions', () => {
170+
expect(parse('fragment x($var: Int = 1) on Type { field }').definitions[0]).toEqual({
171+
kind: Kind.FRAGMENT_DEFINITION,
172+
directives: undefined,
173+
name: {
174+
kind: Kind.NAME,
175+
value: 'x',
176+
},
177+
typeCondition: {
178+
kind: Kind.NAMED_TYPE,
179+
name: {
180+
kind: Kind.NAME,
181+
value: 'Type',
182+
},
183+
},
184+
variableDefinitions: [
185+
{
186+
kind: Kind.VARIABLE_DEFINITION,
187+
type: {
188+
kind: Kind.NAMED_TYPE,
189+
name: {
190+
kind: Kind.NAME,
191+
value: 'Int',
192+
},
193+
},
194+
variable: {
195+
kind: Kind.VARIABLE,
196+
name: {
197+
kind: Kind.NAME,
198+
value: 'var',
199+
},
200+
},
201+
defaultValue: {
202+
kind: Kind.INT,
203+
value: '1',
204+
},
205+
directives: undefined,
206+
},
207+
],
208+
selectionSet: {
209+
kind: Kind.SELECTION_SET,
210+
selections: [
211+
{
212+
alias: undefined,
213+
kind: Kind.FIELD,
214+
directives: undefined,
215+
selectionSet: undefined,
216+
arguments: undefined,
217+
name: {
218+
kind: Kind.NAME,
219+
value: 'field',
220+
},
221+
},
222+
],
223+
},
224+
});
225+
});
226+
227+
it('parses fragment spread arguments', () => {
228+
expect(
229+
parse('query x { ...x(var: 2) } fragment x($var: Int = 1) on Type { field }').definitions[0]
230+
).toHaveProperty('selectionSet.selections.0', {
231+
kind: Kind.FRAGMENT_SPREAD,
232+
directives: undefined,
233+
name: {
234+
kind: Kind.NAME,
235+
value: 'x',
236+
},
237+
arguments: [
238+
{
239+
kind: 'Argument',
240+
name: {
241+
kind: 'Name',
242+
value: 'var',
243+
},
244+
value: {
245+
kind: 'IntValue',
246+
value: '2',
247+
},
248+
},
249+
],
250+
});
251+
});
252+
169253
it('parses fields', () => {
170254
expect(() => parse('{ field: }')).toThrow();
171255
expect(() => parse('{ alias: field() }')).toThrow();

src/__tests__/printer.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,94 @@ describe('print', () => {
117117
).toBe('[Type!]');
118118
});
119119

120+
it('prints fragment-definition with variables', () => {
121+
expect(
122+
print({
123+
kind: Kind.FRAGMENT_DEFINITION,
124+
directives: [],
125+
name: {
126+
kind: Kind.NAME,
127+
value: 'x',
128+
},
129+
typeCondition: {
130+
kind: Kind.NAMED_TYPE,
131+
name: {
132+
kind: Kind.NAME,
133+
value: 'Type',
134+
},
135+
},
136+
variableDefinitions: [
137+
{
138+
kind: Kind.VARIABLE_DEFINITION,
139+
type: {
140+
kind: Kind.NAMED_TYPE,
141+
name: {
142+
kind: Kind.NAME,
143+
value: 'Int',
144+
},
145+
},
146+
variable: {
147+
kind: Kind.VARIABLE,
148+
name: {
149+
kind: Kind.NAME,
150+
value: 'var',
151+
},
152+
},
153+
defaultValue: {
154+
kind: Kind.INT,
155+
value: '1',
156+
},
157+
directives: [],
158+
},
159+
],
160+
selectionSet: {
161+
kind: Kind.SELECTION_SET,
162+
selections: [
163+
{
164+
alias: undefined,
165+
kind: Kind.FIELD,
166+
directives: [],
167+
selectionSet: undefined,
168+
arguments: [],
169+
name: {
170+
kind: Kind.NAME,
171+
value: 'field',
172+
},
173+
},
174+
],
175+
},
176+
} as any)
177+
).toBe(`fragment x($var: Int = 1) on Type {
178+
field
179+
}`);
180+
});
181+
182+
it('prints fragment-spread with arguments', () => {
183+
expect(
184+
print({
185+
kind: Kind.FRAGMENT_SPREAD,
186+
directives: [],
187+
name: {
188+
kind: Kind.NAME,
189+
value: 'x',
190+
},
191+
arguments: [
192+
{
193+
kind: 'Argument',
194+
name: {
195+
kind: 'Name',
196+
value: 'var',
197+
},
198+
value: {
199+
kind: 'IntValue',
200+
value: '2',
201+
},
202+
},
203+
],
204+
} as any)
205+
).toBe(`...x(var: 2)`);
206+
});
207+
120208
// NOTE: The shim won't throw for invalid AST nodes
121209
it('returns empty strings for invalid AST', () => {
122210
const badAST = { random: 'Data' };

src/ast.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export type FragmentSpreadNode = Or<
188188
{
189189
readonly kind: Kind.FRAGMENT_SPREAD;
190190
readonly name: NameNode;
191+
readonly arguments?: ReadonlyArray<ArgumentNode>;
191192
readonly directives?: ReadonlyArray<DirectiveNode>;
192193
readonly loc?: Location;
193194
}
@@ -209,6 +210,7 @@ export type FragmentDefinitionNode = Or<
209210
{
210211
readonly kind: Kind.FRAGMENT_DEFINITION;
211212
readonly name: NameNode;
213+
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
212214
readonly typeCondition: NamedTypeNode;
213215
readonly directives?: ReadonlyArray<DirectiveNode>;
214216
readonly selectionSet: SelectionSetNode;

src/kind.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export declare enum Kind {
88
SELECTION_SET = 'SelectionSet',
99
FIELD = 'Field',
1010
ARGUMENT = 'Argument',
11+
FRAGMENT_ARGUMENT = 'FragmentArgument',
1112
/** Fragments */
1213
FRAGMENT_SPREAD = 'FragmentSpread',
1314
INLINE_FRAGMENT = 'InlineFragment',

src/parser.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function advance(pattern: RegExp) {
2525
}
2626

2727
const leadingRe = / +(?=[^\s])/y;
28+
const nameRe = /[A-Za-z_][0-9A-Za-z_]*/y;
2829
function blockString(string: string) {
2930
const lines = string.split('\n');
3031
let out = '';
@@ -356,6 +357,7 @@ function selectionSet(): ast.SelectionSetNode {
356357
selections.push({
357358
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
358359
name: nameNode(),
360+
arguments: arguments_(false),
359361
directives: directives(false),
360362
});
361363
}
@@ -376,6 +378,7 @@ function selectionSet(): ast.SelectionSetNode {
376378
selections.push({
377379
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
378380
name: nameNode(),
381+
arguments: arguments_(false),
379382
directives: directives(false),
380383
});
381384
}
@@ -451,19 +454,27 @@ function variableDefinitions(): ast.VariableDefinitionNode[] | undefined {
451454
}
452455

453456
function fragmentDefinition(): ast.FragmentDefinitionNode {
454-
const name = nameNode();
455-
if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/)
456-
throw error('FragmentDefinition');
457+
let _name: string | undefined;
458+
let _condition: string | undefined;
459+
if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition');
460+
const _variableDefinitions = variableDefinitions();
461+
if (advance(nameRe) !== 'on') throw error('FragmentDefinition');
462+
ignored();
463+
if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition');
464+
ignored();
465+
const _directives = directives(false);
466+
if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition');
457467
ignored();
458468
return {
459469
kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
460-
name,
470+
name: { kind: 'Name' as Kind.NAME, value: _name },
461471
typeCondition: {
462472
kind: 'NamedType' as Kind.NAMED_TYPE,
463-
name: nameNode(),
473+
name: { kind: 'Name' as Kind.NAME, value: _condition },
464474
},
465-
directives: directives(false),
466-
selectionSet: selectionSetStart(),
475+
variableDefinitions: _variableDefinitions,
476+
directives: _directives,
477+
selectionSet: selectionSet(),
467478
};
468479
}
469480

src/printer.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ const MAX_LINE_LENGTH = 80;
4747

4848
let LF = '\n';
4949

50+
function Arguments(length: number, node: readonly ArgumentNode[]): string {
51+
const args = mapJoin(node, ', ', nodes.Argument);
52+
if (length + args.length + 2 > MAX_LINE_LENGTH) {
53+
return '(' +
54+
(LF += ' ') +
55+
mapJoin(node, LF, nodes.Argument) +
56+
(LF = LF.slice(0, -2)) +
57+
')';
58+
} else {
59+
return '(' + args + ')';
60+
}
61+
}
62+
5063
const nodes = {
5164
OperationDefinition(node: OperationDefinitionNode): string {
5265
let out: string = node.operation;
@@ -70,19 +83,8 @@ const nodes = {
7083
},
7184
Field(node: FieldNode): string {
7285
let out = node.alias ? node.alias.value + ': ' + node.name.value : node.name.value;
73-
if (node.arguments && node.arguments.length) {
74-
const args = mapJoin(node.arguments, ', ', nodes.Argument);
75-
if (out.length + args.length + 2 > MAX_LINE_LENGTH) {
76-
out +=
77-
'(' +
78-
(LF += ' ') +
79-
mapJoin(node.arguments, LF, nodes.Argument) +
80-
(LF = LF.slice(0, -2)) +
81-
')';
82-
} else {
83-
out += '(' + args + ')';
84-
}
85-
}
86+
if (node.arguments && node.arguments.length)
87+
out += Arguments(out.length, node.arguments);
8688
if (node.directives && node.directives.length)
8789
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
8890
if (node.selectionSet && node.selectionSet.selections.length) {
@@ -139,6 +141,8 @@ const nodes = {
139141
},
140142
FragmentSpread(node: FragmentSpreadNode): string {
141143
let out = '...' + node.name.value;
144+
if (node.arguments && node.arguments.length)
145+
out += Arguments(out.length, node.arguments);
142146
if (node.directives && node.directives.length)
143147
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);
144148
return out;
@@ -153,6 +157,8 @@ const nodes = {
153157
},
154158
FragmentDefinition(node: FragmentDefinitionNode): string {
155159
let out = 'fragment ' + node.name.value;
160+
if (node.variableDefinitions && node.variableDefinitions.length)
161+
out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')';
156162
out += ' on ' + node.typeCondition.name.value;
157163
if (node.directives && node.directives.length)
158164
out += ' ' + mapJoin(node.directives, ' ', nodes.Directive);

0 commit comments

Comments
 (0)