Skip to content

Commit 9fc6c0b

Browse files
leebyronyaacovCR
authored andcommitted
Schema Coordinates
Implements graphql/graphql-spec#794 Adds: * DOT punctuator in lexer * Improvements to lexer errors around misuse of `.` * Minor improvement to parser core which simplified this addition * `SchemaCoordinate` node and `isSchemaCoodinate()` predicate * Support in `print()` and `visit()` * Added function `parseSchemaCoordinate()` since it is a parser entry point. * Added function `resolveSchemaCoordinate()` and `resolveASTSchemaCoordinate()` which implement the semantics (name mirrored from `buildASTSchema`) as well as the return type `ResolvedSchemaElement`
1 parent cca3f98 commit 9fc6c0b

16 files changed

+693
-30
lines changed

src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export {
223223
parseValue,
224224
parseConstValue,
225225
parseType,
226+
parseSchemaCoordinate,
226227
// Print
227228
print,
228229
// Visit
@@ -243,6 +244,7 @@ export {
243244
isTypeDefinitionNode,
244245
isTypeSystemExtensionNode,
245246
isTypeExtensionNode,
247+
isSchemaCoordinateNode,
246248
} from './language/index.js';
247249

248250
export type {
@@ -315,6 +317,7 @@ export type {
315317
UnionTypeExtensionNode,
316318
EnumTypeExtensionNode,
317319
InputObjectTypeExtensionNode,
320+
SchemaCoordinateNode,
318321
} from './language/index.js';
319322

320323
// Execute GraphQL queries.
@@ -482,6 +485,8 @@ export {
482485
findBreakingChanges,
483486
findDangerousChanges,
484487
findSchemaChanges,
488+
resolveSchemaCoordinate,
489+
resolveASTSchemaCoordinate,
485490
} from './utilities/index.js';
486491

487492
export type {
@@ -512,4 +517,5 @@ export type {
512517
SafeChange,
513518
DangerousChange,
514519
TypedQueryDocumentNode,
520+
ResolvedSchemaElement,
515521
} from './utilities/index.js';

src/language/__tests__/lexer-test.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,6 @@ describe('Lexer', () => {
165165
});
166166
});
167167

168-
it('reports unexpected characters', () => {
169-
expectSyntaxError('.').to.deep.equal({
170-
message: 'Syntax Error: Unexpected character: ".".',
171-
locations: [{ line: 1, column: 1 }],
172-
});
173-
});
174-
175168
it('errors respect whitespace', () => {
176169
let caughtError;
177170
try {
@@ -965,6 +958,13 @@ describe('Lexer', () => {
965958
value: undefined,
966959
});
967960

961+
expect(lexOne('.')).to.contain({
962+
kind: TokenKind.DOT,
963+
start: 0,
964+
end: 1,
965+
value: undefined,
966+
});
967+
968968
expect(lexOne('...')).to.contain({
969969
kind: TokenKind.SPREAD,
970970
start: 0,

src/language/__tests__/parser-test.ts

+132-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
1111
import { inspect } from '../../jsutils/inspect.js';
1212

1313
import { Kind } from '../kinds.js';
14-
import { parse, parseConstValue, parseType, parseValue } from '../parser.js';
14+
import {
15+
parse,
16+
parseConstValue,
17+
parseSchemaCoordinate,
18+
parseType,
19+
parseValue,
20+
} from '../parser.js';
1521
import { Source } from '../source.js';
1622
import { TokenKind } from '../tokenKind.js';
1723

@@ -679,4 +685,129 @@ describe('Parser', () => {
679685
});
680686
});
681687
});
688+
689+
describe('parseSchemaCoordinate', () => {
690+
it('parses Name', () => {
691+
const result = parseSchemaCoordinate('MyType');
692+
expectJSON(result).toDeepEqual({
693+
kind: Kind.SCHEMA_COORDINATE,
694+
loc: { start: 0, end: 6 },
695+
ofDirective: false,
696+
name: {
697+
kind: Kind.NAME,
698+
loc: { start: 0, end: 6 },
699+
value: 'MyType',
700+
},
701+
memberName: undefined,
702+
argumentName: undefined,
703+
});
704+
});
705+
706+
it('parses Name . Name', () => {
707+
const result = parseSchemaCoordinate('MyType.field');
708+
expectJSON(result).toDeepEqual({
709+
kind: Kind.SCHEMA_COORDINATE,
710+
loc: { start: 0, end: 12 },
711+
ofDirective: false,
712+
name: {
713+
kind: Kind.NAME,
714+
loc: { start: 0, end: 6 },
715+
value: 'MyType',
716+
},
717+
memberName: {
718+
kind: Kind.NAME,
719+
loc: { start: 7, end: 12 },
720+
value: 'field',
721+
},
722+
argumentName: undefined,
723+
});
724+
});
725+
726+
it('rejects Name . Name . Name', () => {
727+
expectToThrowJSON(() =>
728+
parseSchemaCoordinate('MyType.field.deep'),
729+
).to.deep.equal({
730+
message: 'Syntax Error: Expected <EOF>, found ".".',
731+
locations: [{ line: 1, column: 13 }],
732+
});
733+
});
734+
735+
it('parses Name . Name ( Name : )', () => {
736+
const result = parseSchemaCoordinate('MyType.field(arg:)');
737+
expectJSON(result).toDeepEqual({
738+
kind: Kind.SCHEMA_COORDINATE,
739+
loc: { start: 0, end: 18 },
740+
ofDirective: false,
741+
name: {
742+
kind: Kind.NAME,
743+
loc: { start: 0, end: 6 },
744+
value: 'MyType',
745+
},
746+
memberName: {
747+
kind: Kind.NAME,
748+
loc: { start: 7, end: 12 },
749+
value: 'field',
750+
},
751+
argumentName: {
752+
kind: Kind.NAME,
753+
loc: { start: 13, end: 16 },
754+
value: 'arg',
755+
},
756+
});
757+
});
758+
759+
it('rejects Name . Name ( Name : Name )', () => {
760+
expectToThrowJSON(() =>
761+
parseSchemaCoordinate('MyType.field(arg: value)'),
762+
).to.deep.equal({
763+
message: 'Syntax Error: Expected ")", found Name "value".',
764+
locations: [{ line: 1, column: 19 }],
765+
});
766+
});
767+
768+
it('parses @ Name', () => {
769+
const result = parseSchemaCoordinate('@myDirective');
770+
expectJSON(result).toDeepEqual({
771+
kind: Kind.SCHEMA_COORDINATE,
772+
loc: { start: 0, end: 12 },
773+
ofDirective: true,
774+
name: {
775+
kind: Kind.NAME,
776+
loc: { start: 1, end: 12 },
777+
value: 'myDirective',
778+
},
779+
memberName: undefined,
780+
argumentName: undefined,
781+
});
782+
});
783+
784+
it('parses @ Name ( Name : )', () => {
785+
const result = parseSchemaCoordinate('@myDirective(arg:)');
786+
expectJSON(result).toDeepEqual({
787+
kind: Kind.SCHEMA_COORDINATE,
788+
loc: { start: 0, end: 18 },
789+
ofDirective: true,
790+
name: {
791+
kind: Kind.NAME,
792+
loc: { start: 1, end: 12 },
793+
value: 'myDirective',
794+
},
795+
memberName: undefined,
796+
argumentName: {
797+
kind: Kind.NAME,
798+
loc: { start: 13, end: 16 },
799+
value: 'arg',
800+
},
801+
});
802+
});
803+
804+
it('rejects @ Name . Name', () => {
805+
expectToThrowJSON(() =>
806+
parseSchemaCoordinate('@myDirective.field'),
807+
).to.deep.equal({
808+
message: 'Syntax Error: Expected <EOF>, found ".".',
809+
locations: [{ line: 1, column: 13 }],
810+
});
811+
});
812+
});
682813
});

