From 06f37d7b44e5e4f2337138725bdb08c873f1e3f8 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Fri, 30 May 2025 22:56:45 -0500 Subject: [PATCH 01/17] Add intermediate nodes for schema coordinate parsing --- src/language/__tests__/parser-test.ts | 21 ++------ src/language/__tests__/predicates-test.ts | 6 ++- src/language/ast.ts | 58 +++++++++++++++++++---- src/language/kinds_.ts | 17 ++++++- src/language/parser.ts | 45 ++++++++++++++++-- src/language/predicates.ts | 8 +++- src/language/printer.ts | 28 +++++++---- src/utilities/resolveSchemaCoordinate.ts | 49 +++++++++++++------ 8 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index ca9753bcc2..b5737b575d 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -690,25 +690,21 @@ describe('Parser', () => { it('parses Name', () => { const result = parseSchemaCoordinate('MyType'); expectJSON(result).toDeepEqual({ - kind: Kind.SCHEMA_COORDINATE, + kind: Kind.TYPE_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, + kind: Kind.MEMBER_COORDINATE, loc: { start: 0, end: 12 }, - ofDirective: false, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, @@ -719,7 +715,6 @@ describe('Parser', () => { loc: { start: 7, end: 12 }, value: 'field', }, - argumentName: undefined, }); }); @@ -735,9 +730,8 @@ describe('Parser', () => { it('parses Name . Name ( Name : )', () => { const result = parseSchemaCoordinate('MyType.field(arg:)'); expectJSON(result).toDeepEqual({ - kind: Kind.SCHEMA_COORDINATE, + kind: Kind.ARGUMENT_COORDINATE, loc: { start: 0, end: 18 }, - ofDirective: false, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, @@ -768,31 +762,26 @@ describe('Parser', () => { it('parses @ Name', () => { const result = parseSchemaCoordinate('@myDirective'); expectJSON(result).toDeepEqual({ - kind: Kind.SCHEMA_COORDINATE, + kind: Kind.DIRECTIVE_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, + kind: Kind.DIRECTIVE_ARGUMENT_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 }, diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index f2df5ccf08..57907d6aa6 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -145,7 +145,11 @@ describe('AST node predicates', () => { it('isSchemaCoordinateNode', () => { expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([ - 'SchemaCoordinate', + 'ArgumentCoordinate', + 'DirectiveArgumentCoordinate', + 'DirectiveCoordinate', + 'MemberCoordinate', + 'TypeCoordinate', ]); }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index af1c2d6ca7..ef5a0691b7 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -182,7 +182,11 @@ export type ASTNode = | UnionTypeExtensionNode | EnumTypeExtensionNode | InputObjectTypeExtensionNode - | SchemaCoordinateNode; + | TypeCoordinateNode + | MemberCoordinateNode + | ArgumentCoordinateNode + | DirectiveCoordinateNode + | DirectiveArgumentCoordinateNode; /** * Utility type listing all nodes indexed by their kind. @@ -288,7 +292,13 @@ export const QueryDocumentKeys: { UnionTypeExtension: ['name', 'directives', 'types'], EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], - SchemaCoordinate: ['name', 'memberName', 'argumentName'], + + // Schema Coordinates + TypeCoordinate: ['name'], + MemberCoordinate: ['name', 'memberName'], + ArgumentCoordinate: ['name', 'memberName', 'argumentName'], + DirectiveCoordinate: ['name'], + DirectiveArgumentCoordinate: ['name', 'argumentName'], }; const kindValues = new Set(Object.keys(QueryDocumentKeys)); @@ -765,13 +775,45 @@ export interface InputObjectTypeExtensionNode { readonly fields?: ReadonlyArray | undefined; } -// Schema Coordinates +/** Schema Coordinates */ + +export type SchemaCoordinateNode = + | TypeCoordinateNode + | MemberCoordinateNode + | ArgumentCoordinateNode + | DirectiveCoordinateNode + | DirectiveArgumentCoordinateNode; + +export interface TypeCoordinateNode { + readonly kind: typeof Kind.TYPE_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; +} + +export interface MemberCoordinateNode { + readonly kind: typeof Kind.MEMBER_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; + readonly memberName: NameNode; +} + +export interface ArgumentCoordinateNode { + readonly kind: typeof Kind.ARGUMENT_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; + readonly memberName: NameNode; + readonly argumentName: NameNode; +} + +export interface DirectiveCoordinateNode { + readonly kind: typeof Kind.DIRECTIVE_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; +} -export interface SchemaCoordinateNode { - readonly kind: 'SchemaCoordinate'; +export interface DirectiveArgumentCoordinateNode { + readonly kind: typeof Kind.DIRECTIVE_ARGUMENT_COORDINATE; readonly loc?: Location; - readonly ofDirective: boolean; readonly name: NameNode; - readonly memberName?: NameNode | undefined; - readonly argumentName?: NameNode | undefined; + readonly argumentName: NameNode; } diff --git a/src/language/kinds_.ts b/src/language/kinds_.ts index 78ec798531..252feb6107 100644 --- a/src/language/kinds_.ts +++ b/src/language/kinds_.ts @@ -110,5 +110,18 @@ export const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension'; export type INPUT_OBJECT_TYPE_EXTENSION = typeof INPUT_OBJECT_TYPE_EXTENSION; /** Schema Coordinates */ -export const SCHEMA_COORDINATE = 'SchemaCoordinate'; -export type SCHEMA_COORDINATE = typeof SCHEMA_COORDINATE; +export const TYPE_COORDINATE = 'TypeCoordinate'; +export type TYPE_COORDINATE = typeof TYPE_COORDINATE; + +export const MEMBER_COORDINATE = 'MemberCoordinate'; +export type MEMBER_COORDINATE = typeof MEMBER_COORDINATE; + +export const ARGUMENT_COORDINATE = 'ArgumentCoordinate'; +export type ARGUMENT_COORDINATE = typeof ARGUMENT_COORDINATE; + +export const DIRECTIVE_COORDINATE = 'DirectiveCoordinate'; +export type DIRECTIVE_COORDINATE = typeof DIRECTIVE_COORDINATE; + +export const DIRECTIVE_ARGUMENT_COORDINATE = 'DirectiveArgumentCoordinate'; +export type DIRECTIVE_ARGUMENT_COORDINATE = + typeof DIRECTIVE_ARGUMENT_COORDINATE; diff --git a/src/language/parser.ts b/src/language/parser.ts index cb72914f07..24885f8d83 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -4,6 +4,7 @@ import type { GraphQLError } from '../error/GraphQLError.js'; import { syntaxError } from '../error/syntaxError.js'; import type { + ArgumentCoordinateNode, ArgumentNode, BooleanValueNode, ConstArgumentNode, @@ -13,6 +14,8 @@ import type { ConstObjectValueNode, ConstValueNode, DefinitionNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, DirectiveDefinitionNode, DirectiveNode, DocumentNode, @@ -35,6 +38,7 @@ import type { IntValueNode, ListTypeNode, ListValueNode, + MemberCoordinateNode, NamedTypeNode, NameNode, NonNullTypeNode, @@ -54,6 +58,7 @@ import type { SelectionSetNode, StringValueNode, Token, + TypeCoordinateNode, TypeNode, TypeSystemExtensionNode, UnionTypeDefinitionNode, @@ -1481,12 +1486,42 @@ export class Parser { this.expectToken(TokenKind.COLON); this.expectToken(TokenKind.PAREN_R); } - return this.node(start, { - kind: Kind.SCHEMA_COORDINATE, - ofDirective, + + if (ofDirective && !argumentName) { + return this.node(start, { + kind: Kind.DIRECTIVE_COORDINATE, + name, + }); + } + + if (ofDirective && argumentName) { + return this.node(start, { + kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, + name, + argumentName, + }); + } + + if (!ofDirective && memberName && argumentName) { + return this.node(start, { + kind: Kind.ARGUMENT_COORDINATE, + name, + memberName, + argumentName, + }); + } + + if (!ofDirective && memberName && !argumentName) { + return this.node(start, { + kind: Kind.MEMBER_COORDINATE, + name, + memberName, + }); + } + + return this.node(start, { + kind: Kind.TYPE_COORDINATE, name, - memberName, - argumentName, }); } diff --git a/src/language/predicates.ts b/src/language/predicates.ts index fa5923b90d..5146e8244e 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -115,5 +115,11 @@ export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode { export function isSchemaCoordinateNode( node: ASTNode, ): node is SchemaCoordinateNode { - return node.kind === Kind.SCHEMA_COORDINATE; + return ( + node.kind === Kind.TYPE_COORDINATE || + node.kind === Kind.MEMBER_COORDINATE || + node.kind === Kind.ARGUMENT_COORDINATE || + node.kind === Kind.DIRECTIVE_COORDINATE || + node.kind === Kind.DIRECTIVE_ARGUMENT_COORDINATE + ); } diff --git a/src/language/printer.ts b/src/language/printer.ts index dcc9f048b5..085253cbd9 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -321,16 +321,24 @@ const printDocASTReducer: ASTReducer = { join(['extend input', name, join(directives, ' '), block(fields)], ' '), }, - // Schema Coordinate - - SchemaCoordinate: { - leave: ({ ofDirective, name, memberName, argumentName }) => - join([ - ofDirective ? '@' : '', - name, - wrap('.', memberName), - wrap('(', argumentName, ':)'), - ]), + // Schema Coordinates + + TypeCoordinate: { leave: ({ name }) => name }, + + MemberCoordinate: { + leave: ({ name, memberName }) => join([name, wrap('.', memberName)]), + }, + + ArgumentCoordinate: { + leave: ({ name, memberName, argumentName }) => + join([name, wrap('.', memberName), wrap('(', argumentName, ':)')]), + }, + + DirectiveCoordinate: { leave: ({ name }) => join(['@', name]) }, + + DirectiveArgumentCoordinate: { + leave: ({ name, argumentName }) => + join(['@', name, wrap('(', argumentName, ':)')]), }, }; diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 026076672c..153256ec0c 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -81,15 +81,23 @@ export function resolveASTSchemaCoordinate( schema: GraphQLSchema, schemaCoordinate: SchemaCoordinateNode, ): ResolvedSchemaElement | undefined { - const { ofDirective, name, memberName, argumentName } = schemaCoordinate; - if (ofDirective) { + // const { ofDirective, name, memberName, argumentName } = schemaCoordinate; + + if ( + schemaCoordinate.kind === 'DirectiveCoordinate' || + schemaCoordinate.kind === 'DirectiveArgumentCoordinate' + ) { // SchemaCoordinate : // - @ Name // - @ Name ( Name : ) // Let {directiveName} be the value of the first {Name}. // Let {directive} be the directive in the {schema} named {directiveName}. - const directive = schema.getDirective(name.value); - if (!argumentName) { + const { + name: { value: directiveName }, + } = schemaCoordinate; + const directive = schema.getDirective(directiveName); + + if (schemaCoordinate.kind === 'DirectiveCoordinate') { // SchemaCoordinate : @ Name // Return the directive in the {schema} named {directiveName}. if (!directive) { @@ -99,14 +107,17 @@ export function resolveASTSchemaCoordinate( } // SchemaCoordinate : @ Name ( Name : ) - // Assert {directive} must exist. + // TODO: Assert {directive} must exist. if (!directive) { return; } // Let {directiveArgumentName} be the value of the second {Name}. // Return the argument of {directive} named {directiveArgumentName}. + const { + argumentName: { value: directiveArgumentName }, + } = schemaCoordinate; const directiveArgument = directive.args.find( - (arg) => arg.name === argumentName.value, + (arg) => arg.name === directiveArgumentName, ); if (!directiveArgument) { return; @@ -120,8 +131,11 @@ export function resolveASTSchemaCoordinate( // - Name . Name ( Name : ) // Let {typeName} be the value of the first {Name}. // Let {type} be the type in the {schema} named {typeName}. - const type = schema.getType(name.value); - if (!memberName) { + const { + name: { value: typeName }, + } = schemaCoordinate; + const type = schema.getType(typeName); + if (schemaCoordinate.kind === 'TypeCoordinate') { // SchemaCoordinate : Name // Return the type in the {schema} named {typeName}. if (!type) { @@ -130,13 +144,17 @@ export function resolveASTSchemaCoordinate( return { kind: 'NamedType', type }; } - if (!argumentName) { + const { + memberName: { value: memberName }, + } = schemaCoordinate; + + if (schemaCoordinate.kind === 'MemberCoordinate') { // SchemaCoordinate : Name . Name // If {type} is an Enum type: if (isEnumType(type)) { // Let {enumValueName} be the value of the second {Name}. // Return the enum value of {type} named {enumValueName}. - const enumValue = type.getValue(memberName.value); + const enumValue = type.getValue(memberName); if (enumValue == null) { return; } @@ -146,7 +164,7 @@ export function resolveASTSchemaCoordinate( if (isInputObjectType(type)) { // Let {inputFieldName} be the value of the second {Name}. // Return the input field of {type} named {inputFieldName}. - const inputField = type.getFields()[memberName.value]; + const inputField = type.getFields()[memberName]; if (inputField == null) { return; } @@ -159,7 +177,7 @@ export function resolveASTSchemaCoordinate( } // Let {fieldName} be the value of the second {Name}. // Return the field of {type} named {fieldName}. - const field = type.getFields()[memberName.value]; + const field = type.getFields()[memberName]; if (field == null) { return; } @@ -173,15 +191,18 @@ export function resolveASTSchemaCoordinate( } // Let {fieldName} be the value of the second {Name}. // Let {field} be the field of {type} named {fieldName}. - const field = type.getFields()[memberName.value]; + const field = type.getFields()[memberName]; // Assert {field} must exist. if (field == null) { return; } // Let {fieldArgumentName} be the value of the third {Name}. // Return the argument of {field} named {fieldArgumentName}. + const { + argumentName: { value: fieldArgumentName }, + } = schemaCoordinate; const fieldArgument = field.args.find( - (arg) => arg.name === argumentName.value, + (arg) => arg.name === fieldArgumentName, ); if (fieldArgument == null) { return; From b248ff576c3ac65eb6878f99814ca22089edcaef Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sat, 31 May 2025 22:03:41 -0500 Subject: [PATCH 02/17] src/**/__tests__/ --- src/language/parser.ts | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index 24885f8d83..90632e1d17 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1487,31 +1487,27 @@ export class Parser { this.expectToken(TokenKind.PAREN_R); } - if (ofDirective && !argumentName) { + if (ofDirective) { + if (argumentName) { + return this.node(start, { + kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, + name, + argumentName, + }); + } return this.node(start, { kind: Kind.DIRECTIVE_COORDINATE, name, }); - } - - if (ofDirective && argumentName) { - return this.node(start, { - kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, - name, - argumentName, - }); - } - - if (!ofDirective && memberName && argumentName) { - return this.node(start, { - kind: Kind.ARGUMENT_COORDINATE, - name, - memberName, - argumentName, - }); - } - - if (!ofDirective && memberName && !argumentName) { + } else if (memberName) { + if (argumentName) { + return this.node(start, { + kind: Kind.ARGUMENT_COORDINATE, + name, + memberName, + argumentName, + }); + } return this.node(start, { kind: Kind.MEMBER_COORDINATE, name, From ce1cd2cbf94260447e881e7e804ac8a33af8a14e Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sat, 31 May 2025 22:15:17 -0500 Subject: [PATCH 03/17] memberName -> fieldName --- src/language/__tests__/parser-test.ts | 2 +- src/language/ast.ts | 4 ++-- src/language/parser.ts | 2 +- src/language/printer.ts | 4 ++-- src/utilities/resolveSchemaCoordinate.ts | 14 +++++++++----- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index b5737b575d..c0d247ddf5 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -737,7 +737,7 @@ describe('Parser', () => { loc: { start: 0, end: 6 }, value: 'MyType', }, - memberName: { + fieldName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', diff --git a/src/language/ast.ts b/src/language/ast.ts index ef5a0691b7..812b988835 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -296,7 +296,7 @@ export const QueryDocumentKeys: { // Schema Coordinates TypeCoordinate: ['name'], MemberCoordinate: ['name', 'memberName'], - ArgumentCoordinate: ['name', 'memberName', 'argumentName'], + ArgumentCoordinate: ['name', 'fieldName', 'argumentName'], DirectiveCoordinate: ['name'], DirectiveArgumentCoordinate: ['name', 'argumentName'], }; @@ -801,7 +801,7 @@ export interface ArgumentCoordinateNode { readonly kind: typeof Kind.ARGUMENT_COORDINATE; readonly loc?: Location; readonly name: NameNode; - readonly memberName: NameNode; + readonly fieldName: NameNode; readonly argumentName: NameNode; } diff --git a/src/language/parser.ts b/src/language/parser.ts index 90632e1d17..de049abeb5 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1504,7 +1504,7 @@ export class Parser { return this.node(start, { kind: Kind.ARGUMENT_COORDINATE, name, - memberName, + fieldName: memberName, argumentName, }); } diff --git a/src/language/printer.ts b/src/language/printer.ts index 085253cbd9..823b14a02d 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -330,8 +330,8 @@ const printDocASTReducer: ASTReducer = { }, ArgumentCoordinate: { - leave: ({ name, memberName, argumentName }) => - join([name, wrap('.', memberName), wrap('(', argumentName, ':)')]), + leave: ({ name, fieldName, argumentName }) => + join([name, wrap('.', fieldName), wrap('(', argumentName, ':)')]), }, DirectiveCoordinate: { leave: ({ name }) => join(['@', name]) }, diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 153256ec0c..ee9b6a1822 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -144,11 +144,11 @@ export function resolveASTSchemaCoordinate( return { kind: 'NamedType', type }; } - const { - memberName: { value: memberName }, - } = schemaCoordinate; - if (schemaCoordinate.kind === 'MemberCoordinate') { + const { + memberName: { value: memberName }, + } = schemaCoordinate; + // SchemaCoordinate : Name . Name // If {type} is an Enum type: if (isEnumType(type)) { @@ -184,6 +184,10 @@ export function resolveASTSchemaCoordinate( return { kind: 'Field', type, field }; } + const { + fieldName: { value: fieldName }, + } = schemaCoordinate; + // SchemaCoordinate : Name . Name ( Name : ) // Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { @@ -191,7 +195,7 @@ export function resolveASTSchemaCoordinate( } // Let {fieldName} be the value of the second {Name}. // Let {field} be the field of {type} named {fieldName}. - const field = type.getFields()[memberName]; + const field = type.getFields()[fieldName]; // Assert {field} must exist. if (field == null) { return; From 6c3180813b9e410abde352b8231b1bcd58d2abcf Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sat, 31 May 2025 23:15:35 -0500 Subject: [PATCH 04/17] remove comment --- src/utilities/resolveSchemaCoordinate.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index ee9b6a1822..84b8af2f9d 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -81,8 +81,6 @@ export function resolveASTSchemaCoordinate( schema: GraphQLSchema, schemaCoordinate: SchemaCoordinateNode, ): ResolvedSchemaElement | undefined { - // const { ofDirective, name, memberName, argumentName } = schemaCoordinate; - if ( schemaCoordinate.kind === 'DirectiveCoordinate' || schemaCoordinate.kind === 'DirectiveArgumentCoordinate' From a0dfbbef0ca2c64ab65e6509f52efab2c1aaf663 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 00:21:53 -0500 Subject: [PATCH 05/17] - Split up the resolve schema coordinate logic - Add error handling as proposed by Benjie --- .../__tests__/resolveSchemaCoordinate-test.ts | 28 +- src/utilities/resolveSchemaCoordinate.ts | 383 ++++++++++++------ 2 files changed, 264 insertions(+), 147 deletions(-) diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index e316ef52a1..2607b416dc 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -66,12 +66,12 @@ describe('resolveSchemaCoordinate', () => { undefined, ); - expect(resolveSchemaCoordinate(schema, 'Unknown.field')).to.deep.equal( - undefined, + expect(() => resolveSchemaCoordinate(schema, 'Unknown.field')).to.throw( + 'Expected "Unknown" to be defined as a type in the schema.', ); - expect(resolveSchemaCoordinate(schema, 'String.field')).to.deep.equal( - undefined, + expect(() => resolveSchemaCoordinate(schema, 'String.field')).to.throw( + 'Expected "String" to be defined as a type in the schema.', ); }); @@ -130,17 +130,21 @@ describe('resolveSchemaCoordinate', () => { resolveSchemaCoordinate(schema, 'Business.name(unknown:)'), ).to.deep.equal(undefined); - expect( + expect(() => resolveSchemaCoordinate(schema, 'Unknown.field(arg:)'), - ).to.deep.equal(undefined); + ).to.throw('Expected "Unknown" to be defined as a type in the schema.'); - expect( + expect(() => resolveSchemaCoordinate(schema, 'Business.unknown(arg:)'), - ).to.deep.equal(undefined); + ).to.throw( + 'Expected "unknown" to exist as an argument of type "Business" in the schema.', + ); - expect( + expect(() => resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'), - ).to.deep.equal(undefined); + ).to.throw( + 'Expected "SearchCriteria" to be defined as a type in the schema.', + ); }); it('resolves a Directive', () => { @@ -178,8 +182,8 @@ describe('resolveSchemaCoordinate', () => { undefined, ); - expect(resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.deep.equal( - undefined, + expect(() => resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.throw( + 'Expected "unknown" to be defined as a directive in the schema.', ); }); }); diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 84b8af2f9d..4adc112e71 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -1,4 +1,14 @@ -import type { SchemaCoordinateNode } from '../language/ast.js'; +import { inspect } from '../jsutils/inspect.js'; + +import type { + ArgumentCoordinateNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, + MemberCoordinateNode, + SchemaCoordinateNode, + TypeCoordinateNode, +} from '../language/ast.js'; +import { Kind } from '../language/kinds.js'; import { parseSchemaCoordinate } from '../language/parser.js'; import type { Source } from '../language/source.js'; @@ -21,41 +31,55 @@ import type { GraphQLSchema } from '../type/schema.js'; /** * A resolved schema element may be one of the following kinds: */ +export interface ResolvedNamedType { + readonly kind: 'NamedType'; + readonly type: GraphQLNamedType; +} + +export interface ResolvedField { + readonly kind: 'Field'; + readonly type: GraphQLNamedType; + readonly field: GraphQLField; +} + +export interface ResolvedInputField { + readonly kind: 'InputField'; + readonly type: GraphQLNamedType; + readonly inputField: GraphQLInputField; +} + +export interface ResolvedEnumValue { + readonly kind: 'EnumValue'; + readonly type: GraphQLNamedType; + readonly enumValue: GraphQLEnumValue; +} + +export interface ResolvedFieldArgument { + readonly kind: 'FieldArgument'; + readonly type: GraphQLNamedType; + readonly field: GraphQLField; + readonly fieldArgument: GraphQLArgument; +} + +export interface ResolvedDirective { + readonly kind: 'Directive'; + readonly directive: GraphQLDirective; +} + +export interface ResolvedDirectiveArgument { + readonly kind: 'DirectiveArgument'; + readonly directive: GraphQLDirective; + readonly directiveArgument: GraphQLArgument; +} + export type ResolvedSchemaElement = - | { - readonly kind: 'NamedType'; - readonly type: GraphQLNamedType; - } - | { - readonly kind: 'Field'; - readonly type: GraphQLNamedType; - readonly field: GraphQLField; - } - | { - readonly kind: 'InputField'; - readonly type: GraphQLNamedType; - readonly inputField: GraphQLInputField; - } - | { - readonly kind: 'EnumValue'; - readonly type: GraphQLNamedType; - readonly enumValue: GraphQLEnumValue; - } - | { - readonly kind: 'FieldArgument'; - readonly type: GraphQLNamedType; - readonly field: GraphQLField; - readonly fieldArgument: GraphQLArgument; - } - | { - readonly kind: 'Directive'; - readonly directive: GraphQLDirective; - } - | { - readonly kind: 'DirectiveArgument'; - readonly directive: GraphQLDirective; - readonly directiveArgument: GraphQLArgument; - }; + | ResolvedNamedType + | ResolvedField + | ResolvedInputField + | ResolvedEnumValue + | ResolvedFieldArgument + | ResolvedDirective + | ResolvedDirectiveArgument; /** * A schema coordinate is resolved in the context of a GraphQL schema to @@ -75,139 +99,228 @@ export function resolveSchemaCoordinate( } /** - * Resolves schema coordinate from a parsed SchemaCoordinate node. + * SchemaCoordinate : @ Name */ -export function resolveASTSchemaCoordinate( +function resolveDirectiveCoordinate( schema: GraphQLSchema, - schemaCoordinate: SchemaCoordinateNode, -): ResolvedSchemaElement | undefined { - if ( - schemaCoordinate.kind === 'DirectiveCoordinate' || - schemaCoordinate.kind === 'DirectiveArgumentCoordinate' - ) { - // SchemaCoordinate : - // - @ Name - // - @ Name ( Name : ) - // Let {directiveName} be the value of the first {Name}. - // Let {directive} be the directive in the {schema} named {directiveName}. - const { - name: { value: directiveName }, - } = schemaCoordinate; - const directive = schema.getDirective(directiveName); - - if (schemaCoordinate.kind === 'DirectiveCoordinate') { - // SchemaCoordinate : @ Name - // Return the directive in the {schema} named {directiveName}. - if (!directive) { - return; - } - return { kind: 'Directive', directive }; - } + schemaCoordinate: DirectiveCoordinateNode, +): ResolvedDirective | undefined { + // Let {directiveName} be the value of the first {Name}. + const directiveName = schemaCoordinate.name.value; - // SchemaCoordinate : @ Name ( Name : ) - // TODO: Assert {directive} must exist. - if (!directive) { - return; - } - // Let {directiveArgumentName} be the value of the second {Name}. - // Return the argument of {directive} named {directiveArgumentName}. - const { - argumentName: { value: directiveArgumentName }, - } = schemaCoordinate; - const directiveArgument = directive.args.find( - (arg) => arg.name === directiveArgumentName, + // Let {directive} be the directive in the {schema} named {directiveName}. + const directive = schema.getDirective(directiveName); + + // If {directive} does not exist, return undefined. + if (!directive) { + return; + } + + // Otherwise return the directive in the {schema} named {directiveName}. + return { kind: 'Directive', directive }; +} + +/** + * SchemaCoordinate : @ Name ( Name : ) + */ +function resolveDirectiveArgumentCoordinate( + schema: GraphQLSchema, + schemaCoordinate: DirectiveArgumentCoordinateNode, +): ResolvedDirectiveArgument | undefined { + // Let {directiveName} be the value of the first {Name}. + const directiveName = schemaCoordinate.name.value; + + // Let {directive} be the directive in the {schema} named {directiveName}. + const directive = schema.getDirective(directiveName); + + // Assert that {directive} exists. + if (!directive) { + throw new Error( + `Expected ${inspect(directiveName)} to be defined as a directive in the schema.`, ); - if (!directiveArgument) { - return; - } - return { kind: 'DirectiveArgument', directive, directiveArgument }; } - // SchemaCoordinate : - // - Name - // - Name . Name - // - Name . Name ( Name : ) - // Let {typeName} be the value of the first {Name}. - // Let {type} be the type in the {schema} named {typeName}. + // Let {directiveArgumentName} be the value of the second {Name}. const { - name: { value: typeName }, + argumentName: { value: directiveArgumentName }, } = schemaCoordinate; + const directiveArgument = directive.args.find( + (arg) => arg.name === directiveArgumentName, + ); + + // If {directiveArgumentName} does not exist, return undefined. + if (!directiveArgument) { + return; + } + + // Return the argument of {directive} named {directiveArgumentName}. + return { kind: 'DirectiveArgument', directive, directiveArgument }; +} + +/** + * SchemaCoordinate : Name + */ +function resolveTypeCoordinate( + schema: GraphQLSchema, + schemaCoordinate: TypeCoordinateNode, +): ResolvedNamedType | undefined { + // Let {typeName} be the value of the first {Name}. + const typeName = schemaCoordinate.name.value; + + // Let {type} be the type in the {schema} named {typeName}. const type = schema.getType(typeName); - if (schemaCoordinate.kind === 'TypeCoordinate') { - // SchemaCoordinate : Name - // Return the type in the {schema} named {typeName}. - if (!type) { - return; - } - return { kind: 'NamedType', type }; + + // If {type} does not exist, return undefined. + if (!type) { + return; } - if (schemaCoordinate.kind === 'MemberCoordinate') { - const { - memberName: { value: memberName }, - } = schemaCoordinate; - - // SchemaCoordinate : Name . Name - // If {type} is an Enum type: - if (isEnumType(type)) { - // Let {enumValueName} be the value of the second {Name}. - // Return the enum value of {type} named {enumValueName}. - const enumValue = type.getValue(memberName); - if (enumValue == null) { - return; - } - return { kind: 'EnumValue', type, enumValue }; - } - // Otherwise if {type} is an Input Object type: - if (isInputObjectType(type)) { - // Let {inputFieldName} be the value of the second {Name}. - // Return the input field of {type} named {inputFieldName}. - const inputField = type.getFields()[memberName]; - if (inputField == null) { - return; - } - return { kind: 'InputField', type, inputField }; - } - // Otherwise: - // Assert {type} must be an Object or Interface type. - if (!isObjectType(type) && !isInterfaceType(type)) { + // Return the type in the {schema} named {typeName}. + return { kind: 'NamedType', type }; +} + +/** + * SchemaCoordinate : Name . Name + */ +function resolveMemberCoordinate( + schema: GraphQLSchema, + schemaCoordinate: MemberCoordinateNode, +): ResolvedField | ResolvedInputField | ResolvedEnumValue | undefined { + // Let {typeName} be the value of the first {Name}. + const typeName = schemaCoordinate.name.value; + + // Let {type} be the type in the {schema} named {typeName}. + const type = schema.getType(typeName); + + // Assert that {type} exists. + if (!type) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + + const memberName = schemaCoordinate.memberName.value; + + // If {type} is an Enum type: + if (isEnumType(type)) { + // Let {enumValueName} be the value of the second {Name}. + const enumValue = type.getValue(memberName); + + // TODO: Add a spec line about returning undefined if the member name does not exist. + if (enumValue == null) { return; } - // Let {fieldName} be the value of the second {Name}. - // Return the field of {type} named {fieldName}. - const field = type.getFields()[memberName]; - if (field == null) { + + // Return the enum value of {type} named {enumValueName}. + return { kind: 'EnumValue', type, enumValue }; + } + + // Otherwise if {type} is an Input Object type: + if (isInputObjectType(type)) { + // Let {inputFieldName} be the value of the second {Name}. + const inputField = type.getFields()[memberName]; + + // TODO: Add a spec line about returning undefined if the member name does not exist. + if (inputField == null) { return; } - return { kind: 'Field', type, field }; - } - const { - fieldName: { value: fieldName }, - } = schemaCoordinate; + // Return the input field of {type} named {inputFieldName}. + return { kind: 'InputField', type, inputField }; + } - // SchemaCoordinate : Name . Name ( Name : ) + // Otherwise: // Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + + // Let {fieldName} be the value of the second {Name}. + const field = type.getFields()[memberName]; + + // TODO: Add a spec line about returning undefined if the member name does not exist. + if (field == null) { return; } + + // Return the field of {type} named {fieldName}. + return { kind: 'Field', type, field }; +} + +/** + * SchemaCoordinate : Name . Name ( Name : ) + */ +function resolveArgumentCoordinate( + schema: GraphQLSchema, + schemaCoordinate: ArgumentCoordinateNode, +): ResolvedFieldArgument | undefined { + // Let {typeName} be the value of the first {Name}. + const typeName = schemaCoordinate.name.value; + + // Let {type} be the type in the {schema} named {typeName}. + const type = schema.getType(typeName); + + // Assert that {type} exists. + if (!type) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + + const fieldName = schemaCoordinate.fieldName.value; + + // Assert {type} must be an Object or Interface type. + if (!isObjectType(type) && !isInterfaceType(type)) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + // Let {fieldName} be the value of the second {Name}. // Let {field} be the field of {type} named {fieldName}. const field = type.getFields()[fieldName]; + // Assert {field} must exist. if (field == null) { - return; + throw new Error( + `Expected ${inspect(fieldName)} to exist as an argument of type ${inspect(typeName)} in the schema.`, + ); } + // Let {fieldArgumentName} be the value of the third {Name}. - // Return the argument of {field} named {fieldArgumentName}. - const { - argumentName: { value: fieldArgumentName }, - } = schemaCoordinate; + const fieldArgumentName = schemaCoordinate.argumentName.value; const fieldArgument = field.args.find( (arg) => arg.name === fieldArgumentName, ); + + // TODO: Add a spec line about returning undefined if the argument does not exist. if (fieldArgument == null) { return; } + + // Return the argument of {field} named {fieldArgumentName}. return { kind: 'FieldArgument', type, field, fieldArgument }; } + +/** + * Resolves schema coordinate from a parsed SchemaCoordinate node. + */ +export function resolveASTSchemaCoordinate( + schema: GraphQLSchema, + schemaCoordinate: SchemaCoordinateNode, +): ResolvedSchemaElement | undefined { + switch (schemaCoordinate.kind) { + case Kind.DIRECTIVE_COORDINATE: + return resolveDirectiveCoordinate(schema, schemaCoordinate); + case Kind.DIRECTIVE_ARGUMENT_COORDINATE: + return resolveDirectiveArgumentCoordinate(schema, schemaCoordinate); + case Kind.TYPE_COORDINATE: + return resolveTypeCoordinate(schema, schemaCoordinate); + case Kind.MEMBER_COORDINATE: + return resolveMemberCoordinate(schema, schemaCoordinate); + case Kind.ARGUMENT_COORDINATE: + return resolveArgumentCoordinate(schema, schemaCoordinate); + } +} From d74015222132f70c79c7439ba791422f8b126f5a Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:37:43 -0500 Subject: [PATCH 06/17] Update src/utilities/__tests__/resolveSchemaCoordinate-test.ts Co-authored-by: Benjie --- src/utilities/__tests__/resolveSchemaCoordinate-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 2607b416dc..2f26d02445 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -71,7 +71,7 @@ describe('resolveSchemaCoordinate', () => { ); expect(() => resolveSchemaCoordinate(schema, 'String.field')).to.throw( - 'Expected "String" to be defined as a type in the schema.', + 'Expected "String" to be an object type, interface type, input object type, or enum type.', ); }); From 03b6541137877f1ced5c5f570ca9a3c76152769d Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:37:48 -0500 Subject: [PATCH 07/17] Update src/utilities/__tests__/resolveSchemaCoordinate-test.ts Co-authored-by: Benjie --- src/utilities/__tests__/resolveSchemaCoordinate-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 2f26d02445..22d22983b2 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -137,7 +137,7 @@ describe('resolveSchemaCoordinate', () => { expect(() => resolveSchemaCoordinate(schema, 'Business.unknown(arg:)'), ).to.throw( - 'Expected "unknown" to exist as an argument of type "Business" in the schema.', + 'Expected "unknown" to exist as a field of type "Business" in the schema.', ); expect(() => From e3fed77074d67d7e1c51df3170ce85940e1a709f Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:37:54 -0500 Subject: [PATCH 08/17] Update src/utilities/resolveSchemaCoordinate.ts Co-authored-by: Benjie --- src/utilities/resolveSchemaCoordinate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 4adc112e71..525a91f074 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -233,7 +233,7 @@ function resolveMemberCoordinate( // Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { throw new Error( - `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + `Expected ${inspect(typeName)} to be an object type, interface type, input object type, or enum type.`, ); } From 880ec415380bd010de18ccf41b39e2fbdbe8d9f9 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:37:59 -0500 Subject: [PATCH 09/17] Update src/utilities/resolveSchemaCoordinate.ts Co-authored-by: Benjie --- src/utilities/resolveSchemaCoordinate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 525a91f074..d7590c0b71 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -274,7 +274,7 @@ function resolveArgumentCoordinate( // Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { throw new Error( - `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + `Expected ${inspect(typeName)} to be an object type or interface type.`, ); } From ecbd566be9e3762b54b6d6056cf6020b0d5f2609 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:38:13 -0500 Subject: [PATCH 10/17] Update src/utilities/resolveSchemaCoordinate.ts Co-authored-by: Benjie --- src/utilities/resolveSchemaCoordinate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index d7590c0b71..f5a00c7ff9 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -285,7 +285,7 @@ function resolveArgumentCoordinate( // Assert {field} must exist. if (field == null) { throw new Error( - `Expected ${inspect(fieldName)} to exist as an argument of type ${inspect(typeName)} in the schema.`, + `Expected ${inspect(fieldName)} to exist as a field of type ${inspect(typeName)} in the schema.`, ); } From 427999eed37001c5bc6627473412be9f586d969b Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 21:41:01 -0500 Subject: [PATCH 11/17] fix test --- src/utilities/__tests__/resolveSchemaCoordinate-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 22d22983b2..a4eabc4ed9 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -143,7 +143,7 @@ describe('resolveSchemaCoordinate', () => { expect(() => resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'), ).to.throw( - 'Expected "SearchCriteria" to be defined as a type in the schema.', + 'Expected "SearchCriteria" to be an object type or interface type.', ); }); From 829d5ea8225af2739a7edf633f7d401c182f811a Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 22:47:18 -0500 Subject: [PATCH 12/17] add spec comments --- src/utilities/resolveSchemaCoordinate.ts | 200 +++++++++++------------ 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index f5a00c7ff9..96436d0b6a 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -98,65 +98,6 @@ export function resolveSchemaCoordinate( ); } -/** - * SchemaCoordinate : @ Name - */ -function resolveDirectiveCoordinate( - schema: GraphQLSchema, - schemaCoordinate: DirectiveCoordinateNode, -): ResolvedDirective | undefined { - // Let {directiveName} be the value of the first {Name}. - const directiveName = schemaCoordinate.name.value; - - // Let {directive} be the directive in the {schema} named {directiveName}. - const directive = schema.getDirective(directiveName); - - // If {directive} does not exist, return undefined. - if (!directive) { - return; - } - - // Otherwise return the directive in the {schema} named {directiveName}. - return { kind: 'Directive', directive }; -} - -/** - * SchemaCoordinate : @ Name ( Name : ) - */ -function resolveDirectiveArgumentCoordinate( - schema: GraphQLSchema, - schemaCoordinate: DirectiveArgumentCoordinateNode, -): ResolvedDirectiveArgument | undefined { - // Let {directiveName} be the value of the first {Name}. - const directiveName = schemaCoordinate.name.value; - - // Let {directive} be the directive in the {schema} named {directiveName}. - const directive = schema.getDirective(directiveName); - - // Assert that {directive} exists. - if (!directive) { - throw new Error( - `Expected ${inspect(directiveName)} to be defined as a directive in the schema.`, - ); - } - - // Let {directiveArgumentName} be the value of the second {Name}. - const { - argumentName: { value: directiveArgumentName }, - } = schemaCoordinate; - const directiveArgument = directive.args.find( - (arg) => arg.name === directiveArgumentName, - ); - - // If {directiveArgumentName} does not exist, return undefined. - if (!directiveArgument) { - return; - } - - // Return the argument of {directive} named {directiveArgumentName}. - return { kind: 'DirectiveArgument', directive, directiveArgument }; -} - /** * SchemaCoordinate : Name */ @@ -164,18 +105,17 @@ function resolveTypeCoordinate( schema: GraphQLSchema, schemaCoordinate: TypeCoordinateNode, ): ResolvedNamedType | undefined { - // Let {typeName} be the value of the first {Name}. + // 1. Let {typeName} be the value of the first {Name}. + // 2. Let {type} be the type in the {schema} named {typeName}. const typeName = schemaCoordinate.name.value; - - // Let {type} be the type in the {schema} named {typeName}. const type = schema.getType(typeName); - // If {type} does not exist, return undefined. + // 3. If {type} does not exist, return {void}. if (!type) { return; } - // Return the type in the {schema} named {typeName}. + // 4. {type} return { kind: 'NamedType', type }; } @@ -186,66 +126,69 @@ function resolveMemberCoordinate( schema: GraphQLSchema, schemaCoordinate: MemberCoordinateNode, ): ResolvedField | ResolvedInputField | ResolvedEnumValue | undefined { - // Let {typeName} be the value of the first {Name}. + // 1. Let {typeName} be the value of the first {Name}. + // 2. Let {type} be the type in the {schema} named {typeName}. const typeName = schemaCoordinate.name.value; - - // Let {type} be the type in the {schema} named {typeName}. const type = schema.getType(typeName); - // Assert that {type} exists. + // 3. Assert that {type} exists. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } - const memberName = schemaCoordinate.memberName.value; - - // If {type} is an Enum type: + // 4. If {type} is an Enum type: if (isEnumType(type)) { - // Let {enumValueName} be the value of the second {Name}. - const enumValue = type.getValue(memberName); + // 5. Let {enumValueName} be the value of the second {Name}. + // 6. Let {enumValue} be the enum value of {type} named {enumValueName}. + const enumValueName = schemaCoordinate.memberName.value; + const enumValue = type.getValue(enumValueName); - // TODO: Add a spec line about returning undefined if the member name does not exist. + // 7. If {enumValue} does not exist, return {void}. if (enumValue == null) { return; } - // Return the enum value of {type} named {enumValueName}. + // 8. Return {enumValue} return { kind: 'EnumValue', type, enumValue }; } - // Otherwise if {type} is an Input Object type: + // 9. Otherwise if {type} is an Input Object type: if (isInputObjectType(type)) { - // Let {inputFieldName} be the value of the second {Name}. - const inputField = type.getFields()[memberName]; + // 10. Let {inputFieldName} be the value of the second {Name}. + // 11. Let {inputField} be the input field of {type} named {inputFieldName}. + const inputFieldName = schemaCoordinate.memberName.value; + const inputField = type.getFields()[inputFieldName]; - // TODO: Add a spec line about returning undefined if the member name does not exist. + // 12. If {inputField} does not exist, return {void}. if (inputField == null) { return; } - // Return the input field of {type} named {inputFieldName}. + // 13. Return {inputField} return { kind: 'InputField', type, inputField }; } - // Otherwise: - // Assert {type} must be an Object or Interface type. + // 14. Otherwise: + // 15. Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { throw new Error( `Expected ${inspect(typeName)} to be an object type, interface type, input object type, or enum type.`, ); } - // Let {fieldName} be the value of the second {Name}. - const field = type.getFields()[memberName]; + // 16. Let {fieldName} be the value of the second {Name}. + // 17. Let {field} be the field of {type} named {fieldName}. + const fieldName = schemaCoordinate.memberName.value; + const field = type.getFields()[fieldName]; - // TODO: Add a spec line about returning undefined if the member name does not exist. + // 18. If {field} does not exist, return {void}. if (field == null) { return; } - // Return the field of {type} named {fieldName}. + // 19. Return {field} return { kind: 'Field', type, field }; } @@ -256,54 +199,111 @@ function resolveArgumentCoordinate( schema: GraphQLSchema, schemaCoordinate: ArgumentCoordinateNode, ): ResolvedFieldArgument | undefined { - // Let {typeName} be the value of the first {Name}. + // 1. Let {typeName} be the value of the first {Name}. + // 2. Let {type} be the type in the {schema} named {typeName}. const typeName = schemaCoordinate.name.value; - - // Let {type} be the type in the {schema} named {typeName}. const type = schema.getType(typeName); - // Assert that {type} exists. - if (!type) { + // 3. Assert that {type} exists. + if (type == null) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } - const fieldName = schemaCoordinate.fieldName.value; - - // Assert {type} must be an Object or Interface type. + // 4. Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { throw new Error( `Expected ${inspect(typeName)} to be an object type or interface type.`, ); } - // Let {fieldName} be the value of the second {Name}. - // Let {field} be the field of {type} named {fieldName}. + // 5. Let {fieldName} be the value of the second {Name}. + // 6. Let {field} be the field of {type} named {fieldName}. + const fieldName = schemaCoordinate.fieldName.value; const field = type.getFields()[fieldName]; - // Assert {field} must exist. + // 7. Assert {field} must exist. if (field == null) { throw new Error( `Expected ${inspect(fieldName)} to exist as a field of type ${inspect(typeName)} in the schema.`, ); } - // Let {fieldArgumentName} be the value of the third {Name}. + // 8. Let {fieldArgumentName} be the value of the third {Name}. + // 9. Let {fieldArgument} be the argument of {field} named {fieldArgumentName}. const fieldArgumentName = schemaCoordinate.argumentName.value; const fieldArgument = field.args.find( (arg) => arg.name === fieldArgumentName, ); - // TODO: Add a spec line about returning undefined if the argument does not exist. + // 10. If {fieldArgument} does not exist, return {void}. if (fieldArgument == null) { return; } - // Return the argument of {field} named {fieldArgumentName}. + // 11. Return {fieldArgument}. return { kind: 'FieldArgument', type, field, fieldArgument }; } +/** + * SchemaCoordinate : @ Name + */ +function resolveDirectiveCoordinate( + schema: GraphQLSchema, + schemaCoordinate: DirectiveCoordinateNode, +): ResolvedDirective | undefined { + // 1. Let {directiveName} be the value of the first {Name}. + // 2. Let {directive} be the directive in the {schema} named {directiveName}. + const directiveName = schemaCoordinate.name.value; + const directive = schema.getDirective(directiveName); + + // 3. If {directive} does not exist, return {void}. + if (!directive) { + return; + } + + // 4. Otherwise return {directive}. + return { kind: 'Directive', directive }; +} + +/** + * SchemaCoordinate : @ Name ( Name : ) + */ +function resolveDirectiveArgumentCoordinate( + schema: GraphQLSchema, + schemaCoordinate: DirectiveArgumentCoordinateNode, +): ResolvedDirectiveArgument | undefined { + // 1. Let {directiveName} be the value of the first {Name}. + // 2. Let {directive} be the directive in the {schema} named {directiveName}. + const directiveName = schemaCoordinate.name.value; + const directive = schema.getDirective(directiveName); + + // 3. Assert {directive} must exist. + if (!directive) { + throw new Error( + `Expected ${inspect(directiveName)} to be defined as a directive in the schema.`, + ); + } + + // 4. Let {directiveArgumentName} be the value of the second {Name}. + // 5. Let {directiveArgument} be the argument of {directive} named {directiveArgumentName}. + const { + argumentName: { value: directiveArgumentName }, + } = schemaCoordinate; + const directiveArgument = directive.args.find( + (arg) => arg.name === directiveArgumentName, + ); + + // 6. If {directiveArgument} does not exist, return {void}. + if (!directiveArgument) { + return; + } + + // 7. Return {directiveArgument}. + return { kind: 'DirectiveArgument', directive, directiveArgument }; +} + /** * Resolves schema coordinate from a parsed SchemaCoordinate node. */ From e195642718bd38d39d7661f3a7a98ff0f8ac52e5 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 23:02:52 -0500 Subject: [PATCH 13/17] assert that ... -> assert ... --- src/utilities/resolveSchemaCoordinate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 96436d0b6a..bbd07899d7 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -131,7 +131,7 @@ function resolveMemberCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert that {type} exists. + // 3. Assert {type} exists. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, @@ -204,7 +204,7 @@ function resolveArgumentCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert that {type} exists. + // 3. Assert {type} exists. if (type == null) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, From f902ba0d7742804bf32778cbba12f757d457e32a Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 23:05:18 -0500 Subject: [PATCH 14/17] consistent exists/must exist --- src/utilities/resolveSchemaCoordinate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index bbd07899d7..b3842ae62f 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -131,7 +131,7 @@ function resolveMemberCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert {type} exists. + // 3. Assert {type} must exist. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, @@ -204,7 +204,7 @@ function resolveArgumentCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert {type} exists. + // 3. Assert {type} must exist. if (type == null) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, From a49e236e7ce1296cce06b464f86593075d055521 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Sun, 1 Jun 2025 23:15:09 -0500 Subject: [PATCH 15/17] void -> null --- src/utilities/resolveSchemaCoordinate.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index b3842ae62f..b97d91cb91 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -110,7 +110,7 @@ function resolveTypeCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. If {type} does not exist, return {void}. + // 3. If {type} does not exist, return {null}. if (!type) { return; } @@ -145,7 +145,7 @@ function resolveMemberCoordinate( const enumValueName = schemaCoordinate.memberName.value; const enumValue = type.getValue(enumValueName); - // 7. If {enumValue} does not exist, return {void}. + // 7. If {enumValue} does not exist, return {null}. if (enumValue == null) { return; } @@ -161,7 +161,7 @@ function resolveMemberCoordinate( const inputFieldName = schemaCoordinate.memberName.value; const inputField = type.getFields()[inputFieldName]; - // 12. If {inputField} does not exist, return {void}. + // 12. If {inputField} does not exist, return {null}. if (inputField == null) { return; } @@ -183,7 +183,7 @@ function resolveMemberCoordinate( const fieldName = schemaCoordinate.memberName.value; const field = type.getFields()[fieldName]; - // 18. If {field} does not exist, return {void}. + // 18. If {field} does not exist, return {null}. if (field == null) { return; } @@ -237,7 +237,7 @@ function resolveArgumentCoordinate( (arg) => arg.name === fieldArgumentName, ); - // 10. If {fieldArgument} does not exist, return {void}. + // 10. If {fieldArgument} does not exist, return {null}. if (fieldArgument == null) { return; } @@ -258,7 +258,7 @@ function resolveDirectiveCoordinate( const directiveName = schemaCoordinate.name.value; const directive = schema.getDirective(directiveName); - // 3. If {directive} does not exist, return {void}. + // 3. If {directive} does not exist, return {null}. if (!directive) { return; } @@ -295,7 +295,7 @@ function resolveDirectiveArgumentCoordinate( (arg) => arg.name === directiveArgumentName, ); - // 6. If {directiveArgument} does not exist, return {void}. + // 6. If {directiveArgument} does not exist, return {null}. if (!directiveArgument) { return; } From cc9df087e3ac2ecbbf05add115da2867973b32e0 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Mon, 2 Jun 2025 15:45:53 -0500 Subject: [PATCH 16/17] update to use spec wording --- src/utilities/resolveSchemaCoordinate.ts | 85 ++++++++++-------------- 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index b97d91cb91..7e66e4ecc6 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -99,28 +99,26 @@ export function resolveSchemaCoordinate( } /** - * SchemaCoordinate : Name + * TypeCoordinate : Name */ function resolveTypeCoordinate( schema: GraphQLSchema, schemaCoordinate: TypeCoordinateNode, ): ResolvedNamedType | undefined { - // 1. Let {typeName} be the value of the first {Name}. - // 2. Let {type} be the type in the {schema} named {typeName}. + // 1. Let {typeName} be the value of {Name}. const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. If {type} does not exist, return {null}. - if (!type) { + // 2. Return the type in the {schema} named {typeName}, or {null} if no such type exists. + if (type == null) { return; } - // 4. {type} return { kind: 'NamedType', type }; } /** - * SchemaCoordinate : Name . Name + * MemberCoordinate : Name . Name */ function resolveMemberCoordinate( schema: GraphQLSchema, @@ -131,69 +129,66 @@ function resolveMemberCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert {type} must exist. + // 3. Assert: {type} must exist, and must be an Enum, Input Object, Object or Interface type. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } + if ( + !isEnumType(type) && + !isInputObjectType(type) && + !isObjectType(type) && + !isInterfaceType(type) + ) { + throw new Error( + `Expected ${inspect(typeName)} to be an object type, interface type, input object type, or enum type.`, + ); + } // 4. If {type} is an Enum type: if (isEnumType(type)) { - // 5. Let {enumValueName} be the value of the second {Name}. - // 6. Let {enumValue} be the enum value of {type} named {enumValueName}. + // 1. Let {enumValueName} be the value of the second {Name}. const enumValueName = schemaCoordinate.memberName.value; const enumValue = type.getValue(enumValueName); - // 7. If {enumValue} does not exist, return {null}. + // 2. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. if (enumValue == null) { return; } - // 8. Return {enumValue} return { kind: 'EnumValue', type, enumValue }; } - // 9. Otherwise if {type} is an Input Object type: + // 5. Otherwise, if {type} is an Input Object type: if (isInputObjectType(type)) { - // 10. Let {inputFieldName} be the value of the second {Name}. - // 11. Let {inputField} be the input field of {type} named {inputFieldName}. + // 1. Let {inputFieldName} be the value of the second {Name}. const inputFieldName = schemaCoordinate.memberName.value; const inputField = type.getFields()[inputFieldName]; - // 12. If {inputField} does not exist, return {null}. + // 2. Return the input field of {type} named {inputFieldName}, or {null} if no such input field exists. if (inputField == null) { return; } - // 13. Return {inputField} return { kind: 'InputField', type, inputField }; } - // 14. Otherwise: - // 15. Assert {type} must be an Object or Interface type. - if (!isObjectType(type) && !isInterfaceType(type)) { - throw new Error( - `Expected ${inspect(typeName)} to be an object type, interface type, input object type, or enum type.`, - ); - } - - // 16. Let {fieldName} be the value of the second {Name}. - // 17. Let {field} be the field of {type} named {fieldName}. + // 6. Otherwise: + // 1. Let {fieldName} be the value of the second {Name}. const fieldName = schemaCoordinate.memberName.value; const field = type.getFields()[fieldName]; - // 18. If {field} does not exist, return {null}. + // 2. Return the field of {type} named {fieldName}, or {null} if no such field exists. if (field == null) { return; } - // 19. Return {field} return { kind: 'Field', type, field }; } /** - * SchemaCoordinate : Name . Name ( Name : ) + * ArgumentCoordinate : Name . Name ( Name : ) */ function resolveArgumentCoordinate( schema: GraphQLSchema, @@ -204,71 +199,65 @@ function resolveArgumentCoordinate( const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert {type} must exist. + // 3. Assert: {type} must exist, and be an Object or Interface type. if (type == null) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } - - // 4. Assert {type} must be an Object or Interface type. if (!isObjectType(type) && !isInterfaceType(type)) { throw new Error( `Expected ${inspect(typeName)} to be an object type or interface type.`, ); } - // 5. Let {fieldName} be the value of the second {Name}. - // 6. Let {field} be the field of {type} named {fieldName}. + // 4. Let {fieldName} be the value of the second {Name}. + // 5. Let {field} be the field of {type} named {fieldName}. const fieldName = schemaCoordinate.fieldName.value; const field = type.getFields()[fieldName]; - // 7. Assert {field} must exist. + // 7. Assert: {field} must exist. if (field == null) { throw new Error( `Expected ${inspect(fieldName)} to exist as a field of type ${inspect(typeName)} in the schema.`, ); } - // 8. Let {fieldArgumentName} be the value of the third {Name}. - // 9. Let {fieldArgument} be the argument of {field} named {fieldArgumentName}. + // 7. Let {fieldArgumentName} be the value of the third {Name}. const fieldArgumentName = schemaCoordinate.argumentName.value; const fieldArgument = field.args.find( (arg) => arg.name === fieldArgumentName, ); - // 10. If {fieldArgument} does not exist, return {null}. + // 8. Return the argument of {field} named {fieldArgumentName}, or {null} if no such argument exists. if (fieldArgument == null) { return; } - // 11. Return {fieldArgument}. return { kind: 'FieldArgument', type, field, fieldArgument }; } /** - * SchemaCoordinate : @ Name + * DirectiveCoordinate : @ Name */ function resolveDirectiveCoordinate( schema: GraphQLSchema, schemaCoordinate: DirectiveCoordinateNode, ): ResolvedDirective | undefined { - // 1. Let {directiveName} be the value of the first {Name}. - // 2. Let {directive} be the directive in the {schema} named {directiveName}. + // 1. Let {directiveName} be the value of {Name}. const directiveName = schemaCoordinate.name.value; const directive = schema.getDirective(directiveName); - // 3. If {directive} does not exist, return {null}. + // 2. Return the directive in the {schema} named {directiveName}, or {null} if no such directive exists. if (!directive) { return; } - // 4. Otherwise return {directive}. return { kind: 'Directive', directive }; } /** - * SchemaCoordinate : @ Name ( Name : ) + * DirectiveArgumentCoordinate : @ Name ( Name : ) */ function resolveDirectiveArgumentCoordinate( schema: GraphQLSchema, @@ -287,7 +276,6 @@ function resolveDirectiveArgumentCoordinate( } // 4. Let {directiveArgumentName} be the value of the second {Name}. - // 5. Let {directiveArgument} be the argument of {directive} named {directiveArgumentName}. const { argumentName: { value: directiveArgumentName }, } = schemaCoordinate; @@ -295,12 +283,11 @@ function resolveDirectiveArgumentCoordinate( (arg) => arg.name === directiveArgumentName, ); - // 6. If {directiveArgument} does not exist, return {null}. + // 5. Return the argument of {directive} named {directiveArgumentName}, or {null} if no such argument exists. if (!directiveArgument) { return; } - // 7. Return {directiveArgument}. return { kind: 'DirectiveArgument', directive, directiveArgument }; } From 5eea2b1ad08206b2a423a4c93aa8098c8d8521c7 Mon Sep 17 00:00:00 2001 From: Mark Larah Date: Thu, 5 Jun 2025 11:48:03 -0500 Subject: [PATCH 17/17] add enum syntax --- src/language/__tests__/parser-test.ts | 22 ++++- src/language/__tests__/predicates-test.ts | 3 +- src/language/ast.ts | 22 +++-- src/language/kinds_.ts | 7 +- src/language/lexer.ts | 9 ++ src/language/parser.ts | 20 ++++- src/language/predicates.ts | 3 +- src/language/printer.ts | 8 +- src/language/tokenKind.ts | 1 + .../__tests__/resolveSchemaCoordinate-test.ts | 6 +- src/utilities/resolveSchemaCoordinate.ts | 86 ++++++++++++------- 11 files changed, 134 insertions(+), 53 deletions(-) diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index c0d247ddf5..ba3ef79cd9 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -703,14 +703,14 @@ describe('Parser', () => { it('parses Name . Name', () => { const result = parseSchemaCoordinate('MyType.field'); expectJSON(result).toDeepEqual({ - kind: Kind.MEMBER_COORDINATE, + kind: Kind.FIELD_COORDINATE, loc: { start: 0, end: 12 }, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, value: 'MyType', }, - memberName: { + fieldName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', @@ -727,6 +727,24 @@ describe('Parser', () => { }); }); + it('parses Name :: Name', () => { + const result = parseSchemaCoordinate('MyEnum::value'); + expectJSON(result).toDeepEqual({ + kind: Kind.VALUE_COORDINATE, + loc: { start: 0, end: 13 }, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyEnum', + }, + valueName: { + kind: Kind.NAME, + loc: { start: 8, end: 13 }, + value: 'value', + }, + }); + }); + it('parses Name . Name ( Name : )', () => { const result = parseSchemaCoordinate('MyType.field(arg:)'); expectJSON(result).toDeepEqual({ diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 57907d6aa6..7455fd73e4 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -148,8 +148,9 @@ describe('AST node predicates', () => { 'ArgumentCoordinate', 'DirectiveArgumentCoordinate', 'DirectiveCoordinate', - 'MemberCoordinate', + 'FieldCoordinate', 'TypeCoordinate', + 'ValueCoordinate', ]); }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index 812b988835..268a2ddd98 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -183,8 +183,9 @@ export type ASTNode = | EnumTypeExtensionNode | InputObjectTypeExtensionNode | TypeCoordinateNode - | MemberCoordinateNode + | FieldCoordinateNode | ArgumentCoordinateNode + | ValueCoordinateNode | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; @@ -295,8 +296,9 @@ export const QueryDocumentKeys: { // Schema Coordinates TypeCoordinate: ['name'], - MemberCoordinate: ['name', 'memberName'], + FieldCoordinate: ['name', 'fieldName'], ArgumentCoordinate: ['name', 'fieldName', 'argumentName'], + ValueCoordinate: ['name', 'valueName'], DirectiveCoordinate: ['name'], DirectiveArgumentCoordinate: ['name', 'argumentName'], }; @@ -779,8 +781,9 @@ export interface InputObjectTypeExtensionNode { export type SchemaCoordinateNode = | TypeCoordinateNode - | MemberCoordinateNode + | FieldCoordinateNode | ArgumentCoordinateNode + | ValueCoordinateNode | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; @@ -790,11 +793,11 @@ export interface TypeCoordinateNode { readonly name: NameNode; } -export interface MemberCoordinateNode { - readonly kind: typeof Kind.MEMBER_COORDINATE; +export interface FieldCoordinateNode { + readonly kind: typeof Kind.FIELD_COORDINATE; readonly loc?: Location; readonly name: NameNode; - readonly memberName: NameNode; + readonly fieldName: NameNode; } export interface ArgumentCoordinateNode { @@ -805,6 +808,13 @@ export interface ArgumentCoordinateNode { readonly argumentName: NameNode; } +export interface ValueCoordinateNode { + readonly kind: typeof Kind.VALUE_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; + readonly valueName: NameNode; +} + export interface DirectiveCoordinateNode { readonly kind: typeof Kind.DIRECTIVE_COORDINATE; readonly loc?: Location; diff --git a/src/language/kinds_.ts b/src/language/kinds_.ts index 252feb6107..24d909fdfe 100644 --- a/src/language/kinds_.ts +++ b/src/language/kinds_.ts @@ -113,12 +113,15 @@ export type INPUT_OBJECT_TYPE_EXTENSION = typeof INPUT_OBJECT_TYPE_EXTENSION; export const TYPE_COORDINATE = 'TypeCoordinate'; export type TYPE_COORDINATE = typeof TYPE_COORDINATE; -export const MEMBER_COORDINATE = 'MemberCoordinate'; -export type MEMBER_COORDINATE = typeof MEMBER_COORDINATE; +export const FIELD_COORDINATE = 'FieldCoordinate'; +export type FIELD_COORDINATE = typeof FIELD_COORDINATE; export const ARGUMENT_COORDINATE = 'ArgumentCoordinate'; export type ARGUMENT_COORDINATE = typeof ARGUMENT_COORDINATE; +export const VALUE_COORDINATE = 'ValueCoordinate'; +export type VALUE_COORDINATE = typeof VALUE_COORDINATE; + export const DIRECTIVE_COORDINATE = 'DirectiveCoordinate'; export type DIRECTIVE_COORDINATE = typeof DIRECTIVE_COORDINATE; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 44abc05197..a2d305e645 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -98,6 +98,7 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean { kind === TokenKind.DOT || kind === TokenKind.SPREAD || kind === TokenKind.COLON || + kind === TokenKind.TWO_COLON || kind === TokenKind.EQUALS || kind === TokenKind.AT || kind === TokenKind.BRACKET_L || @@ -271,6 +272,14 @@ function readNextToken(lexer: Lexer, start: number): Token { return readDot(lexer, position); } case 0x003a: // : + if (body.charCodeAt(position + 1) === 0x003a) { + return createToken( + lexer, + TokenKind.TWO_COLON, + position, + position + 2, + ); + } return createToken(lexer, TokenKind.COLON, position, position + 1); case 0x003d: // = return createToken(lexer, TokenKind.EQUALS, position, position + 1); diff --git a/src/language/parser.ts b/src/language/parser.ts index de049abeb5..31fa99d074 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -23,6 +23,7 @@ import type { EnumTypeExtensionNode, EnumValueDefinitionNode, EnumValueNode, + FieldCoordinateNode, FieldDefinitionNode, FieldNode, FloatValueNode, @@ -38,7 +39,6 @@ import type { IntValueNode, ListTypeNode, ListValueNode, - MemberCoordinateNode, NamedTypeNode, NameNode, NonNullTypeNode, @@ -63,6 +63,7 @@ import type { TypeSystemExtensionNode, UnionTypeDefinitionNode, UnionTypeExtensionNode, + ValueCoordinateNode, ValueNode, VariableDefinitionNode, VariableNode, @@ -1466,6 +1467,7 @@ export class Parser { * - Name * - Name . Name * - Name . Name ( Name : ) + * - Name :: Name * - @ Name * - @ Name ( Name : ) */ @@ -1473,6 +1475,16 @@ export class Parser { const start = this._lexer.token; const ofDirective = this.expectOptionalToken(TokenKind.AT); const name = this.parseName(); + + if (!ofDirective && this.expectOptionalToken(TokenKind.TWO_COLON)) { + const valueName = this.parseName(); + return this.node(start, { + kind: Kind.VALUE_COORDINATE, + name, + valueName, + }); + } + let memberName: NameNode | undefined; if (!ofDirective && this.expectOptionalToken(TokenKind.DOT)) { memberName = this.parseName(); @@ -1508,10 +1520,10 @@ export class Parser { argumentName, }); } - return this.node(start, { - kind: Kind.MEMBER_COORDINATE, + return this.node(start, { + kind: Kind.FIELD_COORDINATE, name, - memberName, + fieldName: memberName, }); } diff --git a/src/language/predicates.ts b/src/language/predicates.ts index 5146e8244e..488e9828f2 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -117,8 +117,9 @@ export function isSchemaCoordinateNode( ): node is SchemaCoordinateNode { return ( node.kind === Kind.TYPE_COORDINATE || - node.kind === Kind.MEMBER_COORDINATE || + node.kind === Kind.FIELD_COORDINATE || node.kind === Kind.ARGUMENT_COORDINATE || + node.kind === Kind.VALUE_COORDINATE || node.kind === Kind.DIRECTIVE_COORDINATE || node.kind === Kind.DIRECTIVE_ARGUMENT_COORDINATE ); diff --git a/src/language/printer.ts b/src/language/printer.ts index 823b14a02d..2701f8373b 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -325,8 +325,8 @@ const printDocASTReducer: ASTReducer = { TypeCoordinate: { leave: ({ name }) => name }, - MemberCoordinate: { - leave: ({ name, memberName }) => join([name, wrap('.', memberName)]), + FieldCoordinate: { + leave: ({ name, fieldName }) => join([name, wrap('.', fieldName)]), }, ArgumentCoordinate: { @@ -334,6 +334,10 @@ const printDocASTReducer: ASTReducer = { join([name, wrap('.', fieldName), wrap('(', argumentName, ':)')]), }, + ValueCoordinate: { + leave: ({ name, valueName }) => join([name, wrap('::', valueName)]), + }, + DirectiveCoordinate: { leave: ({ name }) => join(['@', name]) }, DirectiveArgumentCoordinate: { diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index 7872370675..f6547f095a 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -13,6 +13,7 @@ export const TokenKind = { DOT: '.', SPREAD: '...' as const, COLON: ':' as const, + TWO_COLON: '::' as const, EQUALS: '=' as const, AT: '@' as const, BRACKET_L: '[' as const, diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index a4eabc4ed9..0fa9cfdf10 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -71,7 +71,7 @@ describe('resolveSchemaCoordinate', () => { ); expect(() => resolveSchemaCoordinate(schema, 'String.field')).to.throw( - 'Expected "String" to be an object type, interface type, input object type, or enum type.', + 'Expected "String" to be an Input Object, Object or Interface type.', ); }); @@ -101,7 +101,7 @@ describe('resolveSchemaCoordinate', () => { const type = schema.getType('SearchFilter') as GraphQLEnumType; const enumValue = type.getValue('OPEN_NOW'); expect( - resolveSchemaCoordinate(schema, 'SearchFilter.OPEN_NOW'), + resolveSchemaCoordinate(schema, 'SearchFilter::OPEN_NOW'), ).to.deep.equal({ kind: 'EnumValue', type, @@ -109,7 +109,7 @@ describe('resolveSchemaCoordinate', () => { }); expect( - resolveSchemaCoordinate(schema, 'SearchFilter.UNKNOWN'), + resolveSchemaCoordinate(schema, 'SearchFilter::UNKNOWN'), ).to.deep.equal(undefined); }); diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 7e66e4ecc6..afebe13199 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -4,9 +4,10 @@ import type { ArgumentCoordinateNode, DirectiveArgumentCoordinateNode, DirectiveCoordinateNode, - MemberCoordinateNode, + FieldCoordinateNode, SchemaCoordinateNode, TypeCoordinateNode, + ValueCoordinateNode, } from '../language/ast.js'; import { Kind } from '../language/kinds.js'; import { parseSchemaCoordinate } from '../language/parser.js'; @@ -118,52 +119,37 @@ function resolveTypeCoordinate( } /** - * MemberCoordinate : Name . Name + * FieldCoordinate : Name . Name */ -function resolveMemberCoordinate( +function resolveFieldCoordinate( schema: GraphQLSchema, - schemaCoordinate: MemberCoordinateNode, -): ResolvedField | ResolvedInputField | ResolvedEnumValue | undefined { + schemaCoordinate: FieldCoordinateNode, +): ResolvedField | ResolvedInputField | undefined { // 1. Let {typeName} be the value of the first {Name}. // 2. Let {type} be the type in the {schema} named {typeName}. const typeName = schemaCoordinate.name.value; const type = schema.getType(typeName); - // 3. Assert: {type} must exist, and must be an Enum, Input Object, Object or Interface type. + // 3. Assert: {type} must exist, and must be an Input Object, Object or Interface type. if (!type) { throw new Error( `Expected ${inspect(typeName)} to be defined as a type in the schema.`, ); } if ( - !isEnumType(type) && !isInputObjectType(type) && !isObjectType(type) && !isInterfaceType(type) ) { throw new Error( - `Expected ${inspect(typeName)} to be an object type, interface type, input object type, or enum type.`, + `Expected ${inspect(typeName)} to be an Input Object, Object or Interface type.`, ); } - // 4. If {type} is an Enum type: - if (isEnumType(type)) { - // 1. Let {enumValueName} be the value of the second {Name}. - const enumValueName = schemaCoordinate.memberName.value; - const enumValue = type.getValue(enumValueName); - - // 2. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. - if (enumValue == null) { - return; - } - - return { kind: 'EnumValue', type, enumValue }; - } - - // 5. Otherwise, if {type} is an Input Object type: + // 4. If {type} is an Input Object type: if (isInputObjectType(type)) { // 1. Let {inputFieldName} be the value of the second {Name}. - const inputFieldName = schemaCoordinate.memberName.value; + const inputFieldName = schemaCoordinate.fieldName.value; const inputField = type.getFields()[inputFieldName]; // 2. Return the input field of {type} named {inputFieldName}, or {null} if no such input field exists. @@ -174,9 +160,9 @@ function resolveMemberCoordinate( return { kind: 'InputField', type, inputField }; } - // 6. Otherwise: + // 5. Otherwise: // 1. Let {fieldName} be the value of the second {Name}. - const fieldName = schemaCoordinate.memberName.value; + const fieldName = schemaCoordinate.fieldName.value; const field = type.getFields()[fieldName]; // 2. Return the field of {type} named {fieldName}, or {null} if no such field exists. @@ -237,6 +223,40 @@ function resolveArgumentCoordinate( return { kind: 'FieldArgument', type, field, fieldArgument }; } +/** + * ValueCoordinate : Name :: Name + */ +function resolveValueCoordinate( + schema: GraphQLSchema, + schemaCoordinate: ValueCoordinateNode, +): ResolvedEnumValue | undefined { + // 1. Let {typeName} be the value of the first {Name}. + // 2. Let {type} be the type in the {schema} named {typeName}. + const typeName = schemaCoordinate.name.value; + const type = schema.getType(typeName); + + // 3. Assert: {type} must exist, and must be an Enum type. + if (!type) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + if (!isEnumType(type)) { + throw new Error(`Expected ${inspect(typeName)} to be an Enum type.`); + } + + // 4. Let {enumValueName} be the value of the second {Name}. + const enumValueName = schemaCoordinate.valueName.value; + const enumValue = type.getValue(enumValueName); + + // 5. Return the enum value of {type} named {enumValueName}, or {null} if no such value exists. + if (enumValue == null) { + return; + } + + return { kind: 'EnumValue', type, enumValue }; +} + /** * DirectiveCoordinate : @ Name */ @@ -299,15 +319,17 @@ export function resolveASTSchemaCoordinate( schemaCoordinate: SchemaCoordinateNode, ): ResolvedSchemaElement | undefined { switch (schemaCoordinate.kind) { - case Kind.DIRECTIVE_COORDINATE: - return resolveDirectiveCoordinate(schema, schemaCoordinate); - case Kind.DIRECTIVE_ARGUMENT_COORDINATE: - return resolveDirectiveArgumentCoordinate(schema, schemaCoordinate); case Kind.TYPE_COORDINATE: return resolveTypeCoordinate(schema, schemaCoordinate); - case Kind.MEMBER_COORDINATE: - return resolveMemberCoordinate(schema, schemaCoordinate); + case Kind.FIELD_COORDINATE: + return resolveFieldCoordinate(schema, schemaCoordinate); case Kind.ARGUMENT_COORDINATE: return resolveArgumentCoordinate(schema, schemaCoordinate); + case Kind.VALUE_COORDINATE: + return resolveValueCoordinate(schema, schemaCoordinate); + case Kind.DIRECTIVE_COORDINATE: + return resolveDirectiveCoordinate(schema, schemaCoordinate); + case Kind.DIRECTIVE_ARGUMENT_COORDINATE: + return resolveDirectiveArgumentCoordinate(schema, schemaCoordinate); } }