diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index ca9753bcc2..ba3ef79cd9 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -690,36 +690,31 @@ 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.FIELD_COORDINATE, loc: { start: 0, end: 12 }, - ofDirective: false, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, value: 'MyType', }, - memberName: { + fieldName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', }, - argumentName: undefined, }); }); @@ -732,18 +727,35 @@ 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({ - kind: Kind.SCHEMA_COORDINATE, + kind: Kind.ARGUMENT_COORDINATE, loc: { start: 0, end: 18 }, - ofDirective: false, name: { kind: Kind.NAME, loc: { start: 0, end: 6 }, value: 'MyType', }, - memberName: { + fieldName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', @@ -768,31 +780,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..7455fd73e4 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -145,7 +145,12 @@ describe('AST node predicates', () => { it('isSchemaCoordinateNode', () => { expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([ - 'SchemaCoordinate', + 'ArgumentCoordinate', + 'DirectiveArgumentCoordinate', + 'DirectiveCoordinate', + 'FieldCoordinate', + 'TypeCoordinate', + 'ValueCoordinate', ]); }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index af1c2d6ca7..268a2ddd98 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -182,7 +182,12 @@ export type ASTNode = | UnionTypeExtensionNode | EnumTypeExtensionNode | InputObjectTypeExtensionNode - | SchemaCoordinateNode; + | TypeCoordinateNode + | FieldCoordinateNode + | ArgumentCoordinateNode + | ValueCoordinateNode + | DirectiveCoordinateNode + | DirectiveArgumentCoordinateNode; /** * Utility type listing all nodes indexed by their kind. @@ -288,7 +293,14 @@ export const QueryDocumentKeys: { UnionTypeExtension: ['name', 'directives', 'types'], EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], - SchemaCoordinate: ['name', 'memberName', 'argumentName'], + + // Schema Coordinates + TypeCoordinate: ['name'], + FieldCoordinate: ['name', 'fieldName'], + ArgumentCoordinate: ['name', 'fieldName', 'argumentName'], + ValueCoordinate: ['name', 'valueName'], + DirectiveCoordinate: ['name'], + DirectiveArgumentCoordinate: ['name', 'argumentName'], }; const kindValues = new Set(Object.keys(QueryDocumentKeys)); @@ -765,13 +777,53 @@ export interface InputObjectTypeExtensionNode { readonly fields?: ReadonlyArray | undefined; } -// Schema Coordinates +/** Schema Coordinates */ + +export type SchemaCoordinateNode = + | TypeCoordinateNode + | FieldCoordinateNode + | ArgumentCoordinateNode + | ValueCoordinateNode + | DirectiveCoordinateNode + | DirectiveArgumentCoordinateNode; + +export interface TypeCoordinateNode { + readonly kind: typeof Kind.TYPE_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; +} + +export interface FieldCoordinateNode { + readonly kind: typeof Kind.FIELD_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; + readonly fieldName: NameNode; +} + +export interface ArgumentCoordinateNode { + readonly kind: typeof Kind.ARGUMENT_COORDINATE; + readonly loc?: Location; + readonly name: NameNode; + readonly fieldName: NameNode; + 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; + 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..24d909fdfe 100644 --- a/src/language/kinds_.ts +++ b/src/language/kinds_.ts @@ -110,5 +110,21 @@ 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 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; + +export const DIRECTIVE_ARGUMENT_COORDINATE = 'DirectiveArgumentCoordinate'; +export type DIRECTIVE_ARGUMENT_COORDINATE = + typeof DIRECTIVE_ARGUMENT_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 cb72914f07..31fa99d074 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, @@ -20,6 +23,7 @@ import type { EnumTypeExtensionNode, EnumValueDefinitionNode, EnumValueNode, + FieldCoordinateNode, FieldDefinitionNode, FieldNode, FloatValueNode, @@ -54,10 +58,12 @@ import type { SelectionSetNode, StringValueNode, Token, + TypeCoordinateNode, TypeNode, TypeSystemExtensionNode, UnionTypeDefinitionNode, UnionTypeExtensionNode, + ValueCoordinateNode, ValueNode, VariableDefinitionNode, VariableNode, @@ -1461,6 +1467,7 @@ export class Parser { * - Name * - Name . Name * - Name . Name ( Name : ) + * - Name :: Name * - @ Name * - @ Name ( Name : ) */ @@ -1468,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(); @@ -1481,12 +1498,38 @@ export class Parser { this.expectToken(TokenKind.COLON); this.expectToken(TokenKind.PAREN_R); } - return this.node(start, { - kind: Kind.SCHEMA_COORDINATE, - ofDirective, + + if (ofDirective) { + if (argumentName) { + return this.node(start, { + kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE, + name, + argumentName, + }); + } + return this.node(start, { + kind: Kind.DIRECTIVE_COORDINATE, + name, + }); + } else if (memberName) { + if (argumentName) { + return this.node(start, { + kind: Kind.ARGUMENT_COORDINATE, + name, + fieldName: memberName, + argumentName, + }); + } + return this.node(start, { + kind: Kind.FIELD_COORDINATE, + name, + fieldName: 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..488e9828f2 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -115,5 +115,12 @@ 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.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 dcc9f048b5..2701f8373b 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -321,16 +321,28 @@ 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 }, + + FieldCoordinate: { + leave: ({ name, fieldName }) => join([name, wrap('.', fieldName)]), + }, + + ArgumentCoordinate: { + leave: ({ name, fieldName, argumentName }) => + join([name, wrap('.', fieldName), wrap('(', argumentName, ':)')]), + }, + + ValueCoordinate: { + leave: ({ name, valueName }) => join([name, wrap('::', valueName)]), + }, + + DirectiveCoordinate: { leave: ({ name }) => join(['@', name]) }, + + DirectiveArgumentCoordinate: { + leave: ({ name, argumentName }) => + join(['@', name, wrap('(', argumentName, ':)')]), }, }; 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 e316ef52a1..0fa9cfdf10 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 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); }); @@ -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 a field of type "Business" in the schema.', + ); - expect( + expect(() => resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'), - ).to.deep.equal(undefined); + ).to.throw( + 'Expected "SearchCriteria" to be an object type or interface type.', + ); }); 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 026076672c..afebe13199 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -1,4 +1,15 @@ -import type { SchemaCoordinateNode } from '../language/ast.js'; +import { inspect } from '../jsutils/inspect.js'; + +import type { + ArgumentCoordinateNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, + FieldCoordinateNode, + SchemaCoordinateNode, + TypeCoordinateNode, + ValueCoordinateNode, +} 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 +32,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,116 +100,236 @@ export function resolveSchemaCoordinate( } /** - * Resolves schema coordinate from a parsed SchemaCoordinate node. + * TypeCoordinate : Name */ -export function resolveASTSchemaCoordinate( +function resolveTypeCoordinate( schema: GraphQLSchema, - schemaCoordinate: SchemaCoordinateNode, -): ResolvedSchemaElement | undefined { - const { ofDirective, name, memberName, argumentName } = schemaCoordinate; - if (ofDirective) { - // 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) { - // SchemaCoordinate : @ Name - // Return the directive in the {schema} named {directiveName}. - if (!directive) { - return; - } - return { kind: 'Directive', directive }; - } + schemaCoordinate: TypeCoordinateNode, +): ResolvedNamedType | undefined { + // 1. Let {typeName} be the value of {Name}. + const typeName = schemaCoordinate.name.value; + const type = schema.getType(typeName); - // SchemaCoordinate : @ Name ( Name : ) - // Assert {directive} must exist. - if (!directive) { - return; - } - // Let {directiveArgumentName} be the value of the second {Name}. - // Return the argument of {directive} named {directiveArgumentName}. - const directiveArgument = directive.args.find( - (arg) => arg.name === argumentName.value, + // 2. Return the type in the {schema} named {typeName}, or {null} if no such type exists. + if (type == null) { + return; + } + + return { kind: 'NamedType', type }; +} + +/** + * FieldCoordinate : Name . Name + */ +function resolveFieldCoordinate( + schema: GraphQLSchema, + 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 Input Object, Object or Interface type. + if (!type) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type 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}. - const type = schema.getType(name.value); - if (!memberName) { - // SchemaCoordinate : Name - // Return the type in the {schema} named {typeName}. - if (!type) { - return; - } - return { kind: 'NamedType', type }; - } - - if (!argumentName) { - // 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); - 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.value]; - 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; - } - // Let {fieldName} be the value of the second {Name}. - // Return the field of {type} named {fieldName}. - const field = type.getFields()[memberName.value]; - if (field == null) { + } + if ( + !isInputObjectType(type) && + !isObjectType(type) && + !isInterfaceType(type) + ) { + throw new Error( + `Expected ${inspect(typeName)} to be an Input Object, Object or Interface 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.fieldName.value; + const inputField = type.getFields()[inputFieldName]; + + // 2. Return the input field of {type} named {inputFieldName}, or {null} if no such input field exists. + if (inputField == null) { return; } - return { kind: 'Field', type, field }; + + return { kind: 'InputField', type, inputField }; } - // SchemaCoordinate : Name . Name ( Name : ) - // Assert {type} must be an Object or Interface type. - if (!isObjectType(type) && !isInterfaceType(type)) { + // 5. Otherwise: + // 1. Let {fieldName} be the value of the second {Name}. + 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. + if (field == null) { return; } - // Let {fieldName} be the value of the second {Name}. - // Let {field} be the field of {type} named {fieldName}. - const field = type.getFields()[memberName.value]; - // Assert {field} must exist. + + return { kind: 'Field', type, field }; +} + +/** + * ArgumentCoordinate : Name . Name ( Name : ) + */ +function resolveArgumentCoordinate( + schema: GraphQLSchema, + schemaCoordinate: ArgumentCoordinateNode, +): ResolvedFieldArgument | 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 be an Object or Interface type. + if (type == null) { + throw new Error( + `Expected ${inspect(typeName)} to be defined as a type in the schema.`, + ); + } + if (!isObjectType(type) && !isInterfaceType(type)) { + throw new Error( + `Expected ${inspect(typeName)} to be an object type or interface type.`, + ); + } + + // 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. if (field == null) { - return; + 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}. - // Return 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 === argumentName.value, + (arg) => arg.name === fieldArgumentName, ); + + // 8. Return the argument of {field} named {fieldArgumentName}, or {null} if no such argument exists. if (fieldArgument == null) { return; } + 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 + */ +function resolveDirectiveCoordinate( + schema: GraphQLSchema, + schemaCoordinate: DirectiveCoordinateNode, +): ResolvedDirective | undefined { + // 1. Let {directiveName} be the value of {Name}. + const directiveName = schemaCoordinate.name.value; + const directive = schema.getDirective(directiveName); + + // 2. Return the directive in the {schema} named {directiveName}, or {null} if no such directive exists. + if (!directive) { + return; + } + + return { kind: 'Directive', directive }; +} + +/** + * DirectiveArgumentCoordinate : @ 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}. + const { + argumentName: { value: directiveArgumentName }, + } = schemaCoordinate; + const directiveArgument = directive.args.find( + (arg) => arg.name === directiveArgumentName, + ); + + // 5. Return the argument of {directive} named {directiveArgumentName}, or {null} if no such argument exists. + if (!directiveArgument) { + return; + } + + return { kind: 'DirectiveArgument', directive, directiveArgument }; +} + +/** + * Resolves schema coordinate from a parsed SchemaCoordinate node. + */ +export function resolveASTSchemaCoordinate( + schema: GraphQLSchema, + schemaCoordinate: SchemaCoordinateNode, +): ResolvedSchemaElement | undefined { + switch (schemaCoordinate.kind) { + case Kind.TYPE_COORDINATE: + return resolveTypeCoordinate(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); + } +}