src/language/__tests__/predicates-test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isConstValueNode,
99
isDefinitionNode,
1010
isExecutableDefinitionNode,
11+
isSchemaCoordinateNode,
1112
isSelectionNode,
1213
isTypeDefinitionNode,
1314
isTypeExtensionNode,
@@ -141,4 +142,10 @@ describe('AST node predicates', () => {
141142
'InputObjectTypeExtension',
142143
]);
143144
});
145+
146+
it('isSchemaCoordinateNode', () => {
147+
expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
148+
'SchemaCoordinate',
149+
]);
150+
});
144151
});

src/language/__tests__/printer-test.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { dedent, dedentString } from '../../__testUtils__/dedent.js';
55
import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
66

77
import { Kind } from '../kinds.js';
8-
import { parse } from '../parser.js';
8+
import { parse, parseSchemaCoordinate } from '../parser.js';
99
import { print } from '../printer.js';
1010

1111
describe('Printer: Query document', () => {
@@ -299,4 +299,18 @@ describe('Printer: Query document', () => {
299299
`),
300300
);
301301
});
302+
303+
it('prints schema coordinates', () => {
304+
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
305+
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
306+
'Name.field',
307+
);
308+
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
309+
'Name.field(arg:)',
310+
);
311+
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
312+
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
313+
'@name(arg:)',
314+
);
315+
});
302316
});

src/language/ast.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ export type ASTNode =
181181
| InterfaceTypeExtensionNode
182182
| UnionTypeExtensionNode
183183
| EnumTypeExtensionNode
184-
| InputObjectTypeExtensionNode;
184+
| InputObjectTypeExtensionNode
185+
| SchemaCoordinateNode;
185186

186187
/**
187188
* Utility type listing all nodes indexed by their kind.
@@ -287,6 +288,8 @@ export const QueryDocumentKeys: {
287288
UnionTypeExtension: ['name', 'directives', 'types'],
288289
EnumTypeExtension: ['name', 'directives', 'values'],
289290
InputObjectTypeExtension: ['name', 'directives', 'fields'],
291+
292+
SchemaCoordinate: ['name', 'memberName', 'argumentName'],
290293
};
291294

292295
const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
@@ -762,3 +765,14 @@ export interface InputObjectTypeExtensionNode {
762765
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
763766
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
764767
}
768+
769+
/** Schema Coordinates */
770+
771+
export interface SchemaCoordinateNode {
772+
readonly kind: typeof Kind.SCHEMA_COORDINATE;
773+
readonly loc?: Location | undefined;
774+
readonly ofDirective: boolean;
775+
readonly name: NameNode;
776+
readonly memberName?: NameNode | undefined;
777+
readonly argumentName?: NameNode | undefined;
778+
}

src/language/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ export { TokenKind } from './tokenKind.js';
1111

1212
export { Lexer } from './lexer.js';
1313

14-
export { parse, parseValue, parseConstValue, parseType } from './parser.js';
14+
export {
15+
parse,
16+
parseValue,
17+
parseConstValue,
18+
parseType,
19+
parseSchemaCoordinate,
20+
} from './parser.js';
1521
export type { ParseOptions } from './parser.js';
1622

1723
export { print } from './printer.js';
@@ -88,6 +94,7 @@ export type {
8894
UnionTypeExtensionNode,
8995
EnumTypeExtensionNode,
9096
InputObjectTypeExtensionNode,
97+
SchemaCoordinateNode,
9198
} from './ast.js';
9299

93100
export {
@@ -101,6 +108,7 @@ export {
101108
isTypeDefinitionNode,
102109
isTypeSystemExtensionNode,
103110
isTypeExtensionNode,
111+
isSchemaCoordinateNode,
104112
} from './predicates.js';
105113

106114
export { DirectiveLocation } from './directiveLocation.js';

src/language/kinds.ts

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export const Kind = {
6767
UNION_TYPE_EXTENSION: 'UnionTypeExtension' as const,
6868
ENUM_TYPE_EXTENSION: 'EnumTypeExtension' as const,
6969
INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension' as const,
70+
71+
/** Schema Coordinates */
72+
SCHEMA_COORDINATE: 'SchemaCoordinate' as const,
7073
};
7174
// eslint-disable-next-line @typescript-eslint/no-redeclare
7275
export type Kind = (typeof Kind)[keyof typeof Kind];

0 commit comments

Comments
 (0)