From 922ff96ebbf2b658a0ec234f2edbb778a1a49d7c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 7 Nov 2024 13:31:38 +0200 Subject: [PATCH 1/3] introduce GraphQLField, GraphQLInputField, GraphQLArgument, and GraphQLEnumValue --- src/execution/__tests__/nonnull-test.ts | 8 +- src/execution/__tests__/oneof-test.ts | 10 +- src/execution/__tests__/variables-test.ts | 12 +- src/execution/values.ts | 10 +- src/index.ts | 31 +- src/type/__tests__/definition-test.ts | 238 +++++----- src/type/__tests__/directive-test.ts | 44 +- src/type/__tests__/enumType-test.ts | 40 +- src/type/__tests__/predicate-test.ts | 108 +++-- src/type/definition.ts | 419 ++++++++++++------ src/type/directives.ts | 38 +- src/type/index.ts | 23 +- src/type/introspection.ts | 45 +- src/type/validate.ts | 104 ++--- .../__tests__/buildClientSchema-test.ts | 55 +-- .../__tests__/findSchemaChanges-test.ts | 2 +- src/utilities/findSchemaChanges.ts | 63 ++- src/utilities/printSchema.ts | 2 +- .../rules/KnownArgumentNamesRule.ts | 5 +- .../rules/ProvidedRequiredArgumentsRule.ts | 20 +- .../rules/VariablesInAllowedPositionRule.ts | 9 +- .../rules/custom/NoDeprecatedCustomRule.ts | 37 +- 22 files changed, 765 insertions(+), 558 deletions(-) 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..adecc0fa35 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -14,7 +14,11 @@ import type { import { Kind } from '../language/kinds.js'; import type { GraphQLArgument, GraphQLField } from '../type/definition.js'; -import { isNonNullType, isRequiredArgument } from '../type/definition.js'; +import { + isArgument, + 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 "${isArgument(argDef) ? 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 "${isArgument(argDef) ? argDef : argDef.name}" has invalid value${printPathArray( path, )}: ${error.message}`; throw error; diff --git a/src/index.ts b/src/index.ts index aa3153b5f7..55b58b3f01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,10 +34,15 @@ export type { GraphQLArgs } from './graphql.js'; export { graphql, graphqlSync } from './graphql.js'; // Create and operate on GraphQL type definitions and schema. +export type { + GraphQLField, + GraphQLArgument, + GraphQLEnumValue, + GraphQLInputField, +} from './type/index.js'; export { resolveObjMapThunk, resolveReadonlyArrayThunk, - // Definitions GraphQLSchema, GraphQLDirective, GraphQLScalarType, @@ -48,17 +53,14 @@ export { GraphQLInputObjectType, GraphQLList, GraphQLNonNull, - // Standard GraphQL Scalars specifiedScalarTypes, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean, GraphQLID, - // Int boundaries constants GRAPHQL_MAX_INT, GRAPHQL_MIN_INT, - // Built-in Directives defined by the Spec specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, @@ -67,11 +69,8 @@ export { GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, GraphQLOneOfDirective, - // "Enum" of Type Kinds TypeKind, - // Constant Deprecation Reason DEFAULT_DEPRECATION_REASON, - // GraphQL Types for introspection. introspectionTypes, __Schema, __Directive, @@ -81,20 +80,22 @@ export { __InputValue, __EnumValue, __TypeKind, - // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, - // Predicates isSchema, isDirective, isType, isScalarType, isObjectType, + isField, + isArgument, isInterfaceType, isUnionType, isEnumType, + isEnumValue, isInputObjectType, + isInputField, isListType, isNonNullType, isInputType, @@ -110,16 +111,19 @@ export { isSpecifiedScalarType, isIntrospectionType, isSpecifiedDirective, - // Assertions assertSchema, assertDirective, assertType, assertScalarType, assertObjectType, + assertField, + assertArgument, assertInterfaceType, assertUnionType, assertEnumType, + assertEnumValue, assertInputObjectType, + assertInputField, assertListType, assertNonNullType, assertInputType, @@ -130,13 +134,10 @@ export { assertWrappingType, assertNullableType, assertNamedType, - // Un-modifiers getNullableType, getNamedType, - // Validate GraphQL schema. validateSchema, assertValidSchema, - // Upholds the spec rules about naming. assertName, assertEnumValueName, } from './type/index.js'; @@ -161,23 +162,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, diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index ad35af937f..9d162d49d4 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -27,9 +27,18 @@ import { GraphQLScalarType, GraphQLUnionType, } from '../definition.js'; +import { GraphQLString } from '../scalars.js'; const ScalarType = new GraphQLScalarType({ name: 'Scalar' }); -const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} }); +const ObjectType = new GraphQLObjectType({ + name: 'Object', + fields: { + someField: { + type: GraphQLString, + args: { someArg: { type: GraphQLString } }, + }, + }, +}); const InterfaceType = new GraphQLInterfaceType({ name: 'Interface', fields: {}, @@ -38,7 +47,7 @@ const UnionType = new GraphQLUnionType({ name: 'Union', types: [ObjectType] }); const EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } }); const InputObjectType = new GraphQLInputObjectType({ name: 'InputObject', - fields: {}, + fields: { someInputField: { type: GraphQLString } }, }); const ListOfScalarsType = new GraphQLList(ScalarType); @@ -237,7 +246,15 @@ describe('Type System: Objects', () => { fields: outputFields, }); - expect(testObject1.getFields()).to.deep.equal(testObject2.getFields()); + const testObject1Fields = testObject1.getFields(); + const testObject2Fields = testObject2.getFields(); + + expect(testObject1Fields.field1.toConfig()).to.deep.equal( + testObject2Fields.field1.toConfig(), + ); + expect(testObject1Fields.field2.toConfig()).to.deep.equal( + testObject2Fields.field2.toConfig(), + ); expect(outputFields).to.deep.equal({ field1: { type: ScalarType, @@ -263,8 +280,14 @@ describe('Type System: Objects', () => { fields: inputFields, }); - expect(testInputObject1.getFields()).to.deep.equal( - testInputObject2.getFields(), + const testInputObject1Fields = testInputObject1.getFields(); + const testInputObject2Fields = testInputObject2.getFields(); + + expect(testInputObject1Fields.field1.toConfig()).to.deep.equal( + testInputObject2Fields.field1.toConfig(), + ); + expect(testInputObject1Fields.field2.toConfig()).to.deep.equal( + testInputObject2Fields.field2.toConfig(), ); expect(inputFields).to.deep.equal({ field1: { type: ScalarType }, @@ -305,18 +328,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({ + parentType: objType, + name: 'f', + description: undefined, + type: ScalarType, + args: [], + resolve: undefined, + subscribe: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -332,28 +354,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({ + parentType: objType, + 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({ + parent: f, + name: 'arg', + description: undefined, + type: ScalarType, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -506,18 +532,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 +730,36 @@ 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({ + name: 'NULL', + description: undefined, + value: null, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + name: 'NAN', + description: undefined, + value: NaN, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[2]).to.deep.include({ + 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 +854,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({ + parentType: inputObjType, + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, }); }); @@ -846,16 +873,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({ + parentType: inputObjType, + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: undefined, + extensions: {}, + deprecationReason: undefined, + astNode: undefined, }); }); @@ -989,13 +1015,22 @@ describe('Type System: Non-Null', () => { }); describe('Type System: test utility methods', () => { - it('stringifies types', () => { + const someField = ObjectType.getFields().someField; + const someArg = someField.args[0]; + const enumValue = EnumType.getValue('foo'); + const someInputField = InputObjectType.getFields().someInputField; + + it('stringifies schema elements', () => { expect(String(ScalarType)).to.equal('Scalar'); expect(String(ObjectType)).to.equal('Object'); + expect(String(someField)).to.equal('Object.someField'); + expect(String(someArg)).to.equal('Object.someField(someArg:)'); expect(String(InterfaceType)).to.equal('Interface'); expect(String(UnionType)).to.equal('Union'); expect(String(EnumType)).to.equal('Enum'); + expect(String(enumValue)).to.equal('Enum.foo'); expect(String(InputObjectType)).to.equal('InputObject'); + expect(String(someInputField)).to.equal('InputObject.someInputField'); expect(String(NonNullScalarType)).to.equal('Scalar!'); expect(String(ListOfScalarsType)).to.equal('[Scalar]'); @@ -1007,10 +1042,15 @@ describe('Type System: test utility methods', () => { it('JSON.stringifies types', () => { expect(JSON.stringify(ScalarType)).to.equal('"Scalar"'); expect(JSON.stringify(ObjectType)).to.equal('"Object"'); - expect(JSON.stringify(InterfaceType)).to.equal('"Interface"'); + expect(JSON.stringify(someField)).to.equal('"Object.someField"'); + expect(JSON.stringify(someArg)).to.equal('"Object.someField(someArg:)"'); expect(JSON.stringify(UnionType)).to.equal('"Union"'); expect(JSON.stringify(EnumType)).to.equal('"Enum"'); + expect(JSON.stringify(enumValue)).to.equal('"Enum.foo"'); expect(JSON.stringify(InputObjectType)).to.equal('"InputObject"'); + expect(JSON.stringify(someInputField)).to.equal( + '"InputObject.someInputField"', + ); expect(JSON.stringify(NonNullScalarType)).to.equal('"Scalar!"'); expect(JSON.stringify(ListOfScalarsType)).to.equal('"[Scalar]"'); diff --git a/src/type/__tests__/directive-test.ts b/src/type/__tests__/directive-test.ts index 03af5e1fd9..7ce257feaa 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({ + parent: directive, + name: 'foo', + description: undefined, + type: GraphQLString, + defaultValue: undefined, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(directive.args[1]).to.deep.include({ + parent: directive, + 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..f36f7c896d 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({ + parentEnum: ComplexEnum, + name: 'ONE', + description: undefined, + value: Complex1, + deprecationReason: undefined, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + parentEnum: ComplexEnum, + 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..fae37a8870 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -10,8 +10,12 @@ import type { } from '../definition.js'; import { assertAbstractType, + assertArgument, assertCompositeType, assertEnumType, + assertEnumValue, + assertField, + assertInputField, assertInputObjectType, assertInputType, assertInterfaceType, @@ -37,8 +41,12 @@ import { GraphQLScalarType, GraphQLUnionType, isAbstractType, + isArgument, isCompositeType, isEnumType, + isEnumValue, + isField, + isInputField, isInputObjectType, isInputType, isInterfaceType, @@ -75,7 +83,10 @@ import { } from '../scalars.js'; import { assertSchema, GraphQLSchema, isSchema } from '../schema.js'; -const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} }); +const ObjectType = new GraphQLObjectType({ + name: 'Object', + fields: { f: { type: GraphQLString, args: { a: { type: GraphQLString } } } }, +}); const InterfaceType = new GraphQLInterfaceType({ name: 'Interface', fields: {}, @@ -84,7 +95,7 @@ const UnionType = new GraphQLUnionType({ name: 'Union', types: [ObjectType] }); const EnumType = new GraphQLEnumType({ name: 'Enum', values: { foo: {} } }); const InputObjectType = new GraphQLInputObjectType({ name: 'InputObject', - fields: {}, + fields: { f: { type: GraphQLString } }, }); const ScalarType = new GraphQLScalarType({ name: 'Scalar' }); const Directive = new GraphQLDirective({ @@ -184,6 +195,34 @@ describe('Type predicates', () => { }); }); + describe('isField', () => { + it('returns true for fields', () => { + const f = ObjectType.getFields().f; + expect(isField(f)).to.equal(true); + expect(() => assertField(f)).to.not.throw(); + }); + + it('returns false for non-field', () => { + const inputField = InputObjectType.getFields().f; + expect(isField(inputField)).to.equal(false); + expect(() => assertField(inputField)).to.throw(); + }); + }); + + describe('isArgument', () => { + it('returns true for arguments', () => { + const a = ObjectType.getFields().f.args[0]; + expect(isArgument(a)).to.equal(true); + expect(() => assertArgument(a)).to.not.throw(); + }); + + it('returns false for non-arguments', () => { + const f = ObjectType.getFields().f; + expect(isArgument(f)).to.equal(false); + expect(() => assertArgument(f)).to.throw(); + }); + }); + describe('isInterfaceType', () => { it('returns true for interface type', () => { expect(isInterfaceType(InterfaceType)).to.equal(true); @@ -237,6 +276,19 @@ describe('Type predicates', () => { }); }); + describe('isEnumValue', () => { + it('returns true for enum value', () => { + const value = EnumType.getValue('foo'); + expect(isEnumValue(value)).to.equal(true); + expect(() => assertEnumValue(value)).to.not.throw(); + }); + + it('returns false for non-enum type', () => { + expect(isEnumValue(EnumType)).to.equal(false); + expect(() => assertEnumValue(EnumType)).to.throw(); + }); + }); + describe('isInputObjectType', () => { it('returns true for input object type', () => { expect(isInputObjectType(InputObjectType)).to.equal(true); @@ -258,6 +310,20 @@ describe('Type predicates', () => { }); }); + describe('isInputField', () => { + it('returns true for input fields', () => { + const f = InputObjectType.getFields().f; + expect(isInputField(f)).to.equal(true); + expect(() => assertInputField(f)).to.not.throw(); + }); + + it('returns false for non-input fields', () => { + const f = ObjectType.getFields().f; + expect(isInputField(f)).to.equal(false); + expect(() => assertInputField(f)).to.throw(); + }); + }); + describe('isListType', () => { it('returns true for a list wrapped type', () => { expect(isListType(new GraphQLList(ObjectType))).to.equal(true); @@ -570,18 +636,13 @@ 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, - }; + const objectType = new GraphQLObjectType({ + name: 'SomeType', + fields: { + someField: { type: GraphQLString, args: { someArg: config } }, + }, + }); + return objectType.getFields().someField.args[0]; } it('returns true for required arguments', () => { @@ -621,18 +682,13 @@ 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, - }; + const inputObjectType = new GraphQLInputObjectType({ + name: 'SomeType', + fields: { + someInputField: config, + }, + }); + return inputObjectType.getFields().someInputField; } it('returns true for required input field', () => { diff --git a/src/type/definition.ts b/src/type/definition.ts index 81d75f6c6e..c9375b5f1b 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -46,6 +46,7 @@ import type { VariableValues } from '../execution/values.js'; import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped.js'; import { assertEnumValueName, assertName } from './assertName.js'; +import type { GraphQLDirective } from './directives.js'; import type { GraphQLSchema } from './schema.js'; // Predicates & Assertions @@ -76,7 +77,7 @@ export function assertType(type: unknown): GraphQLType { } /** - * There are predicates for each kind of GraphQL type. + * There are predicates for each GraphQL schema element. */ export function isScalarType(type: unknown): type is GraphQLScalarType { return instanceOf(type, GraphQLScalarType); @@ -100,6 +101,28 @@ export function assertObjectType(type: unknown): GraphQLObjectType { return type; } +export function isField(field: unknown): field is GraphQLField { + return instanceOf(field, GraphQLField); +} + +export function assertField(field: unknown): GraphQLField { + if (!isField(field)) { + throw new Error(`Expected ${inspect(field)} to be a GraphQL field.`); + } + return field; +} + +export function isArgument(arg: unknown): arg is GraphQLArgument { + return instanceOf(arg, GraphQLArgument); +} + +export function assertArgument(arg: unknown): GraphQLArgument { + if (!isArgument(arg)) { + throw new Error(`Expected ${inspect(arg)} to be a GraphQL argument.`); + } + return arg; +} + export function isInterfaceType(type: unknown): type is GraphQLInterfaceType { return instanceOf(type, GraphQLInterfaceType); } @@ -135,6 +158,17 @@ export function assertEnumType(type: unknown): GraphQLEnumType { return type; } +export function isEnumValue(value: unknown): value is GraphQLEnumValue { + return instanceOf(value, GraphQLEnumValue); +} + +export function assertEnumValue(value: unknown): GraphQLEnumValue { + if (!isEnumValue(value)) { + throw new Error(`Expected ${inspect(value)} to be a GraphQL Enum value.`); + } + return value; +} + export function isInputObjectType( type: unknown, ): type is GraphQLInputObjectType { @@ -150,6 +184,17 @@ export function assertInputObjectType(type: unknown): GraphQLInputObjectType { return type; } +export function isInputField(field: unknown): field is GraphQLInputField { + return instanceOf(field, GraphQLInputField); +} + +export function assertInputField(field: unknown): GraphQLInputField { + if (!isInputField(field)) { + throw new Error(`Expected ${inspect(field)} to be a GraphQL input field.`); + } + return field; +} + export function isListType( type: GraphQLInputType, ): type is GraphQLList; @@ -320,7 +365,9 @@ export function assertAbstractType(type: unknown): GraphQLAbstractType { * }) * ``` */ -export class GraphQLList { +export class GraphQLList + implements GraphQLSchemaElement +{ readonly ofType: T; constructor(ofType: T) { @@ -361,7 +408,9 @@ export class GraphQLList { * ``` * Note: the enforcement of non-nullability occurs within the executor. */ -export class GraphQLNonNull { +export class GraphQLNonNull + implements GraphQLSchemaElement +{ readonly ofType: T; constructor(ofType: T) { @@ -485,6 +534,15 @@ export function getNamedType( } } +/** + * An interface for all Schema Elements. + */ + +export interface GraphQLSchemaElement { + toString: () => string; + toJSON: () => string; +} + /** * Used while defining GraphQL types to allow for circular references in * otherwise immutable type definitions. @@ -590,7 +648,9 @@ export interface GraphQLScalarTypeExtensions { * `coerceInputLiteral()` method. * */ -export class GraphQLScalarType { +export class GraphQLScalarType + implements GraphQLSchemaElement +{ name: string; description: Maybe; specifiedByURL: Maybe; @@ -806,7 +866,9 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { * }); * ``` */ -export class GraphQLObjectType { +export class GraphQLObjectType + implements GraphQLSchemaElement +{ name: string; description: Maybe; isTypeOf: Maybe>; @@ -826,6 +888,7 @@ export class GraphQLObjectType { this.extensionASTNodes = config.extensionASTNodes ?? []; this._fields = (defineFieldMap).bind( undefined, + this, config.fields, ); this._interfaces = defineInterfaces.bind(undefined, config.interfaces); @@ -854,7 +917,7 @@ 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, @@ -878,73 +941,17 @@ function defineInterfaces( } function defineFieldMap( + parentType: + | GraphQLObjectType + | GraphQLInterfaceType, 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, -): GraphQLFieldNormalizedConfigMap { - 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, -): GraphQLFieldNormalizedConfigArgumentMap { - 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(parentType, fieldName, fieldConfig), ); } @@ -1081,7 +1088,13 @@ export type GraphQLFieldNormalizedConfigMap = ObjMap< GraphQLFieldNormalizedConfig >; -export interface GraphQLField { +export class GraphQLField + implements GraphQLSchemaElement +{ + parentType: + | GraphQLObjectType + | GraphQLInterfaceType + | undefined; name: string; description: Maybe; type: GraphQLOutputType; @@ -1091,9 +1104,67 @@ export interface GraphQLField { deprecationReason: Maybe; extensions: Readonly>; astNode: Maybe; + + constructor( + parentType: + | GraphQLObjectType + | GraphQLInterfaceType + | undefined, + name: string, + config: GraphQLFieldConfig, + ) { + this.parentType = parentType; + 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(this, argName, argConfig), + ) + : []; + + this.resolve = config.resolve; + this.subscribe = config.subscribe; + this.deprecationReason = config.deprecationReason; + this.extensions = toObjMapWithSymbols(config.extensions); + this.astNode = config.astNode; + } + + get [Symbol.toStringTag]() { + return 'GraphQLField'; + } + + toConfig(): GraphQLFieldNormalizedConfig { + 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, + }; + } + + toString(): string { + return `${this.parentType ?? ''}.${this.name}`; + } + + toJSON(): string { + return this.toString(); + } } -export interface GraphQLArgument { +export class GraphQLArgument implements GraphQLSchemaElement { + parent: GraphQLField | GraphQLDirective; name: string; description: Maybe; type: GraphQLInputType; @@ -1101,6 +1172,45 @@ export interface GraphQLArgument { deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parent: GraphQLField | GraphQLDirective, + name: string, + config: GraphQLArgumentConfig, + ) { + this.parent = parent; + 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; + } + + get [Symbol.toStringTag]() { + return 'GraphQLArgument'; + } + + toConfig(): GraphQLArgumentNormalizedConfig { + return { + description: this.description, + type: this.type, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } + + toString(): string { + return `${this.parent}(${this.name}:)`; + } + + toJSON(): string { + return this.toString(); + } } export function isRequiredArgument( @@ -1165,7 +1275,9 @@ export interface GraphQLInterfaceTypeExtensions { * }); * ``` */ -export class GraphQLInterfaceType { +export class GraphQLInterfaceType + implements GraphQLSchemaElement +{ name: string; description: Maybe; resolveType: Maybe>; @@ -1185,6 +1297,7 @@ export class GraphQLInterfaceType { this.extensionASTNodes = config.extensionASTNodes ?? []; this._fields = (defineFieldMap).bind( undefined, + this, config.fields, ); this._interfaces = defineInterfaces.bind(undefined, config.interfaces); @@ -1213,7 +1326,7 @@ 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, @@ -1291,7 +1404,7 @@ export interface GraphQLUnionTypeExtensions { * }); * ``` */ -export class GraphQLUnionType { +export class GraphQLUnionType implements GraphQLSchemaElement { name: string; description: Maybe; resolveType: Maybe>; @@ -1385,17 +1498,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 * @@ -1419,7 +1521,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 /* */ implements GraphQLSchemaElement { name: string; description: Maybe; extensions: Readonly; @@ -1443,7 +1545,10 @@ export class GraphQLEnumType /* */ { this._values = typeof config.values === 'function' ? config.values - : enumValuesFromConfig(config.values); + : Object.entries(config.values).map( + ([valueName, valueConfig]) => + new GraphQLEnumValue(this, valueName, valueConfig), + ); this._valueLookup = null; this._nameLookup = null; } @@ -1454,7 +1559,10 @@ export class 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, valueName, valueConfig), + ); } return this._values; } @@ -1561,22 +1669,14 @@ 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, @@ -1649,13 +1749,50 @@ export interface GraphQLEnumValueNormalizedConfig extensions: Readonly; } -export interface GraphQLEnumValue { +export class GraphQLEnumValue implements GraphQLSchemaElement { + parentEnum: GraphQLEnumType; name: string; description: Maybe; value: any /* T */; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parentEnum: GraphQLEnumType, + name: string, + config: GraphQLEnumValueConfig, + ) { + this.parentEnum = parentEnum; + 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; + } + + get [Symbol.toStringTag]() { + return 'GraphQLEnumValue'; + } + + toConfig(): GraphQLEnumValueNormalizedConfig { + return { + description: this.description, + value: this.value, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } + + toString(): string { + return `${this.parentEnum.name}.${this.name}`; + } + + toJSON(): string { + return this.toString(); + } } /** @@ -1692,7 +1829,7 @@ export interface GraphQLInputObjectTypeExtensions { * }); * ``` */ -export class GraphQLInputObjectType { +export class GraphQLInputObjectType implements GraphQLSchemaElement { name: string; description: Maybe; extensions: Readonly; @@ -1710,7 +1847,7 @@ 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, config.fields); } get [Symbol.toStringTag]() { @@ -1725,20 +1862,10 @@ 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, @@ -1756,18 +1883,15 @@ export class GraphQLInputObjectType { } function defineInputFieldMap( + parentType: GraphQLInputObjectType, 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(parentType, fieldName, fieldConfig), + ); } export interface GraphQLInputObjectTypeConfig { @@ -1820,7 +1944,8 @@ export interface GraphQLInputFieldNormalizedConfig export type GraphQLInputFieldNormalizedConfigMap = ObjMap; -export interface GraphQLInputField { +export class GraphQLInputField implements GraphQLSchemaElement { + parentType: GraphQLInputObjectType; name: string; description: Maybe; type: GraphQLInputType; @@ -1828,6 +1953,50 @@ export interface GraphQLInputField { deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + + constructor( + parentType: GraphQLInputObjectType, + name: string, + config: GraphQLInputFieldConfig, + ) { + devAssert( + !('resolve' in config), + `${parentType}.${name} field has a resolve property, but Input Types cannot define resolvers.`, + ); + + this.parentType = parentType; + 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; + } + + get [Symbol.toStringTag]() { + return 'GraphQLInputField'; + } + + toConfig(): GraphQLInputFieldNormalizedConfig { + return { + description: this.description, + type: this.type, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, + deprecationReason: this.deprecationReason, + extensions: this.extensions, + astNode: this.astNode, + }; + } + + toString(): string { + return `${this.parentType}.${this.name}`; + } + + toJSON(): string { + return this.toString(); + } } export function isRequiredInputField(field: GraphQLInputField): boolean { diff --git a/src/type/directives.ts b/src/type/directives.ts index fc1ebba9eb..65b03e2bd9 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,6 +1,10 @@ +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'; @@ -8,15 +12,11 @@ import { DirectiveLocation } from '../language/directiveLocation.js'; import { assertName } from './assertName.js'; import type { - GraphQLArgument, - GraphQLFieldConfigArgumentMap, + GraphQLArgumentConfig, GraphQLFieldNormalizedConfigArgumentMap, + GraphQLSchemaElement, } from './definition.js'; -import { - argsToArgsConfig, - defineArguments, - GraphQLNonNull, -} from './definition.js'; +import { GraphQLArgument, GraphQLNonNull } from './definition.js'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from './scalars.js'; /** @@ -52,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 implements GraphQLSchemaElement { name: string; description: Maybe; locations: ReadonlyArray; @@ -69,8 +69,20 @@ export class GraphQLDirective { this.extensions = toObjMapWithSymbols(config.extensions); this.astNode = config.astNode; + devAssert( + Array.isArray(config.locations), + `@${this.name} locations must be an Array.`, + ); + const args = config.args ?? {}; - this.args = defineArguments(args); + devAssert( + isObjectLike(args) && !Array.isArray(args), + `@${this.name} args must be an object with argument names as keys.`, + ); + + this.args = Object.entries(args).map( + ([argName, argConfig]) => new GraphQLArgument(this, argName, argConfig), + ); } get [Symbol.toStringTag]() { @@ -82,7 +94,11 @@ 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, @@ -102,7 +118,7 @@ export interface GraphQLDirectiveConfig { name: string; description?: Maybe; locations: ReadonlyArray; - args?: Maybe; + args?: Maybe>; isRepeatable?: Maybe; extensions?: Maybe>; astNode?: Maybe; diff --git a/src/type/index.ts b/src/type/index.ts index 25b59d6177..0db35606f8 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -10,20 +10,29 @@ export { } from './schema.js'; export type { GraphQLSchemaConfig, GraphQLSchemaExtensions } from './schema.js'; +export type { + GraphQLField, + GraphQLArgument, + GraphQLEnumValue, + GraphQLInputField, +} from './definition.js'; export { resolveObjMapThunk, resolveReadonlyArrayThunk, - // Predicates isType, isScalarType, isObjectType, + isField, + isArgument, isInterfaceType, isUnionType, isEnumType, + isEnumValue, isInputObjectType, isListType, isNonNullType, isInputType, + isInputField, isOutputType, isLeafType, isCompositeType, @@ -33,14 +42,17 @@ export { isNamedType, isRequiredArgument, isRequiredInputField, - // Assertions assertType, assertScalarType, assertObjectType, + assertField, + assertArgument, assertInterfaceType, assertUnionType, assertEnumType, + assertEnumValue, assertInputObjectType, + assertInputField, assertListType, assertNonNullType, assertInputType, @@ -51,17 +63,14 @@ export { assertWrappingType, assertNullableType, assertNamedType, - // Un-modifiers getNullableType, getNamedType, - // Definitions GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, - // Type Wrappers GraphQLList, GraphQLNonNull, } from './definition.js'; @@ -82,23 +91,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..f6cae47f36 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(undefined, '__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(undefined, '__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(undefined, '__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..00d99a0c8a 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -377,32 +377,35 @@ 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({ + name: 'VEGETABLES', + description: 'Foods that are vegetables.', + value: 'VEGETABLES', + deprecationReason: null, + extensions: {}, + astNode: undefined, + }); + + expect(values[1]).to.deep.include({ + name: 'FRUITS', + description: null, + value: 'FRUITS', + deprecationReason: null, + extensions: {}, + astNode: undefined, + }); + + expect(values[2]).to.deep.include({ + 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/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/printSchema.ts b/src/utilities/printSchema.ts index 52fc342626..a9883464fd 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -258,7 +258,7 @@ function printArgs( ); } -function printInputValue(arg: GraphQLInputField): string { +function printInputValue(arg: GraphQLArgument | GraphQLInputField): string { let argDecl = arg.name + ': ' + String(arg.type); if (arg.defaultValue) { const literal = 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..32ecafdabb 100644 --- a/src/validation/rules/ProvidedRequiredArgumentsRule.ts +++ b/src/validation/rules/ProvidedRequiredArgumentsRule.ts @@ -11,13 +11,8 @@ import { print } from '../../language/printer.js'; import type { ASTVisitor } from '../../language/visitor.js'; import type { GraphQLArgument } from '../../type/definition.js'; -import { - getNamedType, - isRequiredArgument, - isType, -} from '../../type/definition.js'; +import { 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 +46,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 }, ), ); From ecbaa785aa8c6e9500ea13287c411f393a5d0de2 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 1 Dec 2024 15:06:33 +0200 Subject: [PATCH 2/3] reduce diff --- src/index.ts | 13 +++++++++++++ src/type/index.ts | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/index.ts b/src/index.ts index 55b58b3f01..c3deb51ffe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,7 @@ export type { export { resolveObjMapThunk, resolveReadonlyArrayThunk, + // Definitions GraphQLSchema, GraphQLDirective, GraphQLScalarType, @@ -53,14 +54,17 @@ export { GraphQLInputObjectType, GraphQLList, GraphQLNonNull, + // Standard GraphQL Scalars specifiedScalarTypes, GraphQLInt, GraphQLFloat, GraphQLString, GraphQLBoolean, GraphQLID, + // Int boundaries constants GRAPHQL_MAX_INT, GRAPHQL_MIN_INT, + // Built-in Directives defined by the Spec specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, @@ -69,8 +73,11 @@ export { GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, GraphQLOneOfDirective, + // "Enum" of Type Kinds TypeKind, + // Constant Deprecation Reason DEFAULT_DEPRECATION_REASON, + // GraphQL Types for introspection. introspectionTypes, __Schema, __Directive, @@ -80,9 +87,11 @@ export { __InputValue, __EnumValue, __TypeKind, + // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, + // Predicates isSchema, isDirective, isType, @@ -111,6 +120,7 @@ export { isSpecifiedScalarType, isIntrospectionType, isSpecifiedDirective, + // Assertions assertSchema, assertDirective, assertType, @@ -134,10 +144,13 @@ export { assertWrappingType, assertNullableType, assertNamedType, + // Un-modifiers getNullableType, getNamedType, + // Validate GraphQL schema. validateSchema, assertValidSchema, + // Upholds the spec rules about naming. assertName, assertEnumValueName, } from './type/index.js'; diff --git a/src/type/index.ts b/src/type/index.ts index 0db35606f8..ddb209e647 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -19,6 +19,7 @@ export type { export { resolveObjMapThunk, resolveReadonlyArrayThunk, + // Predicates isType, isScalarType, isObjectType, @@ -42,6 +43,7 @@ export { isNamedType, isRequiredArgument, isRequiredInputField, + // Assertions assertType, assertScalarType, assertObjectType, @@ -63,14 +65,17 @@ export { assertWrappingType, assertNullableType, assertNamedType, + // Un-modifiers getNullableType, getNamedType, + // Definitions GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType, + // Type Wrappers GraphQLList, GraphQLNonNull, } from './definition.js'; From 646d600fe14cd3fe5e9eebc6c5a2f525d39b0c16 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 1 Dec 2024 16:32:18 +0200 Subject: [PATCH 3/3] review feedback Co-authored-by: Jovi De Croock --- src/execution/values.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/execution/values.ts b/src/execution/values.ts index adecc0fa35..25c8ef6fd5 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -226,6 +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( + // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged `Argument "${isArgument(argDef) ? argDef : argDef.name}" of required type "${argType}" was not provided.`, { nodes: node }, ); @@ -276,6 +277,7 @@ export function experimentalGetArgumentValues( valueNode, argType, (error, path) => { + // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged error.message = `Argument "${isArgument(argDef) ? argDef : argDef.name}" has invalid value${printPathArray( path, )}: ${error.message}`;