Skip to content

Commit 2c3466e

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 1dbdadc commit 2c3466e

16 files changed

+692
-29
lines changed

src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export {
219219
parseValue,
220220
parseConstValue,
221221
parseType,
222+
parseSchemaCoordinate,
222223
// Print
223224
print,
224225
// Visit
@@ -240,6 +241,7 @@ export {
240241
isTypeDefinitionNode,
241242
isTypeSystemExtensionNode,
242243
isTypeExtensionNode,
244+
isSchemaCoordinateNode,
243245
} from './language/index.js';
244246

245247
export type {
@@ -316,6 +318,7 @@ export type {
316318
UnionTypeExtensionNode,
317319
EnumTypeExtensionNode,
318320
InputObjectTypeExtensionNode,
321+
SchemaCoordinateNode,
319322
} from './language/index.js';
320323

321324
// Execute GraphQL queries.
@@ -465,6 +468,8 @@ export {
465468
DangerousChangeType,
466469
findBreakingChanges,
467470
findDangerousChanges,
471+
resolveSchemaCoordinate,
472+
resolveASTSchemaCoordinate,
468473
} from './utilities/index.js';
469474

470475
export type {
@@ -494,4 +499,5 @@ export type {
494499
BreakingChange,
495500
DangerousChange,
496501
TypedQueryDocumentNode,
502+
ResolvedSchemaElement,
497503
} 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 {
@@ -972,6 +965,13 @@ describe('Lexer', () => {
972965
value: undefined,
973966
});
974967

968+
expect(lexOne('.')).to.contain({
969+
kind: TokenKind.DOT,
970+
start: 0,
971+
end: 1,
972+
value: undefined,
973+
});
974+
975975
expect(lexOne('...')).to.contain({
976976
kind: TokenKind.SPREAD,
977977
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

@@ -883,4 +889,129 @@ describe('Parser', () => {
883889
});
884890
});
885891
});
892+
893+
describe('parseSchemaCoordinate', () => {
894+
it('parses Name', () => {
895+
const result = parseSchemaCoordinate('MyType');
896+
expectJSON(result).toDeepEqual({
897+
kind: Kind.SCHEMA_COORDINATE,
898+
loc: { start: 0, end: 6 },
899+
ofDirective: false,
900+
name: {
901+
kind: Kind.NAME,
902+
loc: { start: 0, end: 6 },
903+
value: 'MyType',
904+
},
905+
memberName: undefined,
906+
argumentName: undefined,
907+
});
908+
});
909+
910+
it('parses Name . Name', () => {
911+
const result = parseSchemaCoordinate('MyType.field');
912+
expectJSON(result).toDeepEqual({
913+
kind: Kind.SCHEMA_COORDINATE,
914+
loc: { start: 0, end: 12 },
915+
ofDirective: false,
916+
name: {
917+
kind: Kind.NAME,
918+
loc: { start: 0, end: 6 },
919+
value: 'MyType',
920+
},
921+
memberName: {
922+
kind: Kind.NAME,
923+
loc: { start: 7, end: 12 },
924+
value: 'field',
925+
},
926+
argumentName: undefined,
927+
});
928+
});
929+
930+
it('rejects Name . Name . Name', () => {
931+
expectToThrowJSON(() =>
932+
parseSchemaCoordinate('MyType.field.deep'),
933+
).to.deep.equal({
934+
message: 'Syntax Error: Expected <EOF>, found ".".',
935+
locations: [{ line: 1, column: 13 }],
936+
});
937+
});
938+
939+
it('parses Name . Name ( Name : )', () => {
940+
const result = parseSchemaCoordinate('MyType.field(arg:)');
941+
expectJSON(result).toDeepEqual({
942+
kind: Kind.SCHEMA_COORDINATE,
943+
loc: { start: 0, end: 18 },
944+
ofDirective: false,
945+
name: {
946+
kind: Kind.NAME,
947+
loc: { start: 0, end: 6 },
948+
value: 'MyType',
949+
},
950+
memberName: {
951+
kind: Kind.NAME,
952+
loc: { start: 7, end: 12 },
953+
value: 'field',
954+
},
955+
argumentName: {
956+
kind: Kind.NAME,
957+
loc: { start: 13, end: 16 },
958+
value: 'arg',
959+
},
960+
});
961+
});
962+
963+
it('rejects Name . Name ( Name : Name )', () => {
964+
expectToThrowJSON(() =>
965+
parseSchemaCoordinate('MyType.field(arg: value)'),
966+
).to.deep.equal({
967+
message: 'Syntax Error: Expected ")", found Name "value".',
968+
locations: [{ line: 1, column: 19 }],
969+
});
970+
});
971+
972+
it('parses @ Name', () => {
973+
const result = parseSchemaCoordinate('@myDirective');
974+
expectJSON(result).toDeepEqual({
975+
kind: Kind.SCHEMA_COORDINATE,
976+
loc: { start: 0, end: 12 },
977+
ofDirective: true,
978+
name: {
979+
kind: Kind.NAME,
980+
loc: { start: 1, end: 12 },
981+
value: 'myDirective',
982+
},
983+
memberName: undefined,
984+
argumentName: undefined,
985+
});
986+
});
987+
988+
it('parses @ Name ( Name : )', () => {
989+
const result = parseSchemaCoordinate('@myDirective(arg:)');
990+
expectJSON(result).toDeepEqual({
991+
kind: Kind.SCHEMA_COORDINATE,
992+
loc: { start: 0, end: 18 },
993+
ofDirective: true,
994+
name: {
995+
kind: Kind.NAME,
996+
loc: { start: 1, end: 12 },
997+
value: 'myDirective',
998+
},
999+
memberName: undefined,
1000+
argumentName: {
1001+
kind: Kind.NAME,
1002+
loc: { start: 13, end: 16 },
1003+
value: 'arg',
1004+
},
1005+
});
1006+
});
1007+
1008+
it('rejects @ Name . Name', () => {
1009+
expectToThrowJSON(() =>
1010+
parseSchemaCoordinate('@myDirective.field'),
1011+
).to.deep.equal({
1012+
message: 'Syntax Error: Expected <EOF>, found ".".',
1013+
locations: [{ line: 1, column: 13 }],
1014+
});
1015+
});
1016+
});
8861017
});

