Skip to content

Commit ec9c063

Browse files
leebyronbenjiemagicmarkyaacovCR
authored
Schema Coordinates (#3044)
Depends on #3115 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 `resolveASTSchemeCoordinate()` which implement the semantics (name mirrored from `buildASTSchema`) as well as the return type `GraphQLSchemaElement` --------- Co-authored-by: Benjie Gillam <[email protected]> Co-authored-by: Mark Larah <[email protected]> Co-authored-by: Yaacov Rydzinski <[email protected]>
1 parent 9e4f796 commit ec9c063

19 files changed

+1231
-16
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ jobs:
5454
run: |
5555
git clone --depth 1 https://github.com/github/gitignore.git
5656
57-
rm gitignore/Global/ModelSim.gitignore
58-
rm gitignore/Global/Images.gitignore
57+
rm -f gitignore/Global/ModelSim.gitignore
58+
rm -f gitignore/Global/Images.gitignore
5959
cat gitignore/Node.gitignore gitignore/Global/*.gitignore > all.gitignore
6060
6161
IGNORED_FILES=$(git ls-files --cached --ignored --exclude-from=all.gitignore)

cspell.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ ignoreRegExpList:
4848

4949
words:
5050
- graphiql
51+
- metafield
5152
- uncoerce
5253
- uncoerced
5354

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export {
239239
parseValue,
240240
parseConstValue,
241241
parseType,
242+
parseSchemaCoordinate,
242243
// Print
243244
print,
244245
// Visit
@@ -258,6 +259,7 @@ export {
258259
isTypeDefinitionNode,
259260
isTypeSystemExtensionNode,
260261
isTypeExtensionNode,
262+
isSchemaCoordinateNode,
261263
} from './language/index.js';
262264

263265
export type {
@@ -330,6 +332,7 @@ export type {
330332
UnionTypeExtensionNode,
331333
EnumTypeExtensionNode,
332334
InputObjectTypeExtensionNode,
335+
SchemaCoordinateNode,
333336
} from './language/index.js';
334337

335338
// Execute GraphQL queries.
@@ -499,6 +502,8 @@ export {
499502
findBreakingChanges,
500503
findDangerousChanges,
501504
findSchemaChanges,
505+
resolveSchemaCoordinate,
506+
resolveASTSchemaCoordinate,
502507
} from './utilities/index.js';
503508

504509
export type {
@@ -529,4 +534,5 @@ export type {
529534
SafeChange,
530535
DangerousChange,
531536
TypedQueryDocumentNode,
537+
ResolvedSchemaElement,
532538
} from './utilities/index.js';

src/language/__tests__/parser-test.ts

Lines changed: 181 additions & 1 deletion
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,178 @@ 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.TYPE_COORDINATE,
694+
loc: { start: 0, end: 6 },
695+
name: {
696+
kind: Kind.NAME,
697+
loc: { start: 0, end: 6 },
698+
value: 'MyType',
699+
},
700+
});
701+
});
702+
703+
it('parses Name . Name', () => {
704+
const result = parseSchemaCoordinate('MyType.field');
705+
expectJSON(result).toDeepEqual({
706+
kind: Kind.MEMBER_COORDINATE,
707+
loc: { start: 0, end: 12 },
708+
name: {
709+
kind: Kind.NAME,
710+
loc: { start: 0, end: 6 },
711+
value: 'MyType',
712+
},
713+
memberName: {
714+
kind: Kind.NAME,
715+
loc: { start: 7, end: 12 },
716+
value: 'field',
717+
},
718+
});
719+
});
720+
721+
it('rejects Name . Name . Name', () => {
722+
expect(() => parseSchemaCoordinate('MyType.field.deep'))
723+
.to.throw()
724+
.to.deep.include({
725+
message: 'Syntax Error: Expected <EOF>, found ..',
726+
locations: [{ line: 1, column: 13 }],
727+
});
728+
});
729+
730+
it('parses Name . Name ( Name : )', () => {
731+
const result = parseSchemaCoordinate('MyType.field(arg:)');
732+
expectJSON(result).toDeepEqual({
733+
kind: Kind.ARGUMENT_COORDINATE,
734+
loc: { start: 0, end: 18 },
735+
name: {
736+
kind: Kind.NAME,
737+
loc: { start: 0, end: 6 },
738+
value: 'MyType',
739+
},
740+
fieldName: {
741+
kind: Kind.NAME,
742+
loc: { start: 7, end: 12 },
743+
value: 'field',
744+
},
745+
argumentName: {
746+
kind: Kind.NAME,
747+
loc: { start: 13, end: 16 },
748+
value: 'arg',
749+
},
750+
});
751+
});
752+
753+
it('rejects Name . Name ( Name : Name )', () => {
754+
expect(() => parseSchemaCoordinate('MyType.field(arg: value)'))
755+
.to.throw()
756+
.to.deep.include({
757+
message: 'Syntax Error: Invalid character: " ".',
758+
locations: [{ line: 1, column: 18 }],
759+
});
760+
});
761+
762+
it('parses @ Name', () => {
763+
const result = parseSchemaCoordinate('@myDirective');
764+
expectJSON(result).toDeepEqual({
765+
kind: Kind.DIRECTIVE_COORDINATE,
766+
loc: { start: 0, end: 12 },
767+
name: {
768+
kind: Kind.NAME,
769+
loc: { start: 1, end: 12 },
770+
value: 'myDirective',
771+
},
772+
});
773+
});
774+
775+
it('parses @ Name ( Name : )', () => {
776+
const result = parseSchemaCoordinate('@myDirective(arg:)');
777+
expectJSON(result).toDeepEqual({
778+
kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE,
779+
loc: { start: 0, end: 18 },
780+
name: {
781+
kind: Kind.NAME,
782+
loc: { start: 1, end: 12 },
783+
value: 'myDirective',
784+
},
785+
argumentName: {
786+
kind: Kind.NAME,
787+
loc: { start: 13, end: 16 },
788+
value: 'arg',
789+
},
790+
});
791+
});
792+
793+
it('parses __Type', () => {
794+
const result = parseSchemaCoordinate('__Type');
795+
expectJSON(result).toDeepEqual({
796+
kind: Kind.TYPE_COORDINATE,
797+
loc: { start: 0, end: 6 },
798+
name: {
799+
kind: Kind.NAME,
800+
loc: { start: 0, end: 6 },
801+
value: '__Type',
802+
},
803+
});
804+
});
805+
806+
it('parses Type.__metafield', () => {
807+
const result = parseSchemaCoordinate('Type.__metafield');
808+
expectJSON(result).toDeepEqual({
809+
kind: Kind.MEMBER_COORDINATE,
810+
loc: { start: 0, end: 16 },
811+
name: {
812+
kind: Kind.NAME,
813+
loc: { start: 0, end: 4 },
814+
value: 'Type',
815+
},
816+
memberName: {
817+
kind: Kind.NAME,
818+
loc: { start: 5, end: 16 },
819+
value: '__metafield',
820+
},
821+
});
822+
});
823+
824+
it('parses Type.__metafield(arg:)', () => {
825+
const result = parseSchemaCoordinate('Type.__metafield(arg:)');
826+
expectJSON(result).toDeepEqual({
827+
kind: Kind.ARGUMENT_COORDINATE,
828+
loc: { start: 0, end: 22 },
829+
name: {
830+
kind: Kind.NAME,
831+
loc: { start: 0, end: 4 },
832+
value: 'Type',
833+
},
834+
fieldName: {
835+
kind: Kind.NAME,
836+
loc: { start: 5, end: 16 },
837+
value: '__metafield',
838+
},
839+
argumentName: {
840+
kind: Kind.NAME,
841+
loc: { start: 17, end: 20 },
842+
value: 'arg',
843+
},
844+
});
845+
});
846+
847+
it('rejects @ Name . Name', () => {
848+
expect(() => parseSchemaCoordinate('@myDirective.field'))
849+
.to.throw()
850+
.to.deep.include({
851+
message: 'Syntax Error: Expected <EOF>, found ..',
852+
locations: [{ line: 1, column: 13 }],
853+
});
854+
});
855+
856+
it('accepts a Source object', () => {
857+
expect(parseSchemaCoordinate('MyType')).to.deep.equal(
858+
parseSchemaCoordinate(new Source('MyType')),
859+
);
860+
});
861+
});
682862
});

src/language/__tests__/predicates-test.ts

Lines changed: 11 additions & 0 deletions
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,14 @@ describe('AST node predicates', () => {
141142
'UnionTypeExtension',
142143
]);
143144
});
145+
146+
it('isSchemaCoordinateNode', () => {
147+
expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
148+
'ArgumentCoordinate',
149+
'DirectiveArgumentCoordinate',
150+
'DirectiveCoordinate',
151+
'MemberCoordinate',
152+
'TypeCoordinate',
153+
]);
154+
});
144155
});

src/language/__tests__/printer-test.ts

Lines changed: 30 additions & 1 deletion
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,33 @@ 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('Name.field');
306+
expect(print(parseSchemaCoordinate('Name.field(arg:)'))).to.equal(
307+
'Name.field(arg:)',
308+
);
309+
expect(print(parseSchemaCoordinate('@name'))).to.equal('@name');
310+
expect(print(parseSchemaCoordinate('@name(arg:)'))).to.equal('@name(arg:)');
311+
expect(print(parseSchemaCoordinate('__Type'))).to.equal('__Type');
312+
expect(print(parseSchemaCoordinate('Type.__metafield'))).to.equal(
313+
'Type.__metafield',
314+
);
315+
expect(print(parseSchemaCoordinate('Type.__metafield(arg:)'))).to.equal(
316+
'Type.__metafield(arg:)',
317+
);
318+
});
319+
320+
it('throws syntax error for ignored tokens in schema coordinates', () => {
321+
expect(() => print(parseSchemaCoordinate('# foo\nName'))).to.throw(
322+
'Syntax Error: Invalid character: "#"',
323+
);
324+
expect(() => print(parseSchemaCoordinate('\nName'))).to.throw(
325+
'Syntax Error: Invalid character: U+000A.',
326+
);
327+
expect(() => print(parseSchemaCoordinate('Name .field'))).to.throw(
328+
'Syntax Error: Invalid character: " "',
329+
);
330+
});
302331
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { expectToThrowJSON } from '../../__testUtils__/expectJSON.js';
5+
6+
import { SchemaCoordinateLexer } from '../schemaCoordinateLexer.js';
7+
import { Source } from '../source.js';
8+
import { TokenKind } from '../tokenKind.js';
9+
10+
function lexSecond(str: string) {
11+
const lexer = new SchemaCoordinateLexer(new Source(str));
12+
lexer.advance();
13+
return lexer.advance();
14+
}
15+
16+
function expectSyntaxError(text: string) {
17+
return expectToThrowJSON(() => lexSecond(text));
18+
}
19+
20+
describe('SchemaCoordinateLexer', () => {
21+
it('can be stringified', () => {
22+
const lexer = new SchemaCoordinateLexer(new Source('Name.field'));
23+
expect(Object.prototype.toString.call(lexer)).to.equal(
24+
'[object SchemaCoordinateLexer]',
25+
);
26+
});
27+
28+
it('tracks a schema coordinate', () => {
29+
const lexer = new SchemaCoordinateLexer(new Source('Name.field'));
30+
expect(lexer.advance()).to.contain({
31+
kind: TokenKind.NAME,
32+
start: 0,
33+
end: 4,
34+
value: 'Name',
35+
});
36+
});
37+
38+
it('forbids ignored tokens', () => {
39+
const lexer = new SchemaCoordinateLexer(new Source('\nName.field'));
40+
expectToThrowJSON(() => lexer.advance()).to.deep.equal({
41+
message: 'Syntax Error: Invalid character: U+000A.',
42+
locations: [{ line: 1, column: 1 }],
43+
});
44+
});
45+
46+
it('lex reports a useful syntax errors', () => {
47+
expectSyntaxError('Foo .bar').to.deep.equal({
48+
message: 'Syntax Error: Invalid character: " ".',
49+
locations: [{ line: 1, column: 4 }],
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)