diff --git a/src/index.ts b/src/index.ts index cb4e5443d2d..7ab226f34ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -182,7 +182,6 @@ export type { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, - GraphQLSchemaElement, } from './type/index'; /** Parse and operate on GraphQL language source files. */ @@ -471,4 +470,5 @@ export type { BreakingChange, DangerousChange, TypedQueryDocumentNode, + GraphQLSchemaElement, } from './utilities/index'; diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index 3d1da4767fd..a9b1815f8dc 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -638,7 +638,7 @@ describe('Parser', () => { loc: { start: 0, end: 6 }, value: 'MyType', }, - fieldName: undefined, + memberName: undefined, argumentName: undefined, }); }); @@ -654,7 +654,7 @@ describe('Parser', () => { loc: { start: 0, end: 6 }, value: 'MyType', }, - fieldName: { + memberName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', @@ -683,7 +683,7 @@ describe('Parser', () => { loc: { start: 0, end: 6 }, value: 'MyType', }, - fieldName: { + memberName: { kind: Kind.NAME, loc: { start: 7, end: 12 }, value: 'field', @@ -716,7 +716,7 @@ describe('Parser', () => { loc: { start: 1, end: 12 }, value: 'myDirective', }, - fieldName: undefined, + memberName: undefined, argumentName: undefined, }); }); @@ -732,7 +732,7 @@ describe('Parser', () => { loc: { start: 1, end: 12 }, value: 'myDirective', }, - fieldName: undefined, + memberName: undefined, argumentName: { kind: Kind.NAME, loc: { start: 13, end: 16 }, diff --git a/src/language/ast.ts b/src/language/ast.ts index 28d26d081a6..58a70167284 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -680,6 +680,6 @@ export interface SchemaCoordinateNode { readonly loc?: Location; readonly isDirective: boolean; readonly name: NameNode; - readonly fieldName?: NameNode; + readonly memberName?: NameNode; readonly argumentName?: NameNode; } diff --git a/src/language/parser.ts b/src/language/parser.ts index ddccbb3ada2..37904b5d2ec 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1386,13 +1386,13 @@ export class Parser { const start = this._lexer.token; const isDirective = this.expectOptionalToken(TokenKind.AT); const name = this.parseName(); - let fieldName; + let memberName; if (!isDirective && this.expectOptionalToken(TokenKind.DOT)) { - fieldName = this.parseName(); + memberName = this.parseName(); } let argumentName; if ( - (isDirective || fieldName) && + (isDirective || memberName) && this.expectOptionalToken(TokenKind.PAREN_L) ) { argumentName = this.parseName(); @@ -1403,7 +1403,7 @@ export class Parser { kind: Kind.SCHEMA_COORDINATE, isDirective, name, - fieldName, + memberName, argumentName, }); } diff --git a/src/language/printer.ts b/src/language/printer.ts index 0ad83fba1ee..02277e98d27 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -306,11 +306,11 @@ const printDocASTReducer: ASTReducer = { // Schema Coordinate SchemaCoordinate: { - leave: ({ isDirective, name, fieldName, argumentName }) => + leave: ({ isDirective, name, memberName, argumentName }) => join([ isDirective && '@', name, - wrap('.', fieldName), + wrap('.', memberName), wrap('(', argumentName, ':)'), ]), }, diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 69163405fb3..723515f5b3e 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -161,7 +161,7 @@ const QueryDocumentKeys = { EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], - SchemaCoordinate: ['name', 'fieldName', 'argumentName'], + SchemaCoordinate: ['name', 'memberName', 'argumentName'], }; export const BREAK: unknown = Object.freeze({}); diff --git a/src/type/element.ts b/src/type/element.ts deleted file mode 100644 index 6364b36a9a9..00000000000 --- a/src/type/element.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { - GraphQLNamedType, - GraphQLField, - GraphQLInputField, - GraphQLEnumValue, - GraphQLArgument, -} from './definition'; -import type { GraphQLDirective } from './directives'; - -export type GraphQLSchemaElement = - | GraphQLNamedType - | GraphQLField - | GraphQLInputField - | GraphQLEnumValue - | GraphQLArgument - | GraphQLDirective; diff --git a/src/type/index.ts b/src/type/index.ts index 90b0a01203b..fab8bb65dcb 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -179,6 +179,3 @@ export type { /** Validate GraphQL schema. */ export { validateSchema, assertValidSchema } from './validate'; - -// Schema Element type. -export { GraphQLSchemaElement } from './element'; diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts index 1133a73da4b..bf7eb0af06a 100644 --- a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -1,11 +1,18 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; +import type { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLObjectType, +} from '../../type/definition'; +import type { GraphQLDirective } from '../../type/directives'; + import { buildSchema } from '../buildASTSchema'; import { resolveSchemaCoordinate } from '../resolveSchemaCoordinate'; describe('resolveSchemaCoordinate', () => { - const schema: any = buildSchema(` + const schema = buildSchema(` type Query { searchBusiness(criteria: SearchCriteria!): [Business] } @@ -31,116 +38,147 @@ describe('resolveSchemaCoordinate', () => { `); it('resolves a Named Type', () => { - const expected = schema.getType('Business'); - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'Business')).to.equal(expected); + expect(resolveSchemaCoordinate(schema, 'Business')).to.deep.equal({ + kind: 'NamedType', + type: schema.getType('Business'), + }); - expect(resolveSchemaCoordinate(schema, 'String')).to.equal( - schema.getType('String'), - ); + expect(resolveSchemaCoordinate(schema, 'String')).to.deep.equal({ + kind: 'NamedType', + type: schema.getType('String'), + }); - expect(resolveSchemaCoordinate(schema, 'private')).to.equal(undefined); + expect(resolveSchemaCoordinate(schema, 'private')).to.deep.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'Unknown')).to.equal(undefined); + expect(resolveSchemaCoordinate(schema, 'Unknown')).to.deep.equal(undefined); }); it('resolves a Type Field', () => { - const expected = schema.getType('Business').getFields().name; - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'Business.name')).to.equal(expected); - - expect(resolveSchemaCoordinate(schema, 'Business.unknown')).to.equal( + const type = schema.getType('Business') as GraphQLObjectType; + const field = type.getFields().name; + expect(resolveSchemaCoordinate(schema, 'Business.name')).to.deep.equal({ + kind: 'Field', + type, + field, + }); + + expect(resolveSchemaCoordinate(schema, 'Business.unknown')).to.deep.equal( undefined, ); - expect(resolveSchemaCoordinate(schema, 'Unknown.field')).to.equal( + expect(resolveSchemaCoordinate(schema, 'Unknown.field')).to.deep.equal( undefined, ); - expect(resolveSchemaCoordinate(schema, 'String.field')).to.equal(undefined); + expect(resolveSchemaCoordinate(schema, 'String.field')).to.deep.equal( + undefined, + ); }); it('does not resolve meta-fields', () => { - expect(resolveSchemaCoordinate(schema, 'Business.__typename')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'Business.__typename'), + ).to.deep.equal(undefined); }); it('resolves a Input Field', () => { - const expected = schema.getType('SearchCriteria').getFields().filter; - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'SearchCriteria.filter')).to.equal( - expected, - ); + const type = schema.getType('SearchCriteria') as GraphQLInputObjectType; + const inputField = type.getFields().filter; + expect( + resolveSchemaCoordinate(schema, 'SearchCriteria.filter'), + ).to.deep.equal({ + kind: 'InputField', + type, + inputField, + }); - expect(resolveSchemaCoordinate(schema, 'SearchCriteria.unknown')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'SearchCriteria.unknown'), + ).to.deep.equal(undefined); }); it('resolves a Enum Value', () => { - const expected = schema.getType('SearchFilter').getValue('OPEN_NOW'); - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'SearchFilter.OPEN_NOW')).to.equal( - expected, - ); + const type = schema.getType('SearchFilter') as GraphQLEnumType; + const enumValue = type.getValue('OPEN_NOW'); + expect( + resolveSchemaCoordinate(schema, 'SearchFilter.OPEN_NOW'), + ).to.deep.equal({ + kind: 'EnumValue', + type, + enumValue, + }); - expect(resolveSchemaCoordinate(schema, 'SearchFilter.UNKNOWN')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'SearchFilter.UNKNOWN'), + ).to.deep.equal(undefined); }); it('resolves a Field Argument', () => { - const expected = schema - .getType('Query') - .getFields() - .searchBusiness.args.find((arg: any) => arg.name === 'criteria'); - expect(expected).not.to.equal(undefined); + const type = schema.getType('Query') as GraphQLObjectType; + const field = type.getFields().searchBusiness; + const fieldArgument = field.args.find((arg) => arg.name === 'criteria'); expect( resolveSchemaCoordinate(schema, 'Query.searchBusiness(criteria:)'), - ).to.equal(expected); + ).to.deep.equal({ + kind: 'FieldArgument', + type, + field, + fieldArgument, + }); - expect(resolveSchemaCoordinate(schema, 'Business.name(unknown:)')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'Business.name(unknown:)'), + ).to.deep.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'Unknown.field(arg:)')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'Unknown.field(arg:)'), + ).to.deep.equal(undefined); - expect(resolveSchemaCoordinate(schema, 'Business.unknown(arg:)')).to.equal( - undefined, - ); + expect( + resolveSchemaCoordinate(schema, 'Business.unknown(arg:)'), + ).to.deep.equal(undefined); expect( resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'), - ).to.equal(undefined); + ).to.deep.equal(undefined); }); it('resolves a Directive', () => { - const expected = schema.getDirective('private'); - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, '@private')).to.equal(expected); + expect(resolveSchemaCoordinate(schema, '@private')).to.deep.equal({ + kind: 'Directive', + directive: schema.getDirective('private'), + }); + + expect(resolveSchemaCoordinate(schema, '@deprecated')).to.deep.equal({ + kind: 'Directive', + directive: schema.getDirective('deprecated'), + }); - expect(resolveSchemaCoordinate(schema, '@unknown')).to.equal(undefined); + expect(resolveSchemaCoordinate(schema, '@unknown')).to.deep.equal( + undefined, + ); - expect(resolveSchemaCoordinate(schema, '@Business')).to.equal(undefined); + expect(resolveSchemaCoordinate(schema, '@Business')).to.deep.equal( + undefined, + ); }); it('resolves a Directive Argument', () => { - const expected = schema - .getDirective('private') - .args.find((arg: any) => arg.name === 'scope'); - expect(expected).not.to.equal(undefined); - expect(resolveSchemaCoordinate(schema, '@private(scope:)')).to.equal( - expected, + const directive = schema.getDirective('private') as GraphQLDirective; + const directiveArgument = directive.args.find( + (arg) => arg.name === 'scope', ); + expect(resolveSchemaCoordinate(schema, '@private(scope:)')).to.deep.equal({ + kind: 'DirectiveArgument', + directive, + directiveArgument, + }); - expect(resolveSchemaCoordinate(schema, '@private(unknown:)')).to.equal( + expect(resolveSchemaCoordinate(schema, '@private(unknown:)')).to.deep.equal( undefined, ); - expect(resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.equal( + expect(resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.deep.equal( undefined, ); }); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 12387395d31..f41823eca7f 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -109,6 +109,7 @@ export { TypedQueryDocumentNode } from './typedQueryDocumentNode'; /** Schema coordinates */ export { + GraphQLSchemaElement, resolveSchemaCoordinate, resolveASTSchemaCoordinate, } from './resolveSchemaCoordinate'; diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 085074affb8..dc819254ab0 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -1,5 +1,4 @@ import type { GraphQLSchema } from '../type/schema'; -import type { GraphQLSchemaElement } from '../type/element'; import type { SchemaCoordinateNode } from '../language/ast'; import type { Source } from '../language/source'; import { @@ -9,6 +8,53 @@ import { isInputObjectType, } from '../type/definition'; import { parseSchemaCoordinate } from '../language/parser'; +import type { + GraphQLNamedType, + GraphQLField, + GraphQLInputField, + GraphQLEnumValue, + GraphQLArgument, +} from '../type/definition'; +import type { GraphQLDirective } from '../type/directives'; + +/** + * A schema element may be one of the following kinds: + */ +export type GraphQLSchemaElement = + | { + 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; + }; /** * A schema coordinate is resolved in the context of a GraphQL schema to @@ -34,7 +80,7 @@ export function resolveASTSchemaCoordinate( schema: GraphQLSchema, schemaCoordinate: SchemaCoordinateNode, ): GraphQLSchemaElement | undefined { - const { isDirective, name, fieldName, argumentName } = schemaCoordinate; + const { isDirective, name, memberName, argumentName } = schemaCoordinate; if (isDirective) { // SchemaCoordinate : // - @ Name @@ -45,7 +91,10 @@ export function resolveASTSchemaCoordinate( if (!argumentName) { // SchemaCoordinate : @ Name // Return the directive in the {schema} named {directiveName}. - return directive ?? undefined; + if (!directive) { + return; + } + return { kind: 'Directive', directive }; } // SchemaCoordinate : @ Name ( Name : ) @@ -53,7 +102,15 @@ export function resolveASTSchemaCoordinate( if (!directive) { return; } - return directive.args.find((arg) => arg.name === argumentName.value); + // 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, + ); + if (!directiveArgument) { + return; + } + return { kind: 'DirectiveArgument', directive, directiveArgument }; } // SchemaCoordinate : @@ -63,10 +120,13 @@ export function resolveASTSchemaCoordinate( // 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 (!fieldName) { + if (!memberName) { // SchemaCoordinate : Name // Return the type in the {schema} named {typeName}. - return type ?? undefined; + if (!type) { + return; + } + return { kind: 'NamedType', type }; } if (!argumentName) { @@ -75,13 +135,21 @@ export function resolveASTSchemaCoordinate( if (isEnumType(type)) { // Let {enumValueName} be the value of the second {Name}. // Return the enum value of {type} named {enumValueName}. - return type.getValue(fieldName.value) ?? undefined; + const enumValue = type.getValue(memberName.value); + if (!enumValue) { + 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}. - return type.getFields()[fieldName.value]; + const inputField = type.getFields()[memberName.value]; + if (!inputField) { + return; + } + return { kind: 'InputField', type, inputField }; } // Otherwise: // Assert {type} must be an Object or Interface type. @@ -90,7 +158,11 @@ export function resolveASTSchemaCoordinate( } // Let {fieldName} be the value of the second {Name}. // Return the field of {type} named {fieldName}. - return type.getFields()[fieldName.value]; + const field = type.getFields()[memberName.value]; + if (!field) { + return; + } + return { kind: 'Field', type, field }; } // SchemaCoordinate : Name . Name ( Name : ) @@ -100,12 +172,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()[fieldName.value]; + const field = type.getFields()[memberName.value]; // Assert {field} must exist. if (!field) { return; } - // Let {argumentName} be the value of the third {Name}. - // Return the argument of {field} named {argumentName}. - return field.args.find((arg) => arg.name === argumentName.value); + // Let {fieldArgumentName} be the value of the third {Name}. + // Return the argument of {field} named {fieldArgumentName}. + const fieldArgument = field.args.find( + (arg) => arg.name === argumentName.value, + ); + if (!fieldArgument) { + return; + } + return { kind: 'FieldArgument', type, field, fieldArgument }; }