diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index 6a321931b9..ac92de1a30 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -683,7 +683,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" of required type "String!" was not provided.', + 'Argument "Query.withNonNullArg(cannotBeNull:)" of required type "String!" was not provided.', locations: [{ line: 3, column: 13 }], path: ['withNonNullArg'], }, @@ -710,7 +710,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" has invalid value: Expected value of non-null type "String!" not to be null.', + 'Argument "Query.withNonNullArg(cannotBeNull:)" has invalid value: Expected value of non-null type "String!" not to be null.', locations: [{ line: 3, column: 42 }], path: ['withNonNullArg'], }, @@ -740,7 +740,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" has invalid value: Expected variable "$testVar" provided to type "String!" to provide a runtime value.', + 'Argument "Query.withNonNullArg(cannotBeNull:)" has invalid value: Expected variable "$testVar" provided to type "String!" to provide a runtime value.', locations: [{ line: 3, column: 42 }], path: ['withNonNullArg'], }, @@ -768,7 +768,7 @@ describe('Execute: handles non-nullable types', () => { errors: [ { message: - 'Argument "cannotBeNull" has invalid value: Expected variable "$testVar" provided to non-null type "String!" not to be null.', + 'Argument "Query.withNonNullArg(cannotBeNull:)" has invalid value: Expected variable "$testVar" provided to non-null type "String!" not to be null.', locations: [{ line: 3, column: 43 }], path: ['withNonNullArg'], }, diff --git a/src/execution/__tests__/oneof-test.ts b/src/execution/__tests__/oneof-test.ts index eed65ae580..c010a21b72 100644 --- a/src/execution/__tests__/oneof-test.ts +++ b/src/execution/__tests__/oneof-test.ts @@ -88,7 +88,7 @@ describe('Execute: Handles OneOf Input Objects', () => { message: // This type of error would be caught at validation-time // hence the vague error message here. - 'Argument "input" has invalid value: Expected variable "$input" provided to type "TestInputObject!" to provide a runtime value.', + 'Argument "Query.test(input:)" has invalid value: Expected variable "$input" provided to type "TestInputObject!" to provide a runtime value.', path: ['test'], }, ], @@ -229,7 +229,7 @@ describe('Execute: Handles OneOf Input Objects', () => { // A nullable variable in a oneOf field position would be caught at validation-time // hence the vague error message here. message: - 'Argument "input" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" not to be null.', + 'Argument "Query.test(input:)" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" not to be null.', locations: [{ line: 3, column: 23 }], path: ['test'], }, @@ -257,7 +257,7 @@ describe('Execute: Handles OneOf Input Objects', () => { // A nullable variable in a oneOf field position would be caught at validation-time // hence the vague error message here. message: - 'Argument "input" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" to provide a runtime value.', + 'Argument "Query.test(input:)" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" to provide a runtime value.', locations: [{ line: 3, column: 23 }], path: ['test'], }, @@ -288,7 +288,7 @@ describe('Execute: Handles OneOf Input Objects', () => { // A nullable variable in a oneOf field position would be caught at validation-time // hence the vague error message here. message: - 'Argument "input" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" not to be null.', + 'Argument "Query.test(input:)" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" not to be null.', locations: [{ line: 6, column: 23 }], path: ['test'], }, @@ -319,7 +319,7 @@ describe('Execute: Handles OneOf Input Objects', () => { // A nullable variable in a oneOf field position would be caught at validation-time // hence the vague error message here. message: - 'Argument "input" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" to provide a runtime value.', + 'Argument "Query.test(input:)" has invalid value: Expected variable "$a" provided to field "a" for OneOf Input Object type "TestInputObject" to provide a runtime value.', locations: [{ line: 6, column: 23 }], path: ['test'], }, diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index ca729d0248..f6f1a0a0aa 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -284,7 +284,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" has invalid value: Expected value of type "TestInputObject" to be an object, found: ["foo", "bar", "baz"].', + 'Argument "TestType.fieldWithObjectInput(input:)" has invalid value: Expected value of type "TestInputObject" to be an object, found: ["foo", "bar", "baz"].', path: ['fieldWithObjectInput'], locations: [{ line: 3, column: 41 }], }, @@ -320,7 +320,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" has invalid value at .e: FaultyScalarErrorMessage', + 'Argument "TestType.fieldWithObjectInput(input:)" has invalid value at .e: FaultyScalarErrorMessage', path: ['fieldWithObjectInput'], locations: [{ line: 3, column: 13 }], extensions: { code: 'FaultyScalarErrorExtensionCode' }, @@ -477,7 +477,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Variable "$input" has invalid value at .e: Argument "input" has invalid value at .e: FaultyScalarErrorMessage', + 'Variable "$input" has invalid value at .e: Argument "TestType.fieldWithObjectInput(input:)" has invalid value at .e: FaultyScalarErrorMessage', locations: [{ line: 2, column: 16 }], extensions: { code: 'FaultyScalarErrorExtensionCode' }, }, @@ -802,7 +802,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" of required type "String!" was not provided.', + 'Argument "TestType.fieldWithNonNullableStringInput(input:)" of required type "String!" was not provided.', locations: [{ line: 1, column: 3 }], path: ['fieldWithNonNullableStringInput'], }, @@ -850,7 +850,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" has invalid value: Expected variable "$foo" provided to type "String!" to provide a runtime value.', + 'Argument "TestType.fieldWithNonNullableStringInput(input:)" has invalid value: Expected variable "$foo" provided to type "String!" to provide a runtime value.', locations: [{ line: 3, column: 50 }], path: ['fieldWithNonNullableStringInput'], }, @@ -1102,7 +1102,7 @@ describe('Execute: Handles inputs', () => { errors: [ { message: - 'Argument "input" has invalid value: String cannot represent a non string value: WRONG_TYPE', + 'Argument "TestType.fieldWithDefaultArgumentValue(input:)" has invalid value: String cannot represent a non string value: WRONG_TYPE', locations: [{ line: 3, column: 48 }], path: ['fieldWithDefaultArgumentValue'], }, diff --git a/src/execution/values.ts b/src/execution/values.ts index ed869e10bd..560948cc8a 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -13,8 +13,12 @@ import type { } from '../language/ast.js'; import { Kind } from '../language/kinds.js'; -import type { GraphQLArgument, GraphQLField } from '../type/definition.js'; -import { isNonNullType, isRequiredArgument } from '../type/definition.js'; +import type { GraphQLField } from '../type/definition.js'; +import { + GraphQLArgument, + isNonNullType, + isRequiredArgument, +} from '../type/definition.js'; import type { GraphQLDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; @@ -222,7 +226,7 @@ export function experimentalGetArgumentValues( // execution. This is a runtime check to ensure execution does not // continue with an invalid argument value. throw new GraphQLError( - `Argument "${argDef.name}" of required type "${argType}" was not provided.`, + `Argument "${argDef instanceof GraphQLArgument ? argDef : argDef.name}" of required type "${argType}" was not provided.`, { nodes: node }, ); } @@ -272,7 +276,7 @@ export function experimentalGetArgumentValues( valueNode, argType, (error, path) => { - error.message = `Argument "${argDef.name}" has invalid value${printPathArray( + error.message = `Argument "${argDef instanceof GraphQLArgument ? argDef : argDef.name}" has invalid value${printPathArray( path, )}: ${error.message}`; throw error; diff --git a/src/index.ts b/src/index.ts index aef9d75b16..52ddb6440b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,12 +40,17 @@ export { // Definitions GraphQLSchema, GraphQLDirective, + GraphQLSchemaElement, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, + GraphQLField, + GraphQLArgument, + GraphQLEnumValue, + GraphQLInputField, GraphQLList, GraphQLNonNull, // Standard GraphQL Scalars @@ -161,23 +166,19 @@ export type { GraphQLSchemaExtensions, GraphQLDirectiveConfig, GraphQLDirectiveExtensions, - GraphQLArgument, GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, - GraphQLEnumValue, GraphQLEnumValueConfig, GraphQLEnumValueConfigMap, GraphQLEnumValueExtensions, - GraphQLField, GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldExtensions, GraphQLFieldMap, GraphQLFieldResolver, - GraphQLInputField, GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, GraphQLInputFieldExtensions, @@ -223,6 +224,7 @@ export { parseValue, parseConstValue, parseType, + parseSchemaCoordinate, // Print print, // Visit @@ -243,6 +245,7 @@ export { isTypeDefinitionNode, isTypeSystemExtensionNode, isTypeExtensionNode, + isSchemaCoordinateNode, } from './language/index.js'; export type { @@ -315,6 +318,7 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + SchemaCoordinateNode, } from './language/index.js'; // Execute GraphQL queries. @@ -482,6 +486,8 @@ export { findBreakingChanges, findDangerousChanges, findSchemaChanges, + resolveSchemaCoordinate, + resolveASTSchemaCoordinate, } from './utilities/index.js'; export type { @@ -512,4 +518,5 @@ export type { SafeChange, DangerousChange, TypedQueryDocumentNode, + ResolvedSchemaElement, } from './utilities/index.js'; diff --git a/src/language/__tests__/lexer-test.ts b/src/language/__tests__/lexer-test.ts index 85603dfaaa..14357eadbe 100644 --- a/src/language/__tests__/lexer-test.ts +++ b/src/language/__tests__/lexer-test.ts @@ -165,13 +165,6 @@ describe('Lexer', () => { }); }); - it('reports unexpected characters', () => { - expectSyntaxError('.').to.deep.equal({ - message: 'Syntax Error: Unexpected character: ".".', - locations: [{ line: 1, column: 1 }], - }); - }); - it('errors respect whitespace', () => { let caughtError; try { @@ -965,6 +958,13 @@ describe('Lexer', () => { value: undefined, }); + expect(lexOne('.')).to.contain({ + kind: TokenKind.DOT, + start: 0, + end: 1, + value: undefined, + }); + expect(lexOne('...')).to.contain({ kind: TokenKind.SPREAD, start: 0, diff --git a/src/language/__tests__/parser-test.ts b/src/language/__tests__/parser-test.ts index d98b6a6f41..786eab3324 100644 --- a/src/language/__tests__/parser-test.ts +++ b/src/language/__tests__/parser-test.ts @@ -11,7 +11,13 @@ import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js'; import { inspect } from '../../jsutils/inspect.js'; import { Kind } from '../kinds.js'; -import { parse, parseConstValue, parseType, parseValue } from '../parser.js'; +import { + parse, + parseConstValue, + parseSchemaCoordinate, + parseType, + parseValue, +} from '../parser.js'; import { Source } from '../source.js'; import { TokenKind } from '../tokenKind.js'; @@ -679,4 +685,129 @@ describe('Parser', () => { }); }); }); + + describe('parseSchemaCoordinate', () => { + it('parses Name', () => { + const result = parseSchemaCoordinate('MyType'); + expectJSON(result).toDeepEqual({ + kind: Kind.SCHEMA_COORDINATE, + loc: { start: 0, end: 6 }, + ofDirective: false, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', + }, + memberName: undefined, + argumentName: undefined, + }); + }); + + it('parses Name . Name', () => { + const result = parseSchemaCoordinate('MyType.field'); + expectJSON(result).toDeepEqual({ + kind: Kind.SCHEMA_COORDINATE, + loc: { start: 0, end: 12 }, + ofDirective: false, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', + }, + memberName: { + kind: Kind.NAME, + loc: { start: 7, end: 12 }, + value: 'field', + }, + argumentName: undefined, + }); + }); + + it('rejects Name . Name . Name', () => { + expectToThrowJSON(() => + parseSchemaCoordinate('MyType.field.deep'), + ).to.deep.equal({ + message: 'Syntax Error: Expected , found ".".', + locations: [{ line: 1, column: 13 }], + }); + }); + + it('parses Name . Name ( Name : )', () => { + const result = parseSchemaCoordinate('MyType.field(arg:)'); + expectJSON(result).toDeepEqual({ + kind: Kind.SCHEMA_COORDINATE, + loc: { start: 0, end: 18 }, + ofDirective: false, + name: { + kind: Kind.NAME, + loc: { start: 0, end: 6 }, + value: 'MyType', + }, + memberName: { + kind: Kind.NAME, + loc: { start: 7, end: 12 }, + value: 'field', + }, + argumentName: { + kind: Kind.NAME, + loc: { start: 13, end: 16 }, + value: 'arg', + }, + }); + }); + + it('rejects Name . Name ( Name : Name )', () => { + expectToThrowJSON(() => + parseSchemaCoordinate('MyType.field(arg: value)'), + ).to.deep.equal({ + message: 'Syntax Error: Expected ")", found Name "value".', + locations: [{ line: 1, column: 19 }], + }); + }); + + it('parses @ Name', () => { + const result = parseSchemaCoordinate('@myDirective'); + expectJSON(result).toDeepEqual({ + kind: Kind.SCHEMA_COORDINATE, + loc: { start: 0, end: 12 }, + ofDirective: true, + name: { + kind: Kind.NAME, + loc: { start: 1, end: 12 }, + value: 'myDirective', + }, + memberName: undefined, + argumentName: undefined, + }); + }); + + it('parses @ Name ( Name : )', () => { + const result = parseSchemaCoordinate('@myDirective(arg:)'); + expectJSON(result).toDeepEqual({ + kind: Kind.SCHEMA_COORDINATE, + loc: { start: 0, end: 18 }, + ofDirective: true, + name: { + kind: Kind.NAME, + loc: { start: 1, end: 12 }, + value: 'myDirective', + }, + memberName: undefined, + argumentName: { + kind: Kind.NAME, + loc: { start: 13, end: 16 }, + value: 'arg', + }, + }); + }); + + it('rejects @ Name . Name', () => { + expectToThrowJSON(() => + parseSchemaCoordinate('@myDirective.field'), + ).to.deep.equal({ + message: 'Syntax Error: Expected , found ".".', + locations: [{ line: 1, column: 13 }], + }); + }); + }); }); diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 350e3f1a6b..b15d5f7319 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -8,6 +8,7 @@ import { isConstValueNode, isDefinitionNode, isExecutableDefinitionNode, + isSchemaCoordinateNode, isSelectionNode, isTypeDefinitionNode, isTypeExtensionNode, @@ -141,4 +142,10 @@ describe('AST node predicates', () => { 'InputObjectTypeExtension', ]); }); + + it('isSchemaCoordinateNode', () => { + expect(filterNodes(isSchemaCoordinateNode)).to.deep.equal([ + 'SchemaCoordinate', + ]); + }); }); diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 624dc75ca2..589d9bfc8d 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -5,7 +5,7 @@ import { dedent, dedentString } from '../../__testUtils__/dedent.js'; import { kitchenSinkQuery } from '../../__testUtils__/kitchenSinkQuery.js'; import { Kind } from '../kinds.js'; -import { parse } from '../parser.js'; +import { parse, parseSchemaCoordinate } from '../parser.js'; import { print } from '../printer.js'; describe('Printer: Query document', () => { @@ -299,4 +299,18 @@ describe('Printer: Query document', () => { `), ); }); + + it('prints schema coordinates', () => { + expect(print(parseSchemaCoordinate(' Name '))).to.equal('Name'); + expect(print(parseSchemaCoordinate(' Name . field '))).to.equal( + 'Name.field', + ); + expect(print(parseSchemaCoordinate(' Name . field ( arg: )'))).to.equal( + 'Name.field(arg:)', + ); + expect(print(parseSchemaCoordinate(' @ name '))).to.equal('@name'); + expect(print(parseSchemaCoordinate(' @ name (arg:) '))).to.equal( + '@name(arg:)', + ); + }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index bce69a6e19..b76e082f48 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -181,7 +181,8 @@ export type ASTNode = | InterfaceTypeExtensionNode | UnionTypeExtensionNode | EnumTypeExtensionNode - | InputObjectTypeExtensionNode; + | InputObjectTypeExtensionNode + | SchemaCoordinateNode; /** * Utility type listing all nodes indexed by their kind. @@ -287,6 +288,8 @@ export const QueryDocumentKeys: { UnionTypeExtension: ['name', 'directives', 'types'], EnumTypeExtension: ['name', 'directives', 'values'], InputObjectTypeExtension: ['name', 'directives', 'fields'], + + SchemaCoordinate: ['name', 'memberName', 'argumentName'], }; const kindValues = new Set(Object.keys(QueryDocumentKeys)); @@ -762,3 +765,14 @@ export interface InputObjectTypeExtensionNode { readonly directives?: ReadonlyArray | undefined; readonly fields?: ReadonlyArray | undefined; } + +/** Schema Coordinates */ + +export interface SchemaCoordinateNode { + readonly kind: typeof Kind.SCHEMA_COORDINATE; + readonly loc?: Location | undefined; + readonly ofDirective: boolean; + readonly name: NameNode; + readonly memberName?: NameNode | undefined; + readonly argumentName?: NameNode | undefined; +} diff --git a/src/language/index.ts b/src/language/index.ts index 4d96766abc..a0d8a02cc5 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -11,7 +11,13 @@ export { TokenKind } from './tokenKind.js'; export { Lexer } from './lexer.js'; -export { parse, parseValue, parseConstValue, parseType } from './parser.js'; +export { + parse, + parseValue, + parseConstValue, + parseType, + parseSchemaCoordinate, +} from './parser.js'; export type { ParseOptions } from './parser.js'; export { print } from './printer.js'; @@ -88,6 +94,7 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + SchemaCoordinateNode, } from './ast.js'; export { @@ -101,6 +108,7 @@ export { isTypeDefinitionNode, isTypeSystemExtensionNode, isTypeExtensionNode, + isSchemaCoordinateNode, } from './predicates.js'; export { DirectiveLocation } from './directiveLocation.js'; diff --git a/src/language/kinds.ts b/src/language/kinds.ts index d41eccecfc..fabc5642e5 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -67,6 +67,9 @@ export const Kind = { UNION_TYPE_EXTENSION: 'UnionTypeExtension' as const, ENUM_TYPE_EXTENSION: 'EnumTypeExtension' as const, INPUT_OBJECT_TYPE_EXTENSION: 'InputObjectTypeExtension' as const, + + /** Schema Coordinates */ + SCHEMA_COORDINATE: 'SchemaCoordinate' as const, }; // eslint-disable-next-line @typescript-eslint/no-redeclare export type Kind = (typeof Kind)[keyof typeof Kind]; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index 300aeebbdf..7ce6ec0c36 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -95,6 +95,7 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean { kind === TokenKind.AMP || kind === TokenKind.PAREN_L || kind === TokenKind.PAREN_R || + kind === TokenKind.DOT || kind === TokenKind.SPREAD || kind === TokenKind.COLON || kind === TokenKind.EQUALS || @@ -246,7 +247,11 @@ function readNextToken(lexer: Lexer, start: number): Token { // - FloatValue // - StringValue // - // Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | } + // Punctuator :: + // - DotPunctuator + // - OtherPunctuator + // + // OtherPunctuator :: one of ! $ & ( ) ... : = @ [ ] { | } case 0x0021: // ! return createToken(lexer, TokenKind.BANG, position, position + 1); case 0x0024: // $ @@ -263,24 +268,7 @@ function readNextToken(lexer: Lexer, start: number): Token { if (nextCode === 0x002e && body.charCodeAt(position + 2) === 0x002e) { return createToken(lexer, TokenKind.SPREAD, position, position + 3); } - if (nextCode === 0x002e) { - throw syntaxError( - lexer.source, - position, - 'Unexpected "..", did you mean "..."?', - ); - } else if (isDigit(nextCode)) { - const digits = lexer.source.body.slice( - position + 1, - readDigits(lexer, position + 1, nextCode), - ); - throw syntaxError( - lexer.source, - position, - `Invalid number, expected digit before ".", did you mean "0.${digits}"?`, - ); - } - break; + return readDot(lexer, position); } case 0x003a: // : return createToken(lexer, TokenKind.COLON, position, position + 1); @@ -333,6 +321,37 @@ function readNextToken(lexer: Lexer, start: number): Token { return createToken(lexer, TokenKind.EOF, bodyLength, bodyLength); } +/** + * Reads a dot token with helpful messages for negative lookahead. + * + * ``` + * DotPunctuator :: `.` [lookahead != {`.`, Digit}] + * ``` + */ +function readDot(lexer: Lexer, start: number): Token { + const nextCode = lexer.source.body.charCodeAt(start + 1); + // Full Stop (.) + if (nextCode === 0x002e) { + throw syntaxError( + lexer.source, + start, + 'Unexpected "..", did you mean "..."?', + ); + } + if (isDigit(nextCode)) { + const digits = lexer.source.body.slice( + start + 1, + readDigits(lexer, start + 1, nextCode), + ); + throw syntaxError( + lexer.source, + start, + `Invalid number, expected digit before ".", did you mean "0.${digits}"?`, + ); + } + return createToken(lexer, TokenKind.DOT, start, start + 1); +} + /** * Reads a comment token from the source file. * diff --git a/src/language/parser.ts b/src/language/parser.ts index 3d2018ba4b..4684b339fb 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -47,6 +47,7 @@ import type { OperationTypeDefinitionNode, ScalarTypeDefinitionNode, ScalarTypeExtensionNode, + SchemaCoordinateNode, SchemaDefinitionNode, SchemaExtensionNode, SelectionNode, @@ -182,6 +183,26 @@ export function parseType( return type; } +/** + * Given a string containing a GraphQL Schema Coordinate (ex. `Type.field`), + * parse the AST for that schema coordinate. + * Throws GraphQLError if a syntax error is encountered. + * + * Consider providing the results to the utility function: + * resolveASTSchemaCoordinate(). Or calling resolveSchemaCoordinate() directly + * with an unparsed source. + */ +export function parseSchemaCoordinate( + source: string | Source, + options?: ParseOptions, +): SchemaCoordinateNode { + const parser = new Parser(source, options); + parser.expectToken(TokenKind.SOF); + const type = parser.parseSchemaCoordinate(); + parser.expectToken(TokenKind.EOF); + return type; +} + /** * This class is exported only to assist people in implementing their own parsers * without duplicating too much code and should be used only as last resort for cases @@ -1433,6 +1454,44 @@ export class Parser { throw this.unexpected(start); } + // Schema Coordinates + + /** + * ``` + * SchemaCoordinate : + * - Name + * - Name . Name + * - Name . Name ( Name : ) + * - @ Name + * - @ Name ( Name : ) + * ``` + */ + parseSchemaCoordinate(): SchemaCoordinateNode { + const start = this._lexer.token; + const ofDirective = this.expectOptionalToken(TokenKind.AT); + const name = this.parseName(); + let memberName; + if (!ofDirective && this.expectOptionalToken(TokenKind.DOT)) { + memberName = this.parseName(); + } + let argumentName; + if ( + (ofDirective || memberName) && + this.expectOptionalToken(TokenKind.PAREN_L) + ) { + argumentName = this.parseName(); + this.expectToken(TokenKind.COLON); + this.expectToken(TokenKind.PAREN_R); + } + return this.node(start, { + kind: Kind.SCHEMA_COORDINATE, + ofDirective, + name, + memberName, + argumentName, + }); + } + // Core parsing utility functions /** diff --git a/src/language/predicates.ts b/src/language/predicates.ts index 29ad5bf289..fa5923b90d 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -3,6 +3,7 @@ import type { ConstValueNode, DefinitionNode, ExecutableDefinitionNode, + SchemaCoordinateNode, SelectionNode, TypeDefinitionNode, TypeExtensionNode, @@ -110,3 +111,9 @@ export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode { node.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION ); } + +export function isSchemaCoordinateNode( + node: ASTNode, +): node is SchemaCoordinateNode { + return node.kind === Kind.SCHEMA_COORDINATE; +} diff --git a/src/language/printer.ts b/src/language/printer.ts index bf2959e59c..126701a011 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -320,6 +320,18 @@ const printDocASTReducer: ASTReducer = { leave: ({ name, directives, fields }) => join(['extend input', name, join(directives, ' '), block(fields)], ' '), }, + + // Schema Coordinate + + SchemaCoordinate: { + leave: ({ ofDirective, name, memberName, argumentName }) => + join([ + ofDirective ? '@' : undefined, + name, + wrap('.', memberName), + wrap('(', argumentName, ':)'), + ]), + }, }; /** diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index d1c7129b04..eae0972b81 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -10,6 +10,7 @@ export const TokenKind = { AMP: '&' as const, PAREN_L: '(' as const, PAREN_R: ')' as const, + DOT: '.' as const, SPREAD: '...' as const, COLON: ':' as const, EQUALS: '=' as const, diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index ad35af937f..a95a7f9250 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -229,11 +229,11 @@ describe('Type System: Objects', () => { }, }; const testObject1 = new GraphQLObjectType({ - name: 'Test1', + name: 'Test', fields: outputFields, }); const testObject2 = new GraphQLObjectType({ - name: 'Test2', + name: 'Test', fields: outputFields, }); @@ -255,11 +255,11 @@ describe('Type System: Objects', () => { field2: { type: ScalarType }, }; const testInputObject1 = new GraphQLInputObjectType({ - name: 'Test1', + name: 'Test', fields: inputFields, }); const testInputObject2 = new GraphQLInputObjectType({ - name: 'Test2', + name: 'Test', fields: inputFields, }); @@ -305,18 +305,17 @@ describe('Type System: Objects', () => { f: { type: ScalarType }, }), }); - expect(objType.getFields()).to.deep.equal({ - f: { - name: 'f', - description: undefined, - type: ScalarType, - args: [], - resolve: undefined, - subscribe: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, + expect(objType.getFields().f).to.deep.include({ + coordinate: 'SomeObject.f', + name: 'f', + description: undefined, + type: ScalarType, + args: [], + resolve: undefined, + subscribe: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -332,28 +331,32 @@ describe('Type System: Objects', () => { }, }, }); - expect(objType.getFields()).to.deep.equal({ - f: { - name: 'f', - description: undefined, - type: ScalarType, - args: [ - { - name: 'arg', - description: undefined, - type: ScalarType, - defaultValue: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - ], - resolve: undefined, - subscribe: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, + + const f = objType.getFields().f; + + expect(f).to.deep.include({ + coordinate: 'SomeObject.f', + name: 'f', + description: undefined, + type: ScalarType, + resolve: undefined, + subscribe: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(f.args).to.have.lengthOf(1); + + expect(f.args[0]).to.deep.include({ + coordinate: 'SomeObject.f(arg:)', + name: 'arg', + description: undefined, + type: ScalarType, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -506,18 +509,16 @@ describe('Type System: Interfaces', () => { f: { type: ScalarType }, }), }); - expect(interfaceType.getFields()).to.deep.equal({ - f: { - name: 'f', - description: undefined, - type: ScalarType, - args: [], - resolve: undefined, - subscribe: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, + expect(interfaceType.getFields().f).to.deep.include({ + name: 'f', + description: undefined, + type: ScalarType, + args: [], + resolve: undefined, + subscribe: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -706,32 +707,39 @@ describe('Type System: Enums', () => { }, }); - expect(EnumTypeWithNullishValue.getValues()).to.deep.equal([ - { - name: 'NULL', - description: undefined, - value: null, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - { - name: 'NAN', - description: undefined, - value: NaN, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - { - name: 'NO_CUSTOM_VALUE', - description: undefined, - value: 'NO_CUSTOM_VALUE', - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - ]); + const values = EnumTypeWithNullishValue.getValues(); + + expect(values).to.have.lengthOf(3); + + expect(values[0]).to.deep.include({ + coordinate: 'EnumWithNullishValue.NULL', + name: 'NULL', + description: undefined, + value: null, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + coordinate: 'EnumWithNullishValue.NAN', + name: 'NAN', + description: undefined, + value: NaN, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[2]).to.deep.include({ + coordinate: 'EnumWithNullishValue.NO_CUSTOM_VALUE', + name: 'NO_CUSTOM_VALUE', + description: undefined, + value: 'NO_CUSTOM_VALUE', + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); }); it('accepts a well defined Enum type with empty value definition', () => { @@ -826,16 +834,15 @@ describe('Type System: Input Objects', () => { f: { type: ScalarType }, }, }); - expect(inputObjType.getFields()).to.deep.equal({ - f: { - name: 'f', - description: undefined, - type: ScalarType, - defaultValue: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, + expect(inputObjType.getFields().f).to.deep.include({ + coordinate: 'SomeInputObject.f', + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -846,16 +853,15 @@ describe('Type System: Input Objects', () => { f: { type: ScalarType }, }), }); - expect(inputObjType.getFields()).to.deep.equal({ - f: { - name: 'f', - description: undefined, - type: ScalarType, - defaultValue: undefined, - extensions: {}, - deprecationReason: undefined, - astNode: undefined, - }, + expect(inputObjType.getFields().f).to.deep.include({ + coordinate: 'SomeInputObject.f', + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: undefined, + extensions: {}, + deprecationReason: undefined, + astNode: undefined, }); }); diff --git a/src/type/__tests__/directive-test.ts b/src/type/__tests__/directive-test.ts index 03af5e1fd9..9b9ca31695 100644 --- a/src/type/__tests__/directive-test.ts +++ b/src/type/__tests__/directive-test.ts @@ -33,29 +33,33 @@ describe('Type System: Directive', () => { expect(directive).to.deep.include({ name: 'Foo', - args: [ - { - name: 'foo', - description: undefined, - type: GraphQLString, - defaultValue: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - { - name: 'bar', - description: undefined, - type: GraphQLInt, - defaultValue: undefined, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - ], isRepeatable: false, locations: ['QUERY'], }); + + expect(directive.args).to.have.lengthOf(2); + + expect(directive.args[0]).to.deep.include({ + coordinate: '@Foo(foo:)', + name: 'foo', + description: undefined, + type: GraphQLString, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(directive.args[1]).to.deep.include({ + coordinate: '@Foo(bar:)', + name: 'bar', + description: undefined, + type: GraphQLInt, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); }); it('defines a repeatable directive', () => { diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index e4f7219908..004140d728 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -381,24 +381,28 @@ describe('Type System: Enum Values', () => { it('presents a getValues() API for complex enums', () => { const values = ComplexEnum.getValues(); - expect(values).to.have.deep.ordered.members([ - { - name: 'ONE', - description: undefined, - value: Complex1, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - { - name: 'TWO', - description: undefined, - value: Complex2, - deprecationReason: undefined, - extensions: {}, - astNode: undefined, - }, - ]); + + expect(values).to.have.lengthOf(2); + + expect(values[0]).to.deep.include({ + coordinate: 'Complex.ONE', + name: 'ONE', + description: undefined, + value: Complex1, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + coordinate: 'Complex.TWO', + name: 'TWO', + description: undefined, + value: Complex2, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); }); it('presents a getValue() API for complex enums', () => { diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index a71ce8c012..5bd680d767 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -3,11 +3,7 @@ import { describe, it } from 'mocha'; import { DirectiveLocation } from '../../language/directiveLocation.js'; -import type { - GraphQLArgument, - GraphQLInputField, - GraphQLInputType, -} from '../definition.js'; +import type { GraphQLInputType } from '../definition.js'; import { assertAbstractType, assertCompositeType, @@ -28,7 +24,9 @@ import { assertWrappingType, getNamedType, getNullableType, + GraphQLArgument, GraphQLEnumType, + GraphQLInputField, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, @@ -570,18 +568,7 @@ describe('Type predicates', () => { type: GraphQLInputType; defaultValue?: unknown; }): GraphQLArgument { - return { - name: 'someArg', - type: config.type, - description: undefined, - defaultValue: - config.defaultValue !== undefined - ? { value: config.defaultValue } - : undefined, - deprecationReason: null, - extensions: Object.create(null), - astNode: undefined, - }; + return new GraphQLArgument('SomeType.someField', 'someArg', config); } it('returns true for required arguments', () => { @@ -621,18 +608,7 @@ describe('Type predicates', () => { type: GraphQLInputType; defaultValue?: unknown; }): GraphQLInputField { - return { - name: 'someInputField', - type: config.type, - description: undefined, - defaultValue: - config.defaultValue !== undefined - ? { value: config.defaultValue } - : undefined, - deprecationReason: null, - extensions: Object.create(null), - astNode: undefined, - }; + return new GraphQLInputField('SomeType', 'someInputField', config); } it('returns true for required input field', () => { diff --git a/src/type/definition.ts b/src/type/definition.ts index 8d51201070..3911bd2794 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -485,6 +485,31 @@ export function getNamedType( } } +/** + * The base class for all Schema Elements. + */ +export class GraphQLSchemaElement { + readonly coordinate: string; + + constructor(coordinate: string) { + this.coordinate = coordinate; + } + + // TODO: add coverage + /* c8 ignore next 3*/ + get [Symbol.toStringTag]() { + return 'GraphQLSchemaElement'; + } + + toString(): string { + return this.coordinate; + } + + toJSON(): string { + return this.toString(); + } +} + /** * Used while defining GraphQL types to allow for circular references in * otherwise immutable type definitions. @@ -590,7 +615,10 @@ export interface GraphQLScalarTypeExtensions { * `coerceInputLiteral()` method. * */ -export class GraphQLScalarType { +export class GraphQLScalarType< + TInternal = unknown, + TExternal = TInternal, +> extends GraphQLSchemaElement { name: string; description: Maybe; specifiedByURL: Maybe; @@ -609,6 +637,8 @@ export class GraphQLScalarType { extensionASTNodes: ReadonlyArray; constructor(config: Readonly>) { + super(config.name); + this.name = assertName(config.name); this.description = config.description; this.specifiedByURL = config.specifiedByURL; @@ -649,7 +679,7 @@ export class GraphQLScalarType { } } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLScalarType'; } @@ -670,14 +700,6 @@ export class GraphQLScalarType { extensionASTNodes: this.extensionASTNodes, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } /* @deprecated in favor of GraphQLScalarOutputValueCoercer, will be removed in v18 */ @@ -806,7 +828,10 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { * }); * ``` */ -export class GraphQLObjectType { +export class GraphQLObjectType< + TSource = any, + TContext = any, +> extends GraphQLSchemaElement { name: string; description: Maybe; isTypeOf: Maybe>; @@ -818,20 +843,23 @@ export class GraphQLObjectType { private _interfaces: ThunkReadonlyArray; constructor(config: Readonly>) { + super(config.name); this.name = assertName(config.name); this.description = config.description; this.isTypeOf = config.isTypeOf; this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; + this._fields = (defineFieldMap).bind( undefined, + this.name, config.fields, ); this._interfaces = defineInterfaces.bind(undefined, config.interfaces); } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLObjectType'; } @@ -854,21 +882,13 @@ export class GraphQLObjectType { name: this.name, description: this.description, interfaces: this.getInterfaces(), - fields: fieldsToFieldsConfig(this.getFields()), + fields: mapValue(this.getFields(), (field) => field.toConfig()), isTypeOf: this.isTypeOf, extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } function defineInterfaces( @@ -878,73 +898,15 @@ function defineInterfaces( } function defineFieldMap( + parentCoordinate: string, fields: ThunkObjMap>, ): GraphQLFieldMap { const fieldMap = resolveObjMapThunk(fields); - return mapValue(fieldMap, (fieldConfig, fieldName) => { - const argsConfig = fieldConfig.args ?? {}; - return { - name: assertName(fieldName), - description: fieldConfig.description, - type: fieldConfig.type, - args: defineArguments(argsConfig), - resolve: fieldConfig.resolve, - subscribe: fieldConfig.subscribe, - deprecationReason: fieldConfig.deprecationReason, - extensions: toObjMapWithSymbols(fieldConfig.extensions), - astNode: fieldConfig.astNode, - }; - }); -} - -export function defineArguments( - args: GraphQLFieldConfigArgumentMap, -): ReadonlyArray { - return Object.entries(args).map(([argName, argConfig]) => ({ - name: assertName(argName), - description: argConfig.description, - type: argConfig.type, - defaultValue: defineDefaultValue(argName, argConfig), - deprecationReason: argConfig.deprecationReason, - extensions: toObjMapWithSymbols(argConfig.extensions), - astNode: argConfig.astNode, - })); -} - -function fieldsToFieldsConfig( - fields: GraphQLFieldMap, -): GraphQLFieldConfigMap { - return mapValue(fields, (field) => ({ - description: field.description, - type: field.type, - args: argsToArgsConfig(field.args), - resolve: field.resolve, - subscribe: field.subscribe, - deprecationReason: field.deprecationReason, - extensions: field.extensions, - astNode: field.astNode, - })); -} - -/** - * @internal - */ -export function argsToArgsConfig( - args: ReadonlyArray, -): GraphQLFieldConfigArgumentMap { - return keyValMap( - args, - (arg) => arg.name, - (arg) => ({ - description: arg.description, - type: arg.type, - defaultValue: arg.defaultValue?.value, - defaultValueLiteral: arg.defaultValue?.literal, - deprecationReason: arg.deprecationReason, - extensions: arg.extensions, - astNode: arg.astNode, - }), + return mapValue( + fieldMap, + (fieldConfig, fieldName) => + new GraphQLField(parentCoordinate, fieldName, fieldConfig), ); } @@ -1064,7 +1026,11 @@ export type GraphQLFieldConfigMap = ObjMap< GraphQLFieldConfig >; -export interface GraphQLField { +export class GraphQLField< + TSource, + TContext, + TArgs = { [argument: string]: any }, +> extends GraphQLSchemaElement { name: string; description: Maybe; type: GraphQLOutputType; @@ -1074,9 +1040,56 @@ export interface GraphQLField { deprecationReason: Maybe; extensions: Readonly>; astNode: Maybe; + + constructor( + parentCoordinate: string, + name: string, + config: GraphQLFieldConfig, + ) { + const coordinate = `${parentCoordinate}.${name}`; + super(coordinate); + this.name = assertName(name); + this.description = config.description; + this.type = config.type; + + const argsConfig = config.args; + this.args = argsConfig + ? Object.entries(argsConfig).map( + ([argName, argConfig]) => + new GraphQLArgument(coordinate, argName, argConfig), + ) + : []; + + this.resolve = config.resolve; + this.subscribe = config.subscribe; + this.deprecationReason = config.deprecationReason; + this.extensions = toObjMapWithSymbols(config.extensions); + this.astNode = config.astNode; + } + + override get [Symbol.toStringTag]() { + return 'GraphQLField'; + } + + toConfig(): GraphQLFieldConfig { + return { + description: this.description, + type: this.type, + args: keyValMap( + this.args, + (arg) => arg.name, + (arg) => arg.toConfig(), + ), + resolve: this.resolve, + subscribe: this.subscribe, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } } -export interface GraphQLArgument { +export class GraphQLArgument extends GraphQLSchemaElement { name: string; description: Maybe; type: GraphQLInputType; @@ -1084,6 +1097,38 @@ export interface GraphQLArgument { deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parentCoordinate: string, + name: string, + config: GraphQLArgumentConfig, + ) { + const coordinate = `${parentCoordinate}(${name}:)`; + super(coordinate); + this.name = assertName(name); + this.description = config.description; + this.type = config.type; + this.defaultValue = defineDefaultValue(name, config); + this.deprecationReason = config.deprecationReason; + this.extensions = toObjMapWithSymbols(config.extensions); + this.astNode = config.astNode; + } + + override get [Symbol.toStringTag]() { + return 'GraphQLArgument'; + } + + toConfig(): GraphQLArgumentConfig { + return { + description: this.description, + type: this.type, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } } export function isRequiredArgument( @@ -1148,7 +1193,10 @@ export interface GraphQLInterfaceTypeExtensions { * }); * ``` */ -export class GraphQLInterfaceType { +export class GraphQLInterfaceType< + TSource = any, + TContext = any, +> extends GraphQLSchemaElement { name: string; description: Maybe; resolveType: Maybe>; @@ -1160,6 +1208,7 @@ export class GraphQLInterfaceType { private _interfaces: ThunkReadonlyArray; constructor(config: Readonly>) { + super(config.name); this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; @@ -1168,12 +1217,13 @@ export class GraphQLInterfaceType { this.extensionASTNodes = config.extensionASTNodes ?? []; this._fields = (defineFieldMap).bind( undefined, + this.name, config.fields, ); this._interfaces = defineInterfaces.bind(undefined, config.interfaces); } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLInterfaceType'; } @@ -1196,21 +1246,13 @@ export class GraphQLInterfaceType { name: this.name, description: this.description, interfaces: this.getInterfaces(), - fields: fieldsToFieldsConfig(this.getFields()), + fields: mapValue(this.getFields(), (field) => field.toConfig()), resolveType: this.resolveType, extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } export interface GraphQLInterfaceTypeConfig { @@ -1274,7 +1316,7 @@ export interface GraphQLUnionTypeExtensions { * }); * ``` */ -export class GraphQLUnionType { +export class GraphQLUnionType extends GraphQLSchemaElement { name: string; description: Maybe; resolveType: Maybe>; @@ -1285,6 +1327,7 @@ export class GraphQLUnionType { private _types: ThunkReadonlyArray; constructor(config: Readonly>) { + super(config.name); this.name = assertName(config.name); this.description = config.description; this.resolveType = config.resolveType; @@ -1295,7 +1338,7 @@ export class GraphQLUnionType { this._types = defineTypes.bind(undefined, config.types); } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLUnionType'; } @@ -1317,14 +1360,6 @@ export class GraphQLUnionType { extensionASTNodes: this.extensionASTNodes, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } function defineTypes( @@ -1368,17 +1403,6 @@ export interface GraphQLEnumTypeExtensions { [attributeName: string | symbol]: unknown; } -function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) { - return Object.entries(values).map(([valueName, valueConfig]) => ({ - name: assertEnumValueName(valueName), - description: valueConfig.description, - value: valueConfig.value !== undefined ? valueConfig.value : valueName, - deprecationReason: valueConfig.deprecationReason, - extensions: toObjMapWithSymbols(valueConfig.extensions), - astNode: valueConfig.astNode, - })); -} - /** * Enum Type Definition * @@ -1402,7 +1426,7 @@ function enumValuesFromConfig(values: GraphQLEnumValueConfigMap) { * Note: If a value is not provided in a definition, the name of the enum value * will be used as its internal value. */ -export class GraphQLEnumType /* */ { +export class GraphQLEnumType /* */ extends GraphQLSchemaElement { name: string; description: Maybe; extensions: Readonly; @@ -1417,6 +1441,7 @@ export class GraphQLEnumType /* */ { private _nameLookup: ObjMap | null; constructor(config: Readonly */>) { + super(config.name); this.name = assertName(config.name); this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -1426,18 +1451,24 @@ export class GraphQLEnumType /* */ { this._values = typeof config.values === 'function' ? config.values - : enumValuesFromConfig(config.values); + : Object.entries(config.values).map( + ([valueName, valueConfig]) => + new GraphQLEnumValue(config.name, valueName, valueConfig), + ); this._valueLookup = null; this._nameLookup = null; } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLEnumType'; } getValues(): ReadonlyArray */> { if (typeof this._values === 'function') { - this._values = enumValuesFromConfig(this._values()); + this._values = Object.entries(this._values()).map( + ([valueName, valueConfig]) => + new GraphQLEnumValue(this.name, valueName, valueConfig), + ); } return this._values; } @@ -1544,35 +1575,19 @@ export class GraphQLEnumType /* */ { } toConfig(): GraphQLEnumTypeNormalizedConfig { - const values = keyValMap( - this.getValues(), - (value) => value.name, - (value) => ({ - description: value.description, - value: value.value, - deprecationReason: value.deprecationReason, - extensions: value.extensions, - astNode: value.astNode, - }), - ); - return { name: this.name, description: this.description, - values, + values: keyValMap( + this.getValues(), + (value) => value.name, + (value) => value.toConfig(), + ), extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } function didYouMeanEnumValue( @@ -1624,13 +1639,43 @@ export interface GraphQLEnumValueConfig { astNode?: Maybe; } -export interface GraphQLEnumValue { +export class GraphQLEnumValue extends GraphQLSchemaElement { name: string; description: Maybe; value: any /* T */; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parentCoordinate: string, + name: string, + config: GraphQLEnumValueConfig, + ) { + const coordinate = `${parentCoordinate}.${name}`; + super(coordinate); + + this.name = assertEnumValueName(name); + this.description = config.description; + this.value = config.value !== undefined ? config.value : name; + this.deprecationReason = config.deprecationReason; + this.extensions = toObjMapWithSymbols(config.extensions); + this.astNode = config.astNode; + } + + override get [Symbol.toStringTag]() { + return 'GraphQLEnumValue'; + } + + toConfig(): GraphQLEnumValueConfig { + return { + description: this.description, + value: this.value, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } } /** @@ -1667,7 +1712,7 @@ export interface GraphQLInputObjectTypeExtensions { * }); * ``` */ -export class GraphQLInputObjectType { +export class GraphQLInputObjectType extends GraphQLSchemaElement { name: string; description: Maybe; extensions: Readonly; @@ -1678,6 +1723,7 @@ export class GraphQLInputObjectType { private _fields: ThunkObjMap; constructor(config: Readonly) { + super(config.name); this.name = assertName(config.name); this.description = config.description; this.extensions = toObjMapWithSymbols(config.extensions); @@ -1685,10 +1731,14 @@ export class GraphQLInputObjectType { this.extensionASTNodes = config.extensionASTNodes ?? []; this.isOneOf = config.isOneOf ?? false; - this._fields = defineInputFieldMap.bind(undefined, config.fields); + this._fields = defineInputFieldMap.bind( + undefined, + this.name, + config.fields, + ); } - get [Symbol.toStringTag]() { + override get [Symbol.toStringTag]() { return 'GraphQLInputObjectType'; } @@ -1700,49 +1750,28 @@ export class GraphQLInputObjectType { } toConfig(): GraphQLInputObjectTypeNormalizedConfig { - const fields = mapValue(this.getFields(), (field) => ({ - description: field.description, - type: field.type, - defaultValue: field.defaultValue?.value, - defaultValueLiteral: field.defaultValue?.literal, - deprecationReason: field.deprecationReason, - extensions: field.extensions, - astNode: field.astNode, - })); - return { name: this.name, description: this.description, - fields, + fields: mapValue(this.getFields(), (field) => field.toConfig()), extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, isOneOf: this.isOneOf, }; } - - toString(): string { - return this.name; - } - - toJSON(): string { - return this.toString(); - } } function defineInputFieldMap( + parentCoordinate: string, fields: ThunkObjMap, ): GraphQLInputFieldMap { const fieldMap = resolveObjMapThunk(fields); - return mapValue(fieldMap, (fieldConfig, fieldName) => ({ - name: assertName(fieldName), - description: fieldConfig.description, - type: fieldConfig.type, - defaultValue: defineDefaultValue(fieldName, fieldConfig), - deprecationReason: fieldConfig.deprecationReason, - extensions: toObjMapWithSymbols(fieldConfig.extensions), - astNode: fieldConfig.astNode, - })); + return mapValue( + fieldMap, + (fieldConfig, fieldName) => + new GraphQLInputField(parentCoordinate, fieldName, fieldConfig), + ); } export interface GraphQLInputObjectTypeConfig { @@ -1787,7 +1816,7 @@ export interface GraphQLInputFieldConfig { export type GraphQLInputFieldConfigMap = ObjMap; -export interface GraphQLInputField { +export class GraphQLInputField extends GraphQLSchemaElement { name: string; description: Maybe; type: GraphQLInputType; @@ -1795,6 +1824,44 @@ export interface GraphQLInputField { deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parentCoordinate: string, + name: string, + config: GraphQLInputFieldConfig, + ) { + const coordinate = `${parentCoordinate}.${name}`; + + devAssert( + !('resolve' in config), + `${coordinate} field has a resolve property, but Input Types cannot define resolvers.`, + ); + + super(coordinate); + this.name = assertName(name); + this.description = config.description; + this.type = config.type; + this.defaultValue = defineDefaultValue(name, config); + this.deprecationReason = config.deprecationReason; + this.extensions = toObjMapWithSymbols(config.extensions); + this.astNode = config.astNode; + } + + override get [Symbol.toStringTag]() { + return 'GraphQLInputField'; + } + + toConfig(): GraphQLInputFieldConfig { + return { + description: this.description, + type: this.type, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } } export function isRequiredInputField(field: GraphQLInputField): boolean { diff --git a/src/type/directives.ts b/src/type/directives.ts index 23faa33717..bc4dbfa32d 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,20 +1,21 @@ +import { devAssert } from '../jsutils/devAssert.js'; import { inspect } from '../jsutils/inspect.js'; import { instanceOf } from '../jsutils/instanceOf.js'; +import { isObjectLike } from '../jsutils/isObjectLike.js'; +import { keyValMap } from '../jsutils/keyValMap.js'; import type { Maybe } from '../jsutils/Maybe.js'; +import type { ObjMap } from '../jsutils/ObjMap.js'; import { toObjMapWithSymbols } from '../jsutils/toObjMap.js'; import type { DirectiveDefinitionNode } from '../language/ast.js'; import { DirectiveLocation } from '../language/directiveLocation.js'; import { assertName } from './assertName.js'; -import type { - GraphQLArgument, - GraphQLFieldConfigArgumentMap, -} from './definition.js'; +import type { GraphQLArgumentConfig } from './definition.js'; import { - argsToArgsConfig, - defineArguments, + GraphQLArgument, GraphQLNonNull, + GraphQLSchemaElement, } from './definition.js'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from './scalars.js'; @@ -51,7 +52,7 @@ export interface GraphQLDirectiveExtensions { * Directives are used by the GraphQL runtime as a way of modifying execution * behavior. Type system creators will usually not create these directly. */ -export class GraphQLDirective { +export class GraphQLDirective extends GraphQLSchemaElement { name: string; description: Maybe; locations: ReadonlyArray; @@ -61,6 +62,8 @@ export class GraphQLDirective { astNode: Maybe; constructor(config: Readonly) { + const coordinate = `@${config.name}`; + super(coordinate); this.name = assertName(config.name); this.description = config.description; this.locations = config.locations; @@ -68,8 +71,21 @@ export class GraphQLDirective { this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; + devAssert( + Array.isArray(config.locations), + `${coordinate} locations must be an Array.`, + ); + const args = config.args ?? {}; - this.args = defineArguments(args); + devAssert( + isObjectLike(args) && !Array.isArray(args), + `${coordinate} args must be an object with argument names as keys.`, + ); + + this.args = Object.entries(args).map( + ([argName, argConfig]) => + new GraphQLArgument(coordinate, argName, argConfig), + ); } get [Symbol.toStringTag]() { @@ -81,34 +97,30 @@ export class GraphQLDirective { name: this.name, description: this.description, locations: this.locations, - args: argsToArgsConfig(this.args), + args: keyValMap( + this.args, + (arg) => arg.name, + (arg) => arg.toConfig(), + ), isRepeatable: this.isRepeatable, extensions: this.extensions, astNode: this.astNode, }; } - - toString(): string { - return '@' + this.name; - } - - toJSON(): string { - return this.toString(); - } } export interface GraphQLDirectiveConfig { name: string; description?: Maybe; locations: ReadonlyArray; - args?: Maybe; + args?: Maybe>; isRepeatable?: Maybe; extensions?: Maybe>; astNode?: Maybe; } interface GraphQLDirectiveNormalizedConfig extends GraphQLDirectiveConfig { - args: GraphQLFieldConfigArgumentMap; + args: ObjMap; isRepeatable: boolean; extensions: Readonly; } diff --git a/src/type/index.ts b/src/type/index.ts index 25b59d6177..ba6acd188d 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -55,12 +55,17 @@ export { getNullableType, getNamedType, // Definitions + GraphQLSchemaElement, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, + GraphQLField, + GraphQLArgument, + GraphQLEnumValue, + GraphQLInputField, // Type Wrappers GraphQLList, GraphQLNonNull, @@ -82,23 +87,19 @@ export type { GraphQLNamedOutputType, ThunkReadonlyArray, ThunkObjMap, - GraphQLArgument, GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, - GraphQLEnumValue, GraphQLEnumValueConfig, GraphQLEnumValueConfigMap, GraphQLEnumValueExtensions, - GraphQLField, GraphQLFieldConfig, GraphQLFieldConfigArgumentMap, GraphQLFieldConfigMap, GraphQLFieldExtensions, GraphQLFieldMap, GraphQLFieldResolver, - GraphQLInputField, GraphQLInputFieldConfig, GraphQLInputFieldConfigMap, GraphQLInputFieldExtensions, diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2f6c498f9a..60435e68f5 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -8,7 +8,6 @@ import { valueToLiteral } from '../utilities/valueToLiteral.js'; import type { GraphQLEnumValue, - GraphQLField, GraphQLFieldConfigMap, GraphQLInputField, GraphQLNamedType, @@ -16,6 +15,7 @@ import type { } from './definition.js'; import { GraphQLEnumType, + GraphQLField, GraphQLList, GraphQLNonNull, GraphQLObjectType, @@ -511,53 +511,24 @@ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ }, }); -/** - * Note that these are GraphQLField and not GraphQLFieldConfig, - * so the format for args is different. - */ - -export const SchemaMetaFieldDef: GraphQLField = { - name: '__schema', +export const SchemaMetaFieldDef = new GraphQLField('', '__schema', { type: new GraphQLNonNull(__Schema), description: 'Access the current type schema of this server.', - args: [], resolve: (_source, _args, _context, { schema }) => schema, - deprecationReason: undefined, - extensions: Object.create(null), - astNode: undefined, -}; +}); -export const TypeMetaFieldDef: GraphQLField = { - name: '__type', +export const TypeMetaFieldDef = new GraphQLField('', '__type', { type: __Type, description: 'Request the type information of a single type.', - args: [ - { - name: 'name', - description: undefined, - type: new GraphQLNonNull(GraphQLString), - defaultValue: undefined, - deprecationReason: undefined, - extensions: Object.create(null), - astNode: undefined, - }, - ], + args: { name: { type: new GraphQLNonNull(GraphQLString) } }, resolve: (_source, { name }, _context, { schema }) => schema.getType(name), - deprecationReason: undefined, - extensions: Object.create(null), - astNode: undefined, -}; +}); -export const TypeNameMetaFieldDef: GraphQLField = { - name: '__typename', +export const TypeNameMetaFieldDef = new GraphQLField('', '__typename', { type: new GraphQLNonNull(GraphQLString), description: 'The name of the current Object type at runtime.', - args: [], resolve: (_source, _args, _context, { parentType }) => parentType.name, - deprecationReason: undefined, - extensions: Object.create(null), - astNode: undefined, -}; +}); export const introspectionTypes: ReadonlyArray = Object.freeze([ diff --git a/src/type/validate.ts b/src/type/validate.ts index 48cc70299a..4224f8586f 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -210,25 +210,23 @@ function validateDirectives(context: SchemaValidationContext): void { // Ensure they are named correctly. validateName(context, arg); - const argStr = `${directive}(${arg.name}:)`; - // Ensure the type is an input type. if (!isInputType(arg.type)) { context.reportError( - `The type of ${argStr} must be Input Type ` + + `The type of ${arg} must be Input Type ` + `but got: ${inspect(arg.type)}.`, arg.astNode, ); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { - context.reportError( - `Required argument ${argStr} cannot be deprecated.`, - [getDeprecatedDirectiveNode(arg.astNode), arg.astNode?.type], - ); + context.reportError(`Required argument ${arg} cannot be deprecated.`, [ + getDeprecatedDirectiveNode(arg.astNode), + arg.astNode?.type, + ]); } - validateDefaultValue(context, arg, argStr); + validateDefaultValue(context, arg); } } } @@ -236,7 +234,6 @@ function validateDirectives(context: SchemaValidationContext): void { function validateDefaultValue( context: SchemaValidationContext, inputValue: GraphQLArgument | GraphQLInputField, - argStr: string, ): void { const defaultValue = inputValue.defaultValue; @@ -250,7 +247,7 @@ function validateDefaultValue( inputValue.type, (error, path) => { context.reportError( - `${argStr} has invalid default value${printPathArray(path)}: ${ + `${inputValue} has invalid default value${printPathArray(path)}: ${ error.message }`, error.nodes, @@ -280,7 +277,7 @@ function validateDefaultValue( if (uncoercedErrors.length === 0) { context.reportError( - `${argStr} has invalid default value: ${inspect( + `${inputValue} has invalid default value: ${inspect( defaultValue.value, )}. Did you mean: ${inspect(uncoercedValue)}?`, inputValue.astNode?.defaultValue, @@ -295,7 +292,7 @@ function validateDefaultValue( // Otherwise report the original set of errors. for (const [error, path] of errors) { context.reportError( - `${argStr} has invalid default value${printPathArray(path)}: ${ + `${inputValue} has invalid default value${printPathArray(path)}: ${ error.message }`, inputValue.astNode?.defaultValue, @@ -437,7 +434,7 @@ function validateFields( // Ensure the type is an output type if (!isOutputType(field.type)) { context.reportError( - `The type of ${type}.${field.name} must be Output Type ` + + `The type of ${field} must be Output Type ` + `but got: ${inspect(field.type)}.`, field.astNode?.type, ); @@ -445,29 +442,25 @@ function validateFields( // Ensure the arguments are valid for (const arg of field.args) { - const argName = arg.name; - // Ensure they are named correctly. validateName(context, arg); - const argStr = `${type}.${field.name}(${argName}:)`; - // Ensure the type is an input type if (!isInputType(arg.type)) { context.reportError( - `The type of ${argStr} must be Input Type but got: ${inspect(arg.type)}.`, + `The type of ${arg} must be Input Type but got: ${inspect(arg.type)}.`, arg.astNode?.type, ); } if (isRequiredArgument(arg) && arg.deprecationReason != null) { - context.reportError( - `Required argument ${type}.${field.name}(${argName}:) cannot be deprecated.`, - [getDeprecatedDirectiveNode(arg.astNode), arg.astNode?.type], - ); + context.reportError(`Required argument ${arg} cannot be deprecated.`, [ + getDeprecatedDirectiveNode(arg.astNode), + arg.astNode?.type, + ]); } - validateDefaultValue(context, arg, argStr); + validateDefaultValue(context, arg); } } } @@ -480,7 +473,7 @@ function validateInterfaces( for (const iface of type.getInterfaces()) { if (!isInterfaceType(iface)) { context.reportError( - `Type ${inspect(type)} must only implement Interface types, ` + + `Type ${type} must only implement Interface types, ` + `it cannot implement ${inspect(iface)}.`, getAllImplementsInterfaceNodes(type, iface), ); @@ -497,7 +490,7 @@ function validateInterfaces( if (ifaceTypeNames.has(iface.name)) { context.reportError( - `Type ${type} can only implement ${iface.name} once.`, + `Type ${type} can only implement ${iface} once.`, getAllImplementsInterfaceNodes(type, iface), ); continue; @@ -519,13 +512,12 @@ function validateTypeImplementsInterface( // Assert each interface field is implemented. for (const ifaceField of Object.values(iface.getFields())) { - const fieldName = ifaceField.name; - const typeField = typeFieldMap[fieldName]; + const typeField = typeFieldMap[ifaceField.name]; // Assert interface field exists on type. if (typeField == null) { context.reportError( - `Interface field ${iface.name}.${fieldName} expected but ${type} does not provide it.`, + `Interface field ${ifaceField} expected but ${type} does not provide it.`, [ifaceField.astNode, type.astNode, ...type.extensionASTNodes], ); continue; @@ -535,22 +527,20 @@ function validateTypeImplementsInterface( // a valid subtype. (covariant) if (!isTypeSubTypeOf(context.schema, typeField.type, ifaceField.type)) { context.reportError( - `Interface field ${iface.name}.${fieldName} expects type ` + - `${inspect(ifaceField.type)} but ${type}.${fieldName} ` + - `is type ${inspect(typeField.type)}.`, + `Interface field ${ifaceField} expects type ${ifaceField.type} ` + + `but ${typeField} is type ${typeField.type}.`, [ifaceField.astNode?.type, typeField.astNode?.type], ); } // Assert each interface field arg is implemented. for (const ifaceArg of ifaceField.args) { - const argName = ifaceArg.name; - const typeArg = typeField.args.find((arg) => arg.name === argName); + const typeArg = typeField.args.find((arg) => arg.name === ifaceArg.name); // Assert interface field arg exists on object field. if (!typeArg) { context.reportError( - `Interface field argument ${iface.name}.${fieldName}(${argName}:) expected but ${type}.${fieldName} does not provide it.`, + `Interface field argument ${ifaceArg} expected but ${typeField} does not provide it.`, [ifaceArg.astNode, typeField.astNode], ); continue; @@ -561,10 +551,8 @@ function validateTypeImplementsInterface( // TODO: change to contravariant? if (!isEqualType(ifaceArg.type, typeArg.type)) { context.reportError( - `Interface field argument ${iface.name}.${fieldName}(${argName}:) ` + - `expects type ${inspect(ifaceArg.type)} but ` + - `${type}.${fieldName}(${argName}:) is type ` + - `${inspect(typeArg.type)}.`, + `Interface field argument ${ifaceArg} expects type ${ifaceArg.type} ` + + `but ${typeArg} is type ${typeArg.type}.`, [ifaceArg.astNode?.type, typeArg.astNode?.type], ); } @@ -572,17 +560,17 @@ function validateTypeImplementsInterface( // Assert additional arguments must not be required. for (const typeArg of typeField.args) { - const argName = typeArg.name; - const ifaceArg = ifaceField.args.find((arg) => arg.name === argName); - if (!ifaceArg && isRequiredArgument(typeArg)) { - context.reportError( - `Argument "${type}.${fieldName}(${argName}:)" must not be required type "${inspect( - typeArg.type, - )}" if not provided by the Interface field "${ - iface.name - }.${fieldName}".`, - [typeArg.astNode, ifaceField.astNode], + if (isRequiredArgument(typeArg)) { + const ifaceArg = ifaceField.args.find( + (arg) => arg.name === typeArg.name, ); + if (!ifaceArg) { + context.reportError( + `Argument "${typeArg}" must not be required type "${typeArg.type}" ` + + `if not provided by the Interface field "${ifaceField}".`, + [typeArg.astNode, ifaceField.astNode], + ); + } } } } @@ -598,8 +586,8 @@ function validateTypeImplementsAncestors( if (!ifaceInterfaces.includes(transitive)) { context.reportError( transitive === type - ? `Type ${type} cannot implement ${iface.name} because it would create a circular reference.` - : `Type ${type} must implement ${transitive.name} because it is implemented by ${iface.name}.`, + ? `Type ${type} cannot implement ${iface} because it would create a circular reference.` + : `Type ${type} must implement ${transitive} because it is implemented by ${iface}.`, [ ...getAllImplementsInterfaceNodes(iface, transitive), ...getAllImplementsInterfaceNodes(type, iface), @@ -617,7 +605,7 @@ function validateUnionMembers( if (memberTypes.length === 0) { context.reportError( - `Union type ${union.name} must define one or more member types.`, + `Union type ${union} must define one or more member types.`, [union.astNode, ...union.extensionASTNodes], ); } @@ -626,7 +614,7 @@ function validateUnionMembers( for (const memberType of memberTypes) { if (includedTypeNames.has(memberType.name)) { context.reportError( - `Union type ${union.name} can only include type ${memberType} once.`, + `Union type ${union} can only include type ${memberType} once.`, getUnionMemberTypeNodes(union, memberType.name), ); continue; @@ -634,7 +622,7 @@ function validateUnionMembers( includedTypeNames.add(memberType.name); if (!isObjectType(memberType)) { context.reportError( - `Union type ${union.name} can only include Object types, ` + + `Union type ${union} can only include Object types, ` + `it cannot include ${inspect(memberType)}.`, getUnionMemberTypeNodes(union, String(memberType)), ); @@ -669,7 +657,7 @@ function validateInputFields( if (fields.length === 0) { context.reportError( - `Input Object type ${inputObj.name} must define one or more fields.`, + `Input Object type ${inputObj} must define one or more fields.`, [inputObj.astNode, ...inputObj.extensionASTNodes], ); } @@ -682,22 +670,20 @@ function validateInputFields( // Ensure the type is an input type if (!isInputType(field.type)) { context.reportError( - `The type of ${inputObj.name}.${field.name} must be Input Type ` + + `The type of ${field} must be Input Type ` + `but got: ${inspect(field.type)}.`, field.astNode?.type, ); } - const fieldStr = `${inputObj.name}.${field.name}`; - if (isRequiredInputField(field) && field.deprecationReason != null) { context.reportError( - `Required input field ${fieldStr} cannot be deprecated.`, + `Required input field ${field} cannot be deprecated.`, [getDeprecatedDirectiveNode(field.astNode), field.astNode?.type], ); } - validateDefaultValue(context, field, fieldStr); + validateDefaultValue(context, field); if (inputObj.isOneOf) { validateOneOfInputObjectField(inputObj, field, context); diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 4fd2dbcd34..2f85191039 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -377,32 +377,38 @@ describe('Type System: build schema from introspection', () => { // Client types do not get server-only values, so `value` mirrors `name`, // rather than using the integers defined in the "server" schema. - expect(clientFoodEnum.getValues()).to.deep.equal([ - { - name: 'VEGETABLES', - description: 'Foods that are vegetables.', - value: 'VEGETABLES', - deprecationReason: null, - extensions: {}, - astNode: undefined, - }, - { - name: 'FRUITS', - description: null, - value: 'FRUITS', - deprecationReason: null, - extensions: {}, - astNode: undefined, - }, - { - name: 'OILS', - description: null, - value: 'OILS', - deprecationReason: 'Too fatty', - extensions: {}, - astNode: undefined, - }, - ]); + const values = clientFoodEnum.getValues(); + expect(values).to.have.lengthOf(3); + + expect(values[0]).to.deep.include({ + coordinate: 'Food.VEGETABLES', + name: 'VEGETABLES', + description: 'Foods that are vegetables.', + value: 'VEGETABLES', + deprecationReason: null, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + coordinate: 'Food.FRUITS', + name: 'FRUITS', + description: null, + value: 'FRUITS', + deprecationReason: null, + extensions: {}, + astNode: undefined, + }); + + expect(values[2]).to.deep.include({ + coordinate: 'Food.OILS', + name: 'OILS', + description: null, + value: 'OILS', + deprecationReason: 'Too fatty', + extensions: {}, + astNode: undefined, + }); }); it('builds a schema with an input object', () => { diff --git a/src/utilities/__tests__/findSchemaChanges-test.ts b/src/utilities/__tests__/findSchemaChanges-test.ts index 4aa351707b..7bcdf32264 100644 --- a/src/utilities/__tests__/findSchemaChanges-test.ts +++ b/src/utilities/__tests__/findSchemaChanges-test.ts @@ -149,7 +149,7 @@ describe('findSchemaChanges', () => { { type: SafeChangeType.DESCRIPTION_CHANGED, description: - 'Description of argument Query.foo(x) has changed to "New Description".', + 'Description of argument Query.foo(x:) has changed to "New Description".', }, ]); }); diff --git a/src/utilities/__tests__/resolveSchemaCoordinate-test.ts b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts new file mode 100644 index 0000000000..e316ef52a1 --- /dev/null +++ b/src/utilities/__tests__/resolveSchemaCoordinate-test.ts @@ -0,0 +1,185 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import type { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLObjectType, +} from '../../type/definition.js'; +import type { GraphQLDirective } from '../../type/directives.js'; + +import { buildSchema } from '../buildASTSchema.js'; +import { resolveSchemaCoordinate } from '../resolveSchemaCoordinate.js'; + +describe('resolveSchemaCoordinate', () => { + const schema = buildSchema(` + type Query { + searchBusiness(criteria: SearchCriteria!): [Business] + } + + input SearchCriteria { + name: String + filter: SearchFilter + } + + enum SearchFilter { + OPEN_NOW + DELIVERS_TAKEOUT + VEGETARIAN_MENU + } + + type Business { + id: ID + name: String + email: String @private(scope: "loggedIn") + } + + directive @private(scope: String!) on FIELD_DEFINITION + `); + + it('resolves a Named Type', () => { + expect(resolveSchemaCoordinate(schema, 'Business')).to.deep.equal({ + kind: 'NamedType', + type: schema.getType('Business'), + }); + + expect(resolveSchemaCoordinate(schema, 'String')).to.deep.equal({ + kind: 'NamedType', + type: schema.getType('String'), + }); + + expect(resolveSchemaCoordinate(schema, 'private')).to.deep.equal(undefined); + + expect(resolveSchemaCoordinate(schema, 'Unknown')).to.deep.equal(undefined); + }); + + it('resolves a Type Field', () => { + 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.deep.equal( + undefined, + ); + + expect(resolveSchemaCoordinate(schema, 'String.field')).to.deep.equal( + undefined, + ); + }); + + it('does not resolve meta-fields', () => { + expect( + resolveSchemaCoordinate(schema, 'Business.__typename'), + ).to.deep.equal(undefined); + }); + + it('resolves a Input Field', () => { + 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.deep.equal(undefined); + }); + + it('resolves a Enum Value', () => { + 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.deep.equal(undefined); + }); + + it('resolves a Field Argument', () => { + 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.deep.equal({ + kind: 'FieldArgument', + type, + field, + fieldArgument, + }); + + expect( + resolveSchemaCoordinate(schema, 'Business.name(unknown:)'), + ).to.deep.equal(undefined); + + expect( + resolveSchemaCoordinate(schema, 'Unknown.field(arg:)'), + ).to.deep.equal(undefined); + + expect( + resolveSchemaCoordinate(schema, 'Business.unknown(arg:)'), + ).to.deep.equal(undefined); + + expect( + resolveSchemaCoordinate(schema, 'SearchCriteria.name(arg:)'), + ).to.deep.equal(undefined); + }); + + it('resolves a Directive', () => { + 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.deep.equal( + undefined, + ); + + expect(resolveSchemaCoordinate(schema, '@Business')).to.deep.equal( + undefined, + ); + }); + + it('resolves a Directive Argument', () => { + 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.deep.equal( + undefined, + ); + + expect(resolveSchemaCoordinate(schema, '@unknown(arg:)')).to.deep.equal( + undefined, + ); + }); +}); diff --git a/src/utilities/findSchemaChanges.ts b/src/utilities/findSchemaChanges.ts index 4cffd5aad6..fb87cd494d 100644 --- a/src/utilities/findSchemaChanges.ts +++ b/src/utilities/findSchemaChanges.ts @@ -160,7 +160,7 @@ function findDirectiveChanges( for (const oldDirective of directivesDiff.removed) { schemaChanges.push({ type: BreakingChangeType.DIRECTIVE_REMOVED, - description: `Directive @${oldDirective.name} was removed.`, + description: `Directive ${oldDirective} was removed.`, }); } @@ -178,7 +178,7 @@ function findDirectiveChanges( if (isRequiredArgument(newArg)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_DIRECTIVE_ARG_ADDED, - description: `A required argument @${oldDirective.name}(${newArg.name}:) was added.`, + description: `A required argument ${newArg} was added.`, }); } else { schemaChanges.push({ @@ -191,7 +191,7 @@ function findDirectiveChanges( for (const oldArg of argsDiff.removed) { schemaChanges.push({ type: BreakingChangeType.DIRECTIVE_ARG_REMOVED, - description: `Argument @${oldDirective.name}(${oldArg.name}:) was removed.`, + description: `Argument ${oldArg} was removed.`, }); } @@ -257,7 +257,7 @@ function findDirectiveChanges( if (oldDirective.isRepeatable && !newDirective.isRepeatable) { schemaChanges.push({ type: BreakingChangeType.DIRECTIVE_REPEATABLE_REMOVED, - description: `Repeatable flag was removed from @${oldDirective.name}.`, + description: `Repeatable flag was removed from ${oldDirective}.`, }); } else if (newDirective.isRepeatable && !oldDirective.isRepeatable) { schemaChanges.push({ @@ -277,7 +277,7 @@ function findDirectiveChanges( if (!newDirective.locations.includes(location)) { schemaChanges.push({ type: BreakingChangeType.DIRECTIVE_LOCATION_REMOVED, - description: `${location} was removed from @${oldDirective.name}.`, + description: `${location} was removed from ${oldDirective}.`, }); } } @@ -349,9 +349,9 @@ function findTypeChanges( } else if (oldType.constructor !== newType.constructor) { schemaChanges.push({ type: BreakingChangeType.TYPE_CHANGED_KIND, - description: - `${oldType} changed from ` + - `${typeKindName(oldType)} to ${typeKindName(newType)}.`, + description: `${oldType} changed from ${typeKindName( + oldType, + )} to ${typeKindName(newType)}.`, }); } } @@ -373,12 +373,12 @@ function findInputObjectTypeChanges( if (isRequiredInputField(newField)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_INPUT_FIELD_ADDED, - description: `A required field ${oldType}.${newField.name} was added.`, + description: `A required field ${newField} was added.`, }); } else { schemaChanges.push({ type: DangerousChangeType.OPTIONAL_INPUT_FIELD_ADDED, - description: `An optional field ${oldType}.${newField.name} was added.`, + description: `An optional field ${newField} was added.`, }); } } @@ -386,7 +386,7 @@ function findInputObjectTypeChanges( for (const oldField of fieldsDiff.removed) { schemaChanges.push({ type: BreakingChangeType.FIELD_REMOVED, - description: `Field ${oldType}.${oldField.name} was removed.`, + description: `Field ${oldField} was removed.`, }); } @@ -398,9 +398,7 @@ function findInputObjectTypeChanges( if (!isSafe) { schemaChanges.push({ type: BreakingChangeType.FIELD_CHANGED_KIND, - description: - `Field ${oldType}.${oldField.name} changed type from ` + - `${String(oldField.type)} to ${String(newField.type)}.`, + description: `Field ${newField} changed type from ${oldField.type} to ${newField.type}.`, }); } else if (oldField.type.toString() !== newField.type.toString()) { schemaChanges.push({ @@ -456,14 +454,14 @@ function findEnumTypeChanges( for (const newValue of valuesDiff.added) { schemaChanges.push({ type: DangerousChangeType.VALUE_ADDED_TO_ENUM, - description: `Enum value ${oldType}.${newValue.name} was added.`, + description: `Enum value ${newValue} was added.`, }); } for (const oldValue of valuesDiff.removed) { schemaChanges.push({ type: BreakingChangeType.VALUE_REMOVED_FROM_ENUM, - description: `Enum value ${oldType}.${oldValue.name} was removed.`, + description: `Enum value ${oldValue} was removed.`, }); } @@ -489,14 +487,14 @@ function findImplementedInterfacesChanges( for (const newInterface of interfacesDiff.added) { schemaChanges.push({ type: DangerousChangeType.IMPLEMENTED_INTERFACE_ADDED, - description: `${newInterface.name} added to interfaces implemented by ${oldType}.`, + description: `${newInterface} added to interfaces implemented by ${oldType}.`, }); } for (const oldInterface of interfacesDiff.removed) { schemaChanges.push({ type: BreakingChangeType.IMPLEMENTED_INTERFACE_REMOVED, - description: `${oldType} no longer implements interface ${oldInterface.name}.`, + description: `${oldType} no longer implements interface ${oldInterface}.`, }); } @@ -516,7 +514,7 @@ function findFieldChanges( for (const oldField of fieldsDiff.removed) { schemaChanges.push({ type: BreakingChangeType.FIELD_REMOVED, - description: `Field ${oldType}.${oldField.name} was removed.`, + description: `Field ${oldField} was removed.`, }); } @@ -528,7 +526,7 @@ function findFieldChanges( } for (const [oldField, newField] of fieldsDiff.persisted) { - schemaChanges.push(...findArgChanges(oldType, oldField, newField)); + schemaChanges.push(...findArgChanges(oldField, newField)); const isSafe = isChangeSafeForObjectOrInterfaceField( oldField.type, @@ -537,9 +535,7 @@ function findFieldChanges( if (!isSafe) { schemaChanges.push({ type: BreakingChangeType.FIELD_CHANGED_KIND, - description: - `Field ${oldType}.${oldField.name} changed type from ` + - `${String(oldField.type)} to ${String(newField.type)}.`, + description: `Field ${newField} changed type from ${oldField.type} to ${newField.type}.`, }); } else if (oldField.type.toString() !== newField.type.toString()) { schemaChanges.push({ @@ -562,7 +558,6 @@ function findFieldChanges( } function findArgChanges( - oldType: GraphQLObjectType | GraphQLInterfaceType, oldField: GraphQLField, newField: GraphQLField, ): Array { @@ -572,7 +567,7 @@ function findArgChanges( for (const oldArg of argsDiff.removed) { schemaChanges.push({ type: BreakingChangeType.ARG_REMOVED, - description: `Argument ${oldType}.${oldField.name}(${oldArg.name}:) was removed.`, + description: `Argument ${oldArg} was removed.`, }); } @@ -585,15 +580,13 @@ function findArgChanges( if (!isSafe) { schemaChanges.push({ type: BreakingChangeType.ARG_CHANGED_KIND, - description: - `Argument ${oldType}.${oldField.name}(${oldArg.name}:) has changed type from ` + - `${String(oldArg.type)} to ${String(newArg.type)}.`, + description: `Argument ${newArg} has changed type from ${oldArg.type} to ${newArg.type}.`, }); } else if (oldArg.defaultValue !== undefined) { if (newArg.defaultValue === undefined) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, - description: `${oldType}.${oldField.name}(${oldArg.name}:) defaultValue was removed.`, + description: `${oldArg} defaultValue was removed.`, }); } else { // Since we looking only for client's observable changes we should @@ -605,7 +598,7 @@ function findArgChanges( if (oldValueStr !== newValueStr) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, - description: `${oldType}.${oldField.name}(${oldArg.name}:) has changed defaultValue from ${oldValueStr} to ${newValueStr}.`, + description: `${oldArg} has changed defaultValue from ${oldValueStr} to ${newValueStr}.`, }); } } @@ -616,13 +609,13 @@ function findArgChanges( const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); schemaChanges.push({ type: SafeChangeType.ARG_DEFAULT_VALUE_ADDED, - description: `${oldType}.${oldField.name}(${oldArg.name}:) added a defaultValue ${newValueStr}.`, + description: `${oldArg} added a defaultValue ${newValueStr}.`, }); } else if (oldArg.type.toString() !== newArg.type.toString()) { schemaChanges.push({ type: SafeChangeType.ARG_CHANGED_KIND_SAFE, description: - `Argument ${oldType}.${oldField.name}(${oldArg.name}:) has changed type from ` + + `Argument ${oldArg} has changed type from ` + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); } @@ -630,7 +623,7 @@ function findArgChanges( if (oldArg.description !== newArg.description) { schemaChanges.push({ type: SafeChangeType.DESCRIPTION_CHANGED, - description: `Description of argument ${oldType}.${oldField.name}(${oldArg.name}) has changed to "${newArg.description}".`, + description: `Description of argument ${oldArg} has changed to "${newArg.description}".`, }); } } @@ -639,12 +632,12 @@ function findArgChanges( if (isRequiredArgument(newArg)) { schemaChanges.push({ type: BreakingChangeType.REQUIRED_ARG_ADDED, - description: `A required argument ${oldType}.${oldField.name}(${newArg.name}:) was added.`, + description: `A required argument ${newArg} was added.`, }); } else { schemaChanges.push({ type: DangerousChangeType.OPTIONAL_ARG_ADDED, - description: `An optional argument ${oldType}.${oldField.name}(${newArg.name}:) was added.`, + description: `An optional argument ${newArg} was added.`, }); } } diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 5b891cded1..470ff1ee29 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -127,3 +127,10 @@ export type { // Wrapper type that contains DocumentNode and types that can be deduced from it. export type { TypedQueryDocumentNode } from './typedQueryDocumentNode.js'; + +// Schema coordinates +export { + resolveSchemaCoordinate, + resolveASTSchemaCoordinate, +} from './resolveSchemaCoordinate.js'; +export type { ResolvedSchemaElement } from './resolveSchemaCoordinate.js'; diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts new file mode 100644 index 0000000000..9b9ce6f38b --- /dev/null +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -0,0 +1,190 @@ +import type { SchemaCoordinateNode } from '../language/ast.js'; +import { parseSchemaCoordinate } from '../language/parser.js'; +import type { Source } from '../language/source.js'; + +import type { + GraphQLArgument, + GraphQLEnumValue, + GraphQLField, + GraphQLInputField, + GraphQLNamedType, +} from '../type/definition.js'; +import { + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, +} from '../type/definition.js'; +import type { GraphQLDirective } from '../type/directives.js'; +import type { GraphQLSchema } from '../type/schema.js'; + +/** + * A resolved schema element may be one of the following kinds: + */ +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; + }; + +/** + * A schema coordinate is resolved in the context of a GraphQL schema to + * uniquely identifies a schema element. It returns undefined if the schema + * coordinate does not resolve to a schema element. + * + * https://spec.graphql.org/draft/#sec-Schema-Coordinates.Semantics + */ +export function resolveSchemaCoordinate( + schema: GraphQLSchema, + schemaCoordinate: string | Source, +): ResolvedSchemaElement | undefined { + return resolveASTSchemaCoordinate( + schema, + parseSchemaCoordinate(schemaCoordinate), + ); +} + +/** + * Resolves schema coordinate from a parsed SchemaCoordinate node. + */ +export function resolveASTSchemaCoordinate( + 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 : @ 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, + ); + 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) { + 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) { + return; + } + return { kind: 'Field', type, field }; + } + + // SchemaCoordinate : Name . Name ( Name : ) + // Assert {type} must be an Object or Interface type. + if (!isObjectType(type) && !isInterfaceType(type)) { + 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. + if (field == null) { + return; + } + // 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 }; +} diff --git a/src/validation/rules/KnownArgumentNamesRule.ts b/src/validation/rules/KnownArgumentNamesRule.ts index 8db00b1497..40b925aab1 100644 --- a/src/validation/rules/KnownArgumentNamesRule.ts +++ b/src/validation/rules/KnownArgumentNamesRule.ts @@ -55,9 +55,8 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { Argument(argNode) { const argDef = context.getArgument(); const fieldDef = context.getFieldDef(); - const parentType = context.getParentType(); - if (!argDef && fieldDef && parentType) { + if (!argDef && fieldDef) { const argName = argNode.name.value; const suggestions = context.hideSuggestions ? [] @@ -67,7 +66,7 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { ); context.reportError( new GraphQLError( - `Unknown argument "${argName}" on field "${parentType}.${fieldDef.name}".` + + `Unknown argument "${argName}" on field "${fieldDef}".` + didYouMean(suggestions), { nodes: argNode }, ), diff --git a/src/validation/rules/ProvidedRequiredArgumentsRule.ts b/src/validation/rules/ProvidedRequiredArgumentsRule.ts index 5576b4f2d6..27bbccd4a4 100644 --- a/src/validation/rules/ProvidedRequiredArgumentsRule.ts +++ b/src/validation/rules/ProvidedRequiredArgumentsRule.ts @@ -12,12 +12,10 @@ import type { ASTVisitor } from '../../language/visitor.js'; import type { GraphQLArgument } from '../../type/definition.js'; import { - getNamedType, isRequiredArgument, isType, } from '../../type/definition.js'; import { specifiedDirectives } from '../../type/directives.js'; -import { isIntrospectionType } from '../../type/introspection.js'; import { typeFromAST } from '../../utilities/typeFromAST.js'; @@ -51,20 +49,9 @@ export function ProvidedRequiredArgumentsRule( ); for (const argDef of fieldDef.args) { if (!providedArgs.has(argDef.name) && isRequiredArgument(argDef)) { - const fieldType = getNamedType(context.getType()); - let parentTypeStr: string | undefined; - if (fieldType && isIntrospectionType(fieldType)) { - parentTypeStr = '.'; - } else { - const parentType = context.getParentType(); - if (parentType) { - parentTypeStr = `${context.getParentType()}.`; - } - } - const argTypeStr = inspect(argDef.type); context.reportError( new GraphQLError( - `Argument "${parentTypeStr}${fieldDef.name}(${argDef.name}:)" of type "${argTypeStr}" is required, but it was not provided.`, + `Argument "${argDef}" of type "${argDef.type}" is required, but it was not provided.`, { nodes: fieldNode }, ), ); diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index dfcedbe547..d5e24d13d4 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -1,4 +1,3 @@ -import { inspect } from '../../jsutils/inspect.js'; import type { Maybe } from '../../jsutils/Maybe.js'; import { GraphQLError } from '../../error/GraphQLError.js'; @@ -74,11 +73,9 @@ export function VariablesInAllowedPositionRule( defaultValue, ) ) { - const varTypeStr = inspect(varType); - const typeStr = inspect(type); context.reportError( new GraphQLError( - `Variable "$${varName}" of type "${varTypeStr}" used in position expecting type "${typeStr}".`, + `Variable "$${varName}" of type "${varType}" used in position expecting type "${type}".`, { nodes: [varDef, node] }, ), ); @@ -89,11 +86,9 @@ export function VariablesInAllowedPositionRule( parentType.isOneOf && isNullableType(varType) ) { - const varTypeStr = inspect(varType); - const parentTypeStr = inspect(parentType); context.reportError( new GraphQLError( - `Variable "$${varName}" is of type "${varTypeStr}" but must be non-nullable to be used for OneOf Input Object "${parentTypeStr}".`, + `Variable "$${varName}" is of type "${varType}" but must be non-nullable to be used for OneOf Input Object "${parentType}".`, { nodes: [varDef, node] }, ), ); diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.ts b/src/validation/rules/custom/NoDeprecatedCustomRule.ts index 1e3328039e..6fcf5651a7 100644 --- a/src/validation/rules/custom/NoDeprecatedCustomRule.ts +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.ts @@ -1,5 +1,3 @@ -import { invariant } from '../../../jsutils/invariant.js'; - import { GraphQLError } from '../../../error/GraphQLError.js'; import type { ASTVisitor } from '../../../language/visitor.js'; @@ -24,11 +22,9 @@ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { const fieldDef = context.getFieldDef(); const deprecationReason = fieldDef?.deprecationReason; if (fieldDef && deprecationReason != null) { - const parentType = context.getParentType(); - invariant(parentType != null); context.reportError( new GraphQLError( - `The field ${parentType}.${fieldDef.name} is deprecated. ${deprecationReason}`, + `The field ${fieldDef} is deprecated. ${deprecationReason}`, { nodes: node }, ), ); @@ -38,25 +34,12 @@ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { const argDef = context.getArgument(); const deprecationReason = argDef?.deprecationReason; if (argDef && deprecationReason != null) { - const directiveDef = context.getDirective(); - if (directiveDef != null) { - context.reportError( - new GraphQLError( - `The argument "@${directiveDef.name}(${argDef.name}:)" is deprecated. ${deprecationReason}`, - { nodes: node }, - ), - ); - } else { - const parentType = context.getParentType(); - const fieldDef = context.getFieldDef(); - invariant(parentType != null && fieldDef != null); - context.reportError( - new GraphQLError( - `The argument "${parentType}.${fieldDef.name}(${argDef.name}:)" is deprecated. ${deprecationReason}`, - { nodes: node }, - ), - ); - } + context.reportError( + new GraphQLError( + `The argument "${argDef}" is deprecated. ${deprecationReason}`, + { nodes: node }, + ), + ); } }, ObjectField(node) { @@ -67,7 +50,7 @@ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { if (deprecationReason != null) { context.reportError( new GraphQLError( - `The input field ${inputObjectDef.name}.${inputFieldDef.name} is deprecated. ${deprecationReason}`, + `The input field ${inputFieldDef} is deprecated. ${deprecationReason}`, { nodes: node }, ), ); @@ -78,11 +61,9 @@ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { const enumValueDef = context.getEnumValue(); const deprecationReason = enumValueDef?.deprecationReason; if (enumValueDef && deprecationReason != null) { - const enumTypeDef = getNamedType(context.getInputType()); - invariant(enumTypeDef != null); context.reportError( new GraphQLError( - `The enum value "${enumTypeDef.name}.${enumValueDef.name}" is deprecated. ${deprecationReason}`, + `The enum value "${enumValueDef}" is deprecated. ${deprecationReason}`, { nodes: node }, ), );