Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/good-spiders-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphql.web': minor
---

Add support for variable definitions on fragments and arguments on fragment spreads (Fragment Arguments Spec Addition)
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/parser.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
"selectionSet": undefined,
},
{
"arguments": undefined,
"directives": [
{
"arguments": undefined,
Expand Down Expand Up @@ -669,6 +670,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = `
"value": "Friend",
},
},
"variableDefinitions": undefined,
},
{
"directives": undefined,
Expand Down
99 changes: 99 additions & 0 deletions src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,105 @@ describe('parse', () => {
expect(() => parse('fragment Name on Type { field }')).not.toThrow();
});

it('parses fragment variable definitions', () => {
expect(parse('fragment x($var: Int = 1) on Type { field }').definitions[0]).toEqual({
kind: Kind.FRAGMENT_DEFINITION,
directives: undefined,
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: undefined,
},
],
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [
{
alias: undefined,
kind: Kind.FIELD,
directives: undefined,
selectionSet: undefined,
arguments: undefined,
name: {
kind: Kind.NAME,
value: 'field',
},
},
],
},
});
});

it('parses fragment spread arguments', () => {
expect(parse('query x { ...x(varA: 2, varB: $var) }').definitions[0]).toHaveProperty(
'selectionSet.selections.0',
{
kind: Kind.FRAGMENT_SPREAD,
directives: undefined,
name: {
kind: Kind.NAME,
value: 'x',
},
arguments: [
{
kind: 'FragmentArgument',
name: {
kind: 'Name',
value: 'varA',
},
value: {
kind: 'IntValue',
value: '2',
},
},
{
kind: 'FragmentArgument',
name: {
kind: 'Name',
value: 'varB',
},
value: {
kind: 'Variable',
name: {
kind: 'Name',
value: 'var',
},
},
},
],
}
);
});

it('parses fields', () => {
expect(() => parse('{ field: }')).toThrow();
expect(() => parse('{ alias: field() }')).toThrow();
Expand Down
88 changes: 88 additions & 0 deletions src/__tests__/printer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,94 @@
).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($var: Int = 1) on Type {
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' };
Expand Down Expand Up @@ -184,22 +272,22 @@

it('Handles empty array selections', () => {
const document: DocumentNode = {
kind: Kind.DOCUMENT,

Check failure on line 275 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.DOCUMENT' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.DOCUMENT'.

Check failure on line 275 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.DOCUMENT' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.DOCUMENT'.
definitions: [
{
kind: Kind.OPERATION_DEFINITION,

Check failure on line 278 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.OPERATION_DEFINITION' is not assignable to type 'Kind.OPERATION_DEFINITION | Kind.FRAGMENT_DEFINITION | Kind.SCHEMA_DEFINITION | Kind.SCALAR_TYPE_DEFINITION | Kind.OBJECT_TYPE_DEFINITION | Kind.INTERFACE_TYPE_DEFINITION | Kind.UNION_TYPE_DEFINITION | Kind.ENUM_TYPE_DEFINITION | Kind.INPUT_OBJECT_TYPE_DEFINITION | Kind.DIRECTIVE_DEFINITION | Kind.SCHEMA_EXTENSION |...'.

Check failure on line 278 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.OPERATION_DEFINITION' is not assignable to type 'Kind.OPERATION_DEFINITION | Kind.FRAGMENT_DEFINITION | Kind.SCHEMA_DEFINITION | Kind.SCALAR_TYPE_DEFINITION | Kind.OBJECT_TYPE_DEFINITION | Kind.INTERFACE_TYPE_DEFINITION | Kind.UNION_TYPE_DEFINITION | Kind.ENUM_TYPE_DEFINITION | Kind.INPUT_OBJECT_TYPE_DEFINITION | Kind.DIRECTIVE_DEFINITION | Kind.SCHEMA_EXTENSION |...'.
operation: OperationTypeNode.QUERY,
name: undefined,
selectionSet: {
kind: Kind.SELECTION_SET,

Check failure on line 282 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.SELECTION_SET' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.SELECTION_SET'.

Check failure on line 282 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.SELECTION_SET' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.SELECTION_SET'.
selections: [
{
kind: Kind.FIELD,

Check failure on line 285 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.FIELD' is not assignable to type 'Kind.FIELD | Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT'.

Check failure on line 285 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.FIELD' is not assignable to type 'Kind.FIELD | Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT'.
name: { kind: Kind.NAME, value: 'id' },

Check failure on line 286 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.NAME' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.NAME'.

Check failure on line 286 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.NAME' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.NAME'.
alias: undefined,
arguments: [],
directives: [],
selectionSet: { kind: Kind.SELECTION_SET, selections: [] },

Check failure on line 290 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.SELECTION_SET' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.SELECTION_SET'.

Check failure on line 290 in src/__tests__/printer.test.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.SELECTION_SET' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.SELECTION_SET'.
},
],
},
Expand Down
13 changes: 12 additions & 1 deletion src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ASTNode = Or<
| FieldNode
| ArgumentNode
| FragmentSpreadNode
| FragmentArgumentNode
| InlineFragmentNode
| FragmentDefinitionNode
| IntValueNode
Expand Down Expand Up @@ -185,6 +186,13 @@ export type ConstArgumentNode = Or<
}
>;

export type FragmentArgumentNode = {
readonly kind: Kind.FRAGMENT_ARGUMENT;
readonly name: NameNode;
readonly value: ValueNode;
readonly loc?: Location;
};

export type FragmentSpreadNode = Or<
GraphQL.FragmentSpreadNode,
{
Expand All @@ -193,7 +201,9 @@ export type FragmentSpreadNode = Or<
readonly directives?: ReadonlyArray<DirectiveNode>;
readonly loc?: Location;
}
>;
> & {
readonly arguments?: ReadonlyArray<FragmentArgumentNode>;
};

export type InlineFragmentNode = Or<
GraphQL.InlineFragmentNode,
Expand All @@ -212,6 +222,7 @@ export type FragmentDefinitionNode = Or<
readonly kind: Kind.FRAGMENT_DEFINITION;
readonly name: NameNode;
readonly description?: StringValueNode;
readonly variableDefinitions?: ReadonlyArray<VariableDefinitionNode>;
readonly typeCondition: NamedTypeNode;
readonly directives?: ReadonlyArray<DirectiveNode>;
readonly selectionSet: SelectionSetNode;
Expand Down
1 change: 1 addition & 0 deletions src/kind.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export declare enum Kind {
SELECTION_SET = 'SelectionSet',
FIELD = 'Field',
ARGUMENT = 'Argument',
FRAGMENT_ARGUMENT = 'FragmentArgument',
/** Fragments */
FRAGMENT_SPREAD = 'FragmentSpread',
INLINE_FRAGMENT = 'InlineFragment',
Expand Down
38 changes: 28 additions & 10 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
}

const leadingRe = / +(?=[^\s])/y;
const nameRe = /[A-Za-z_][0-9A-Za-z_]*/y;
function blockString(string: string) {
const lines = string.split('\n');
let out = '';
Expand Down Expand Up @@ -84,7 +85,7 @@

function nameNode(): ast.NameNode {
return {
kind: 'Name' as Kind.NAME,

Check failure on line 88 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.NAME' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.NAME'.

Check failure on line 88 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.NAME' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.NAME'.
value: name(),
};
}
Expand All @@ -106,7 +107,7 @@
idx++;
ignored();
return {
kind: 'ListValue' as Kind.LIST,

Check failure on line 110 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.LIST' is not assignable to type 'Kind.VARIABLE | Kind.INT | Kind.FLOAT | Kind.STRING | Kind.BOOLEAN | Kind.NULL | Kind.ENUM | Kind.LIST | Kind.OBJECT'. Did you mean 'Kind.INT'?

Check failure on line 110 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'Kind.LIST' is not assignable to type 'Kind.VARIABLE | Kind.INT | Kind.FLOAT | Kind.STRING | Kind.BOOLEAN | Kind.NULL | Kind.ENUM | Kind.LIST | Kind.OBJECT'. Did you mean 'Kind.INT'?
values,
};

Expand All @@ -119,7 +120,7 @@
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('ObjectField');
ignored();
fields.push({
kind: 'ObjectField' as Kind.OBJECT_FIELD,

Check failure on line 123 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.OBJECT_FIELD' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.OBJECT_FIELD'.

Check failure on line 123 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Checks

Type 'import("/home/runner/work/graphql.web/graphql.web/src/kind").Kind.OBJECT_FIELD' is not assignable to type 'import("/home/runner/work/graphql.web/graphql.web/node_modules/.pnpm/[email protected]/node_modules/graphql/language/kinds").Kind.OBJECT_FIELD'.
name,
value: value(constant),
});
Expand Down Expand Up @@ -245,17 +246,22 @@
};
}

function arguments_(constant: boolean): ast.ArgumentNode[] | undefined {
function arguments_(
constant: boolean,
fragmentArgument?: boolean
): ast.ArgumentNode[] | ast.FragmentArgumentNode[] | undefined {
if (input.charCodeAt(idx) === 40 /*'('*/) {
const args: ast.ArgumentNode[] = [];
const args: ast.ArgumentNode[] | ast.FragmentArgumentNode[] = [];
idx++;
ignored();
do {
const name = nameNode();
if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument');
ignored();
args.push({
kind: 'Argument' as Kind.ARGUMENT,
kind: fragmentArgument
? ('FragmentArgument' as Kind.FRAGMENT_ARGUMENT)
: ('Argument' as Kind.ARGUMENT),
name,
value: value(constant),
});
Expand Down Expand Up @@ -357,6 +363,8 @@
selections.push({
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
name: nameNode(),
// @ts-expect-error
arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[],
directives: directives(false),
});
}
Expand All @@ -377,6 +385,8 @@
selections.push({
kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD,
name: nameNode(),
// @ts-expect-error
arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[],
directives: directives(false),
});
}
Expand Down Expand Up @@ -460,19 +470,27 @@
}

function fragmentDefinition(description?: ast.StringValueNode): ast.FragmentDefinitionNode {
const name = nameNode();
if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/)
throw error('FragmentDefinition');
let _name: string | undefined;
let _condition: string | undefined;
if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition');
const _variableDefinitions = variableDefinitions();
if (advance(nameRe) !== 'on') throw error('FragmentDefinition');
ignored();
if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition');
ignored();
const _directives = directives(false);
if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition');
ignored();
const fragDef: ast.FragmentDefinitionNode = {
kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION,
name,
name: { kind: 'Name' as Kind.NAME, value: _name },
typeCondition: {
kind: 'NamedType' as Kind.NAMED_TYPE,
name: nameNode(),
name: { kind: 'Name' as Kind.NAME, value: _condition },
},
directives: directives(false),
selectionSet: selectionSetStart(),
variableDefinitions: _variableDefinitions,
directives: _directives,
selectionSet: selectionSet(),
};
if (description) {
fragDef.description = description;
Expand Down
Loading
Loading