diff --git a/src/__tests__/__snapshots__/parser.test.ts.snap b/src/__tests__/__snapshots__/parser.test.ts.snap index 692ae95..2db9d13 100644 --- a/src/__tests__/__snapshots__/parser.test.ts.snap +++ b/src/__tests__/__snapshots__/parser.test.ts.snap @@ -185,6 +185,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "selectionSet": undefined, }, { + "arguments": [], "directives": [ { "arguments": [], @@ -644,7 +645,8 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "value": { "block": true, "kind": "StringValue", - "value": "block string uses """", + "value": "block string uses """ +", }, }, ], @@ -669,6 +671,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "value": "Friend", }, }, + "variableDefinitions": undefined, }, { "directives": [], diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 9e6808e..49ed020 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -106,6 +106,90 @@ describe('parse', () => { expect(() => parse('fragment Name on Type { field }')).not.toThrow(); }); + it('parses fragment variable-definitions', () => { + expect(parse('fragment x on Type ($var: Int = 1) { field }').definitions[0]).toEqual({ + kind: Kind.FRAGMENT_DEFINITION, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Type', + }, + }, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'var', + }, + }, + defaultValue: { + kind: Kind.INT, + value: '1', + }, + directives: [], + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + alias: undefined, + kind: Kind.FIELD, + directives: [], + selectionSet: undefined, + arguments: [], + name: { + kind: Kind.NAME, + value: 'field', + }, + }, + ], + }, + }); + }); + + it.only('parses fragment-spread arguments', () => { + expect( + parse('query x { ...x(var: 2) } fragment x on Type ($var: Int = 1) { field }').definitions[0] + ).toHaveProperty('selectionSet.selections.0', { + kind: Kind.FRAGMENT_SPREAD, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'var', + }, + value: { + kind: 'IntValue', + value: '2', + }, + }, + ], + }); + }); + it('parses fields', () => { expect(() => parse('{ field: }')).toThrow(); expect(() => parse('{ alias: field() }')).toThrow(); diff --git a/src/__tests__/printer.test.ts b/src/__tests__/printer.test.ts index daa7d8f..9cabe70 100644 --- a/src/__tests__/printer.test.ts +++ b/src/__tests__/printer.test.ts @@ -4,6 +4,7 @@ import * as graphql16 from 'graphql16'; import { parse } from '../parser'; import { print, printString, printBlockString } from '../printer'; import kitchenSinkAST from './fixtures/kitchen_sink.json'; +import { Kind } from 'src/kind'; function dedentString(string: string) { const trimmedStr = string @@ -115,6 +116,94 @@ describe('print', () => { ).toBe('[Type!]'); }); + it('prints fragment-definition with variables', () => { + expect( + print({ + kind: Kind.FRAGMENT_DEFINITION, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Type', + }, + }, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'var', + }, + }, + defaultValue: { + kind: Kind.INT, + value: '1', + }, + directives: [], + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + alias: undefined, + kind: Kind.FIELD, + directives: [], + selectionSet: undefined, + arguments: [], + name: { + kind: Kind.NAME, + value: 'field', + }, + }, + ], + }, + } as any) + ).toBe(`fragment x on Type($var: Int = 1) { + field +}`); + }); + + it('prints fragment-spread with arguments', () => { + expect( + print({ + kind: Kind.FRAGMENT_SPREAD, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'var', + }, + value: { + kind: 'IntValue', + value: '2', + }, + }, + ], + } as any) + ).toBe(`...x(var: 2)`); + }); + // NOTE: The shim won't throw for invalid AST nodes it('returns empty strings for invalid AST', () => { const badAST = { random: 'Data' }; diff --git a/src/ast.ts b/src/ast.ts index a69f8b8..7da8460 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -188,6 +188,7 @@ export type FragmentSpreadNode = Or< { readonly kind: Kind.FRAGMENT_SPREAD; readonly name: NameNode; + readonly arguments?: ReadonlyArray; readonly directives?: ReadonlyArray; readonly loc?: Location; } @@ -209,6 +210,7 @@ export type FragmentDefinitionNode = Or< { readonly kind: Kind.FRAGMENT_DEFINITION; readonly name: NameNode; + readonly variableDefinitions?: ReadonlyArray; readonly typeCondition: NamedTypeNode; readonly directives?: ReadonlyArray; readonly selectionSet: SelectionSetNode; diff --git a/src/parser.ts b/src/parser.ts index 728a594..fa2f05c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -319,10 +319,12 @@ function fragmentSpread(): ast.FragmentSpreadNode | ast.InlineFragmentNode | und const _idx = idx; let _name: ast.NameNode | undefined; if ((_name = name()) && _name.value !== 'on') { + const _arguments = arguments_(false); return { kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: _name, directives: directives(false), + arguments: _arguments, }; } else { idx = _idx; @@ -404,7 +406,9 @@ function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { if (!_name) throw error('FragmentDefinition'); ignored(); const _typeCondition = typeCondition(); + if (!_typeCondition) throw error('FragmentDefinition'); + const _variableDefinitions = variableDefinitions(); const _directives = directives(false); const _selectionSet = selectionSet(); if (!_selectionSet) throw error('FragmentDefinition'); @@ -412,6 +416,7 @@ function fragmentDefinition(): ast.FragmentDefinitionNode | undefined { kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, name: _name, typeCondition: _typeCondition, + variableDefinitions: _variableDefinitions.length ? _variableDefinitions : undefined, directives: _directives, selectionSet: _selectionSet, }; diff --git a/src/printer.ts b/src/printer.ts index 23550ee..5a593b3 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -97,6 +97,7 @@ const nodes: { }, FragmentSpread(node) { let out = '...' + node.name.value; + if (hasItems(node.arguments)) out += '(' + node.arguments.map(nodes.Argument!).join(', ') + ')'; if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); return out; }, @@ -109,6 +110,9 @@ const nodes: { FragmentDefinition(node) { let out = 'fragment ' + node.name.value; out += ' on ' + node.typeCondition.name.value; + if (hasItems(node.variableDefinitions)) { + out += '(' + node.variableDefinitions.map(nodes.VariableDefinition!).join(', ') + ')'; + } if (hasItems(node.directives)) out += ' ' + node.directives.map(nodes.Directive!).join(' '); return out + ' ' + print(node.selectionSet); },