Skip to content

Commit 934a631

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 5c7d4d1 commit 934a631

16 files changed

+695
-7
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 {
@@ -315,6 +317,7 @@ export type {
315317
UnionTypeExtensionNode,
316318
EnumTypeExtensionNode,
317319
InputObjectTypeExtensionNode,
320+
SchemaCoordinateNode,
318321
} from './language/index.js';
319322

320323
// Execute GraphQL queries.
@@ -464,6 +467,8 @@ export {
464467
DangerousChangeType,
465468
findBreakingChanges,
466469
findDangerousChanges,
470+
resolveSchemaCoordinate,
471+
resolveASTSchemaCoordinate,
467472
} from './utilities/index.js';
468473

469474
export type {
@@ -493,4 +498,5 @@ export type {
493498
BreakingChange,
494499
DangerousChange,
495500
TypedQueryDocumentNode,
501+
ResolvedSchemaElement,
496502
} from './utilities/index.js';

src/language/__tests__/lexer-test.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,8 @@ describe('Lexer', () => {
852852
});
853853

854854
expectSyntaxError('.123').to.deep.equal({
855-
message: 'Syntax Error: Unexpected character: ".".',
855+
message:
856+
'Syntax Error: Invalid number, expected digit before ".", did you mean "0.123"?',
856857
locations: [{ line: 1, column: 1 }],
857858
});
858859

@@ -964,6 +965,13 @@ describe('Lexer', () => {
964965
value: undefined,
965966
});
966967

968+
expect(lexOne('.')).to.contain({
969+
kind: TokenKind.DOT,
970+
start: 0,
971+
end: 1,
972+
value: undefined,
973+
});
974+
967975
expect(lexOne('...')).to.contain({
968976
kind: TokenKind.SPREAD,
969977
start: 0,
@@ -1030,7 +1038,7 @@ describe('Lexer', () => {
10301038

10311039
it('lex reports useful unknown character error', () => {
10321040
expectSyntaxError('..').to.deep.equal({
1033-
message: 'Syntax Error: Unexpected character: ".".',
1041+
message: 'Syntax Error: Unexpected "..", did you mean "..."?',
10341042
locations: [{ line: 1, column: 1 }],
10351043
});
10361044

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

@@ -864,4 +870,129 @@ describe('Parser', () => {
864870
});
865871
});
866872
});
873+
874+
describe('parseSchemaCoordinate', () => {
875+
it('parses Name', () => {
876+
const result = parseSchemaCoordinate('MyType');
877+
expectJSON(result).toDeepEqual({
878+
kind: Kind.SCHEMA_COORDINATE,
879+
loc: { start: 0, end: 6 },
880+
ofDirective: false,
881+
name: {
882+
kind: Kind.NAME,
883+
loc: { start: 0, end: 6 },
884+
value: 'MyType',
885+
},
886+
memberName: undefined,
887+
argumentName: undefined,
888+
});
889+
});
890+
891+
it('parses Name . Name', () => {
892+
const result = parseSchemaCoordinate('MyType.field');
893+
expectJSON(result).toDeepEqual({
894+
kind: Kind.SCHEMA_COORDINATE,
895+
loc: { start: 0, end: 12 },
896+
ofDirective: false,
897+
name: {
898+
kind: Kind.NAME,
899+
loc: { start: 0, end: 6 },
900+
value: 'MyType',
901+
},
902+
memberName: {
903+
kind: Kind.NAME,
904+
loc: { start: 7, end: 12 },
905+
value: 'field',
906+
},
907+
argumentName: undefined,
908+
});
909+
});
910+
911+
it('rejects Name . Name . Name', () => {
912+
expectToThrowJSON(() =>
913+
parseSchemaCoordinate('MyType.field.deep'),
914+
).to.deep.equal({
915+
message: 'Syntax Error: Expected <EOF>, found ".".',
916+
locations: [{ line: 1, column: 13 }],
917+
});
918+
});
919+
920+
it('parses Name . Name ( Name : )', () => {
921+
const result = parseSchemaCoordinate('MyType.field(arg:)');
922+
expectJSON(result).toDeepEqual({
923+
kind: Kind.SCHEMA_COORDINATE,
924+
loc: { start: 0, end: 18 },
925+
ofDirective: false,
926+
name: {
927+
kind: Kind.NAME,
928+
loc: { start: 0, end: 6 },
929+
value: 'MyType',
930+
},
931+
memberName: {
932+
kind: Kind.NAME,
933+
loc: { start: 7, end: 12 },
934+
value: 'field',
935+
},
936+
argumentName: {
937+
kind: Kind.NAME,
938+
loc: { start: 13, end: 16 },
939+
value: 'arg',
940+
},
941+
});
942+
});
943+
944+
it('rejects Name . Name ( Name : Name )', () => {
945+
expectToThrowJSON(() =>
946+
parseSchemaCoordinate('MyType.field(arg: value)'),
947+
).to.deep.equal({
948+
message: 'Syntax Error: Expected ")", found Name "value".',
949+
locations: [{ line: 1, column: 19 }],
950+
});
951+
});
952+
953+
it('parses @ Name', () => {
954+
const result = parseSchemaCoordinate('@myDirective');
955+
expectJSON(result).toDeepEqual({
956+
kind: Kind.SCHEMA_COORDINATE,
957+
loc: { start: 0, end: 12 },
958+
ofDirective: true,
959+
name: {
960+
kind: Kind.NAME,
961+
loc: { start: 1, end: 12 },
962+
value: 'myDirective',
963+
},
964+
memberName: undefined,
965+
argumentName: undefined,
966+
});
967+
});
968+
969+
it('parses @ Name ( Name : )', () => {
970+
const result = parseSchemaCoordinate('@myDirective(arg:)');
971+
expectJSON(result).toDeepEqual({
972+
kind: Kind.SCHEMA_COORDINATE,
973+
loc: { start: 0, end: 18 },
974+
ofDirective: true,
975+
name: {
976+
kind: Kind.NAME,
977+
loc: { start: 1, end: 12 },
978+
value: 'myDirective',
979+
},
980+
memberName: undefined,
981+
argumentName: {
982+
kind: Kind.NAME,
983+
loc: { start: 13, end: 16 },
984+
value: 'arg',
985+
},
986+
});
987+
});
988+
989+
it('rejects @ Name . Name', () => {
990+
expectToThrowJSON(() =>
991+
parseSchemaCoordinate('@myDirective.field'),
992+
).to.deep.equal({
993+
message: 'Syntax Error: Expected <EOF>, found ".".',
994+
locations: [{ line: 1, column: 13 }],
995+
});
996+
});
997+
});
867998
});

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', () => {
@@ -290,4 +290,18 @@ describe('Printer: Query document', () => {
290290
`),
291291
);
292292
});
293+
294+
it('prints schema coordinates', () => {
295+
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
296+
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
297+
'Name.field',
298+
);
299+
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
300+
'Name.field(arg:)',
301+
);
302+
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
303+
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
304+
'@name(arg:)',
305+
);
306+
});
293307
});

src/language/ast.ts

+14
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export type ASTNode =
181181
| UnionTypeExtensionNode
182182
| EnumTypeExtensionNode
183183
| InputObjectTypeExtensionNode
184+
| SchemaCoordinateNode
184185
| NonNullAssertionNode
185186
| ErrorBoundaryNode
186187
| ListNullabilityOperatorNode;
@@ -295,6 +296,8 @@ export const QueryDocumentKeys: {
295296
UnionTypeExtension: ['name', 'directives', 'types'],
296297
EnumTypeExtension: ['name', 'directives', 'values'],
297298
InputObjectTypeExtension: ['name', 'directives', 'fields'],
299+
300+
SchemaCoordinate: ['name', 'memberName', 'argumentName'],
298301
};
299302

300303
const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
@@ -785,3 +788,14 @@ export interface InputObjectTypeExtensionNode {
785788
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
786789
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
787790
}
791+
792+
/** Schema Coordinates */
793+
794+
export interface SchemaCoordinateNode {
795+
readonly kind: Kind.SCHEMA_COORDINATE;
796+
readonly loc?: Location | undefined;
797+
readonly ofDirective: boolean;
798+
readonly name: NameNode;
799+
readonly memberName?: NameNode | undefined;
800+
readonly argumentName?: NameNode | undefined;
801+
}

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';
@@ -91,6 +97,7 @@ export type {
9197
UnionTypeExtensionNode,
9298
EnumTypeExtensionNode,
9399
InputObjectTypeExtensionNode,
100+
SchemaCoordinateNode,
94101
} from './ast.js';
95102

96103
export {
@@ -105,6 +112,7 @@ export {
105112
isTypeDefinitionNode,
106113
isTypeSystemExtensionNode,
107114
isTypeExtensionNode,
115+
isSchemaCoordinateNode,
108116
} from './predicates.js';
109117

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

src/language/kinds.ts

+3
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,7 @@ export enum Kind {
7171
UNION_TYPE_EXTENSION = 'UnionTypeExtension',
7272
ENUM_TYPE_EXTENSION = 'EnumTypeExtension',
7373
INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension',
74+
75+
/** Schema Coordinates */
76+
SCHEMA_COORDINATE = 'SchemaCoordinate',
7477
}

0 commit comments

Comments
 (0)