src/language/__tests__/predicates-test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isDefinitionNode,
1010
isExecutableDefinitionNode,
1111
isNullabilityAssertionNode,
12+
isSchemaCoordinateNode,
1213
isSelectionNode,
1314
isTypeDefinitionNode,
1415
isTypeExtensionNode,
@@ -150,4 +151,10 @@ describe('AST node predicates', () => {
150151
'InputObjectTypeExtension',
151152
]);
152153
});
154+
155+
it('isSchemaCoordinateNode', () => {
156+
expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
157+
'SchemaCoordinate',
158+
]);
159+
});
153160
});

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', () => {
@@ -318,4 +318,18 @@ describe('Printer: Query document', () => {
318318
`),
319319
);
320320
});
321+
322+
it('prints schema coordinates', () => {
323+
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
324+
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
325+
'Name.field',
326+
);
327+
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
328+
'Name.field(arg:)',
329+
);
330+
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
331+
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
332+
'@name(arg:)',
333+
);
334+
});
321335
});

src/language/ast.ts

+14
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export type ASTNode =
182182
| UnionTypeExtensionNode
183183
| EnumTypeExtensionNode
184184
| InputObjectTypeExtensionNode
185+
| SchemaCoordinateNode
185186
| NonNullAssertionNode
186187
| ErrorBoundaryNode
187188
| ListNullabilityOperatorNode;
@@ -297,6 +298,8 @@ export const QueryDocumentKeys: {
297298
UnionTypeExtension: ['name', 'directives', 'types'],
298299
EnumTypeExtension: ['name', 'directives', 'values'],
299300
InputObjectTypeExtension: ['name', 'directives', 'fields'],
301+
302+
SchemaCoordinate: ['name', 'memberName', 'argumentName'],
300303
};
301304

302305
const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
@@ -795,3 +798,14 @@ export interface InputObjectTypeExtensionNode {
795798
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
796799
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
797800
}
801+
802+
/** Schema Coordinates */
803+
804+
export interface SchemaCoordinateNode {
805+
readonly kind: Kind.SCHEMA_COORDINATE;
806+
readonly loc?: Location | undefined;
807+
readonly ofDirective: boolean;
808+
readonly name: NameNode;
809+
readonly memberName?: NameNode | undefined;
810+
readonly argumentName?: NameNode | undefined;
811+
}

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';
@@ -92,6 +98,7 @@ export type {
9298
UnionTypeExtensionNode,
9399
EnumTypeExtensionNode,
94100
InputObjectTypeExtensionNode,
101+
SchemaCoordinateNode,
95102
} from './ast.js';
96103

97104
export {
@@ -106,6 +113,7 @@ export {
106113
isTypeDefinitionNode,
107114
isTypeSystemExtensionNode,
108115
isTypeExtensionNode,
116+
isSchemaCoordinateNode,
109117
} from './predicates.js';
110118

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

src/language/kinds.ts

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ enum Kind {
7272
UNION_TYPE_EXTENSION = 'UnionTypeExtension',
7373
ENUM_TYPE_EXTENSION = 'EnumTypeExtension',
7474
INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension',
75+
76+
/** Schema Coordinates */
77+
SCHEMA_COORDINATE = 'SchemaCoordinate',
7578
}
7679

7780
export { Kind };

0 commit comments

Comments
 (0)