Skip to content

Commit

Permalink
Schema Coordinates
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
leebyron authored and yaacovCR committed Nov 6, 2024
1 parent cca3f98 commit 9fc6c0b
Show file tree
Hide file tree
Showing 16 changed files with 693 additions and 30 deletions.
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export {
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
// Print
print,
// Visit
Expand All @@ -243,6 +244,7 @@ export {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from './language/index.js';

export type {
Expand Down Expand Up @@ -315,6 +317,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
SchemaCoordinateNode,
} from './language/index.js';

// Execute GraphQL queries.
Expand Down Expand Up @@ -482,6 +485,8 @@ export {
findBreakingChanges,
findDangerousChanges,
findSchemaChanges,
resolveSchemaCoordinate,
resolveASTSchemaCoordinate,
} from './utilities/index.js';

export type {
Expand Down Expand Up @@ -512,4 +517,5 @@ export type {
SafeChange,
DangerousChange,
TypedQueryDocumentNode,
ResolvedSchemaElement,
} from './utilities/index.js';
14 changes: 7 additions & 7 deletions src/language/__tests__/lexer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,6 @@ describe('Lexer', () => {
});
});

it('reports unexpected characters', () => {
expectSyntaxError('.').to.deep.equal({
message: 'Syntax Error: Unexpected character: ".".',
locations: [{ line: 1, column: 1 }],
});
});

it('errors respect whitespace', () => {
let caughtError;
try {
Expand Down Expand Up @@ -965,6 +958,13 @@ describe('Lexer', () => {
value: undefined,
});

expect(lexOne('.')).to.contain({
kind: TokenKind.DOT,
start: 0,
end: 1,
value: undefined,
});

expect(lexOne('...')).to.contain({
kind: TokenKind.SPREAD,
start: 0,
Expand Down
133 changes: 132 additions & 1 deletion src/language/__tests__/parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';
import { inspect } from '../../jsutils/inspect.js';

import { Kind } from '../kinds.js';
import { parse, parseConstValue, parseType, parseValue } from '../parser.js';
import {
parse,
parseConstValue,
parseSchemaCoordinate,
parseType,
parseValue,
} from '../parser.js';
import { Source } from '../source.js';
import { TokenKind } from '../tokenKind.js';

Expand Down Expand Up @@ -679,4 +685,129 @@ describe('Parser', () => {
});
});
});

describe('parseSchemaCoordinate', () => {
it('parses Name', () => {
const result = parseSchemaCoordinate('MyType');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 6 },
ofDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: undefined,
argumentName: undefined,
});
});

it('parses Name . Name', () => {
const result = parseSchemaCoordinate('MyType.field');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
ofDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
argumentName: undefined,
});
});

it('rejects Name . Name . Name', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('MyType.field.deep'),
).to.deep.equal({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});

it('parses Name . Name ( Name : )', () => {
const result = parseSchemaCoordinate('MyType.field(arg:)');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
ofDirective: false,
name: {
kind: Kind.NAME,
loc: { start: 0, end: 6 },
value: 'MyType',
},
memberName: {
kind: Kind.NAME,
loc: { start: 7, end: 12 },
value: 'field',
},
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});

it('rejects Name . Name ( Name : Name )', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('MyType.field(arg: value)'),
).to.deep.equal({
message: 'Syntax Error: Expected ")", found Name "value".',
locations: [{ line: 1, column: 19 }],
});
});

it('parses @ Name', () => {
const result = parseSchemaCoordinate('@myDirective');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 12 },
ofDirective: true,
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
memberName: undefined,
argumentName: undefined,
});
});

it('parses @ Name ( Name : )', () => {
const result = parseSchemaCoordinate('@myDirective(arg:)');
expectJSON(result).toDeepEqual({
kind: Kind.SCHEMA_COORDINATE,
loc: { start: 0, end: 18 },
ofDirective: true,
name: {
kind: Kind.NAME,
loc: { start: 1, end: 12 },
value: 'myDirective',
},
memberName: undefined,
argumentName: {
kind: Kind.NAME,
loc: { start: 13, end: 16 },
value: 'arg',
},
});
});

