Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
DirectiveExtensionNode,
SchemaCoordinateNode,
TypeCoordinateNode,
MemberCoordinateNode,
Expand Down
2 changes: 2 additions & 0 deletions src/language/__tests__/predicates-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('AST node predicates', () => {
it('isDefinitionNode', () => {
expect(filterNodes(isDefinitionNode)).to.deep.equal([
'DirectiveDefinition',
'DirectiveExtension',
'EnumTypeDefinition',
'EnumTypeExtension',
'FragmentDefinition',
Expand Down Expand Up @@ -122,6 +123,7 @@ describe('AST node predicates', () => {

it('isTypeSystemExtensionNode', () => {
expect(filterNodes(isTypeSystemExtensionNode)).to.deep.equal([
'DirectiveExtension',
'EnumTypeExtension',
'InputObjectTypeExtension',
'InterfaceTypeExtension',
Expand Down
15 changes: 15 additions & 0 deletions src/language/__tests__/printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ describe('Printer: Query document', () => {
`);
});

it('Experimental: prints directives on directives', () => {
const queryASTWithDirectiveDirective = parse(
`
directive @foo @bar on FIELD_DEFINITION
extend directive @foo @baz
`,
{ experimentalDirectivesOnDirectiveDefinitions: true },
);
expect(print(queryASTWithDirectiveDirective)).to.equal(dedent`
directive @foo @bar on FIELD_DEFINITION

extend directive @foo @baz
`);
});

it('correctly prints fragment defined arguments', () => {
const fragmentWithArgumentDefinition = parse(
`
Expand Down
43 changes: 43 additions & 0 deletions src/language/__tests__/schema-parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,7 @@ input Hello {
{
kind: 'DirectiveDefinition',
description: undefined,
directives: undefined,
name: {
kind: 'Name',
value: 'foo',
Expand Down Expand Up @@ -1068,6 +1069,7 @@ input Hello {
{
kind: 'DirectiveDefinition',
description: undefined,
directives: undefined,
name: {
kind: 'Name',
value: 'foo',
Expand Down Expand Up @@ -1103,6 +1105,47 @@ input Hello {
});
});

it('Directive extension with experimental option enabled', () => {
const doc = parse('extend directive @foo @bar', {
experimentalDirectivesOnDirectiveDefinitions: true,
});

expectJSON(doc).toDeepEqual({
kind: 'Document',
definitions: [
{
kind: 'DirectiveExtension',
name: {
kind: 'Name',
value: 'foo',
loc: { start: 18, end: 21 },
},
directives: [
{
kind: 'Directive',
name: {
kind: 'Name',
value: 'bar',
loc: { start: 23, end: 26 },
},
arguments: undefined,
loc: { start: 22, end: 26 },
},
],
loc: { start: 0, end: 26 },
},
],
loc: { start: 0, end: 26 },
});
});

it('Directive extension requires experimental option', () => {
expectSyntaxError('extend directive @foo @bar').to.deep.equal({
message: 'Syntax Error: Unexpected Name "directive".',
locations: [{ line: 1, column: 8 }],
});
});

it('parses kitchen sink schema', () => {
expect(() => parse(kitchenSinkSDL)).to.not.throw();
});
Expand Down
23 changes: 21 additions & 2 deletions src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type ASTNode =
| UnionTypeExtensionNode
| EnumTypeExtensionNode
| InputObjectTypeExtensionNode
| DirectiveExtensionNode
| TypeCoordinateNode
| MemberCoordinateNode
| ArgumentCoordinateNode
Expand Down Expand Up @@ -290,9 +291,16 @@ export const QueryDocumentKeys: {
EnumValueDefinition: ['description', 'name', 'directives'],
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],

DirectiveDefinition: ['description', 'name', 'arguments', 'locations'],
DirectiveDefinition: [
'description',
'name',
'arguments',
'directives',
'locations',
],

SchemaExtension: ['directives', 'operationTypes'],
DirectiveExtension: ['name', 'directives'],

ScalarTypeExtension: ['name', 'directives'],
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
Expand Down Expand Up @@ -719,13 +727,17 @@ export interface DirectiveDefinitionNode {
readonly description?: StringValueNode | undefined;
readonly name: NameNode;
readonly arguments?: ReadonlyArray<InputValueDefinitionNode> | undefined;
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
readonly repeatable: boolean;
readonly locations: ReadonlyArray<NameNode>;
}

/** Type System Extensions */

export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;
export type TypeSystemExtensionNode =
| SchemaExtensionNode
| TypeExtensionNode
| DirectiveExtensionNode;

export interface SchemaExtensionNode {
readonly kind: KindTypeMap['SCHEMA_EXTENSION'];
Expand Down Expand Up @@ -795,6 +807,13 @@ export interface InputObjectTypeExtensionNode {
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
}

export interface DirectiveExtensionNode {
readonly kind: KindTypeMap['DIRECTIVE_EXTENSION'];
readonly loc?: Location | undefined;
readonly name: NameNode;
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
}

/** Schema Coordinates */

export type SchemaCoordinateNode =
Expand Down
1 change: 1 addition & 0 deletions src/language/directiveLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const DirectiveLocation = {
ENUM_VALUE: 'ENUM_VALUE' as const,
INPUT_OBJECT: 'INPUT_OBJECT' as const,
INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION' as const,
DIRECTIVE_DEFINITION: 'DIRECTIVE_DEFINITION' as const,
FRAGMENT_VARIABLE_DEFINITION: 'FRAGMENT_VARIABLE_DEFINITION' as const,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare
Expand Down
1 change: 1 addition & 0 deletions src/language/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
DirectiveExtensionNode,
SchemaCoordinateNode,
TypeCoordinateNode,
MemberCoordinateNode,
Expand Down
2 changes: 2 additions & 0 deletions src/language/kinds_.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export type DIRECTIVE_DEFINITION = typeof DIRECTIVE_DEFINITION;
/** Type System Extensions */
export const SCHEMA_EXTENSION = 'SchemaExtension';
export type SCHEMA_EXTENSION = typeof SCHEMA_EXTENSION;
export const DIRECTIVE_EXTENSION = 'DirectiveExtension';
export type DIRECTIVE_EXTENSION = typeof DIRECTIVE_EXTENSION;

/** Type Extensions */
export const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
Expand Down
42 changes: 42 additions & 0 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
DirectiveArgumentCoordinateNode,
DirectiveCoordinateNode,
DirectiveDefinitionNode,
DirectiveExtensionNode,
DirectiveNode,
DocumentNode,
EnumTypeDefinitionNode,
Expand Down Expand Up @@ -117,6 +118,18 @@ export interface ParseOptions {
*/
experimentalFragmentArguments?: boolean | undefined;

/**
* EXPERIMENTAL:
*
* If enabled, the parser will parse directives on directive definitions.
* This syntax is not part of the GraphQL specification and may change.
*
* ```graphql
* directive @foo @bar on FIELD
* ```
*/
experimentalDirectivesOnDirectiveDefinitions?: boolean | undefined;

/**
* You may override the Lexer class used to lex the source; this is used by
* schema coordinates to introduce a lexer with a restricted syntax.
Expand Down Expand Up @@ -1218,6 +1231,7 @@ export class Parser {
* - UnionTypeExtension
* - EnumTypeExtension
* - InputObjectTypeDefinition
* - DirectiveDefinitionExtension
*/
parseTypeSystemExtension(): TypeSystemExtensionNode {
const keywordToken = this._lexer.lookahead();
Expand All @@ -1238,6 +1252,11 @@ export class Parser {
return this.parseEnumTypeExtension();
case 'input':
return this.parseInputObjectTypeExtension();
case 'directive':
if (this._options.experimentalDirectivesOnDirectiveDefinitions) {
return this.parseDirectiveDefinitionExtension();
}
break;
}
}

Expand Down Expand Up @@ -1420,6 +1439,23 @@ export class Parser {
});
}

parseDirectiveDefinitionExtension(): DirectiveExtensionNode {
const start = this._lexer.token;
this.expectKeyword('extend');
this.expectKeyword('directive');
this.expectToken(TokenKind.AT);
const name = this.parseName();
const directives = this.parseConstDirectives();
if (directives === undefined) {
throw this.unexpected();
}
return this.node<DirectiveExtensionNode>(start, {
kind: Kind.DIRECTIVE_EXTENSION,
name,
directives,
});
}

/**
* ```
* DirectiveDefinition :
Expand All @@ -1433,6 +1469,10 @@ export class Parser {
this.expectToken(TokenKind.AT);
const name = this.parseName();
const args = this.parseArgumentDefs();
const directives = this._options
.experimentalDirectivesOnDirectiveDefinitions
? this.parseConstDirectives()
: undefined;
const repeatable = this.expectOptionalKeyword('repeatable');
this.expectKeyword('on');
const locations = this.parseDirectiveLocations();
Expand All @@ -1441,6 +1481,7 @@ export class Parser {
description,
name,
arguments: args,
directives,
repeatable,
locations,
});
Expand Down Expand Up @@ -1481,6 +1522,7 @@ export class Parser {
* `ENUM_VALUE`
* `INPUT_OBJECT`
* `INPUT_FIELD_DEFINITION`
* `DIRECTIVE_DEFINITION`
*/
parseDirectiveLocation(): NameNode {
const start = this._lexer.token;
Expand Down
6 changes: 5 additions & 1 deletion src/language/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ export function isTypeDefinitionNode(
export function isTypeSystemExtensionNode(
node: ASTNode,
): node is TypeSystemExtensionNode {
return node.kind === Kind.SCHEMA_EXTENSION || isTypeExtensionNode(node);
return (
node.kind === Kind.SCHEMA_EXTENSION ||
node.kind === Kind.DIRECTIVE_EXTENSION ||
isTypeExtensionNode(node)
);
}

export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode {
Expand Down
15 changes: 14 additions & 1 deletion src/language/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,21 @@ const printDocASTReducer: ASTReducer<string> = {
},

DirectiveDefinition: {
leave: ({ description, name, arguments: args, repeatable, locations }) =>
leave: ({
description,
name,
arguments: args,
directives,
repeatable,
locations,
}) =>
wrap('', description, '\n') +
'directive @' +
name +
(hasMultilineItems(args)
? wrap('(\n', indent(join(args, '\n')), '\n)')
: wrap('(', join(args, ', '), ')')) +
wrap(' ', join(directives, ' ')) +
(repeatable ? ' repeatable' : '') +
' on ' +
join(locations, ' | '),
Expand Down Expand Up @@ -328,6 +336,11 @@ const printDocASTReducer: ASTReducer<string> = {
join(['extend input', name, join(directives, ' '), block(fields)], ' '),
},

DirectiveExtension: {
leave: ({ name, directives }) =>
join(['extend directive @' + name, join(directives, ' ')], ' '),
},

// Schema Coordinates

TypeCoordinate: { leave: ({ name }) => name },
Expand Down
16 changes: 16 additions & 0 deletions src/type/__tests__/directive-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ describe('Type System: Directive', () => {
});
});

it('defines a deprecated directive', () => {
const directive = new GraphQLDirective({
name: 'Foo',
locations: [DirectiveLocation.QUERY],
deprecationReason: 'Some reason',
});

expect(directive).to.deep.include({
name: 'Foo',
args: [],
isRepeatable: false,
locations: ['QUERY'],
deprecationReason: 'Some reason',
});
});

it('can be stringified, JSON.stringified and Object.toStringified', () => {
const directive = new GraphQLDirective({
name: 'Foo',
Expand Down
Loading
Loading