it('rejects @ Name . Name', () => {
expectToThrowJSON(() =>
parseSchemaCoordinate('@myDirective.field'),
).to.deep.equal({
message: 'Syntax Error: Expected <EOF>, found ".".',
locations: [{ line: 1, column: 13 }],
});
});
});
});
7 changes: 7 additions & 0 deletions src/language/__tests__/predicates-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isConstValueNode,
isDefinitionNode,
isExecutableDefinitionNode,
isSchemaCoordinateNode,
isSelectionNode,
isTypeDefinitionNode,
isTypeExtensionNode,
Expand Down Expand Up @@ -141,4 +142,10 @@ describe('AST node predicates', () => {
'InputObjectTypeExtension',
]);
});

it('isSchemaCoordinateNode', () => {
expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([
'SchemaCoordinate',
]);
});
});
16 changes: 15 additions & 1 deletion src/language/__tests__/printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { dedent, dedentString } from '../../__testUtils__/dedent.js';
import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js';

import { Kind } from '../kinds.js';
import { parse } from '../parser.js';
import { parse, parseSchemaCoordinate } from '../parser.js';
import { print } from '../printer.js';

describe('Printer: Query document', () => {
Expand Down Expand Up @@ -299,4 +299,18 @@ describe('Printer: Query document', () => {
`),
);
});

it('prints schema coordinates', () => {
expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name');
expect(print(parseSchemaCoordinate(' Name . field '))).to.equal(
'Name.field',
);
expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal(
'Name.field(arg:)',
);
expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name');
expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal(
'@name(arg:)',
);
});
});
16 changes: 15 additions & 1 deletion src/language/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ export type ASTNode =
| InterfaceTypeExtensionNode
| UnionTypeExtensionNode
| EnumTypeExtensionNode
| InputObjectTypeExtensionNode;
| InputObjectTypeExtensionNode
| SchemaCoordinateNode;

/**
* Utility type listing all nodes indexed by their kind.
Expand Down Expand Up @@ -287,6 +288,8 @@ export const QueryDocumentKeys: {
UnionTypeExtension: ['name', 'directives', 'types'],
EnumTypeExtension: ['name', 'directives', 'values'],
InputObjectTypeExtension: ['name', 'directives', 'fields'],

SchemaCoordinate: ['name', 'memberName', 'argumentName'],
};

const kindValues = new Set<string>(Object.keys(QueryDocumentKeys));
Expand Down Expand Up @@ -762,3 +765,14 @@ export interface InputObjectTypeExtensionNode {
readonly directives?: ReadonlyArray<ConstDirectiveNode> | undefined;
readonly fields?: ReadonlyArray<InputValueDefinitionNode> | undefined;
}

/** Schema Coordinates */

export interface SchemaCoordinateNode {
readonly kind: typeof Kind.SCHEMA_COORDINATE;
readonly loc?: Location | undefined;
readonly ofDirective: boolean;
readonly name: NameNode;
readonly memberName?: NameNode | undefined;
readonly argumentName?: NameNode | undefined;
}
10 changes: 9 additions & 1 deletion src/language/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ export { TokenKind } from './tokenKind.js';

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

export { parse, parseValue, parseConstValue, parseType } from './parser.js';
export {
parse,
parseValue,
parseConstValue,
parseType,
parseSchemaCoordinate,
} from './parser.js';
export type { ParseOptions } from './parser.js';

export { print } from './printer.js';
Expand Down Expand Up @@ -88,6 +94,7 @@ export type {
UnionTypeExtensionNode,
EnumTypeExtensionNode,
InputObjectTypeExtensionNode,
SchemaCoordinateNode,
} from './ast.js';

export {
Expand All @@ -101,6 +108,7 @@ export {
isTypeDefinitionNode,
isTypeSystemExtensionNode,
isTypeExtensionNode,
isSchemaCoordinateNode,
} from './predicates.js';

export { DirectiveLocation } from './directiveLocation.js';
3 changes: 3 additions & 0 deletions src/language/kinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export const Kind = {
UNION_TYPE_EXTENSION: 'UnionTypeExtension' as const,
ENUM_TYPE_EXTENSION: 'EnumTypeExtension' as const,
INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension' as const,

/** Schema Coordinates */
SCHEMA_COORDINATE: 'SchemaCoordinate' as const,
};
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Kind = (typeof Kind)[keyof typeof Kind];
Loading

0 comments on commit 9fc6c0b

Please sign in to comment.