diff --git a/.changeset/fix-is-using-types-union-and-interface.md b/.changeset/fix-is-using-types-union-and-interface.md new file mode 100644 index 00000000000..7083269342f --- /dev/null +++ b/.changeset/fix-is-using-types-union-and-interface.md @@ -0,0 +1,5 @@ +--- +"@graphql-codegen/plugin-helpers": patch +--- + +fix(plugin-helpers): guard `isUsingTypes` against union types and properly resolve interface fields diff --git a/packages/utils/plugins-helpers/src/helpers.ts b/packages/utils/plugins-helpers/src/helpers.ts index 2050547189b..27515247870 100644 --- a/packages/utils/plugins-helpers/src/helpers.ts +++ b/packages/utils/plugins-helpers/src/helpers.ts @@ -2,10 +2,11 @@ import { ASTNode, DocumentNode, FieldNode, - GraphQLObjectType, + GraphQLNamedType, GraphQLOutputType, GraphQLSchema, InputValueDefinitionNode, + isInterfaceType, isListType, isNonNullType, isObjectType, @@ -89,7 +90,7 @@ export function isUsingTypes( schema?: GraphQLSchema, ): boolean { let foundFields = 0; - const typesStack: GraphQLObjectType[] = []; + const typesStack: Array = []; visit(document, { SelectionSet: { @@ -124,15 +125,20 @@ export function isUsingTypes( `Unable to find parent type! Please make sure you operation passes validation`, ); } - const field = lastType.getFields()[parent.name.value]; - if (!field) { - throw new Error( - `Unable to find field "${parent.name.value}" on type "${lastType}"!`, - ); + if (isObjectType(lastType) || isInterfaceType(lastType)) { + const field = lastType.getFields()[parent.name.value]; + + if (!field) { + throw new Error( + `Unable to find field "${parent.name.value}" on type "${lastType}"!`, + ); + } + + return getBaseType(field.type).name; } - return getBaseType(field.type).name; + return null; } if (parent.kind === Kind.OPERATION_DEFINITION) { if (parent.operation === 'query') { @@ -164,18 +170,13 @@ export function isUsingTypes( if (parent.typeCondition) { return parent.typeCondition.name.value; } - return typesStack[typesStack.length - 1].name; + return typesStack[typesStack.length - 1]?.name || null; } return null; })(); - // `as any` is needed because `typesStack` is typed `GraphQLObjectType[]`, but `schema.getType()` - // returns `GraphQLNamedType | undefined`. Removing it is a small follow-up: - // type `typesStack` as `Array`, then - // 1. in the `Kind.FIELD` branch, guard `lastType` with `isObjectType`/`isInterfaceType` before `getFields()` - // 2. in the `Kind.INLINE_FRAGMENT` branch, return `typesStack[typesStack.length - 1]?.name || null` - typesStack.push((nextTypeName ? schema.getType(nextTypeName) : undefined) as any); + typesStack.push(nextTypeName ? schema.getType(nextTypeName) : undefined); return node; } @@ -219,7 +220,7 @@ export function isUsingTypes( if (schema) { const lastType = typesStack[typesStack.length - 1]; - if (lastType && isObjectType(lastType)) { + if (lastType && (isObjectType(lastType) || isInterfaceType(lastType))) { const field = lastType.getFields()[node.name.value]; if (!field) { diff --git a/packages/utils/plugins-helpers/tests/is-using-types.spec.ts b/packages/utils/plugins-helpers/tests/is-using-types.spec.ts index 7da3e2dd2c7..1faf9fdc451 100644 --- a/packages/utils/plugins-helpers/tests/is-using-types.spec.ts +++ b/packages/utils/plugins-helpers/tests/is-using-types.spec.ts @@ -186,6 +186,98 @@ describe('isUsingTypes', () => { expect(isUsingTypes(ast, [], schema)).toBeTruthy(); }); + + it('community #934 - should not crash when selecting fields on union type', () => { + const schema = buildSchema(/* GraphQL */ ` + type A { + id: Int! + } + + type B { + id: Int! + fieldOnlyOnB: String + } + + union AorB = B | A + + type Query { + getABList: [AorB!]! + } + `); + + const ast = parse(/* GraphQL */ ` + query { + getABList { + ... on B { + fieldOnlyOnB + } + invalidField { + invalidSubField + } + } + } + `); + + expect(() => isUsingTypes(ast, [], schema)).not.toThrow(); + expect(isUsingTypes(ast, [], schema)).toBeTruthy(); + }); + + it('should correctly detect types and nullable fields on interface types', () => { + const schema = buildSchema(/* GraphQL */ ` + interface Node { + id: ID! + nullableField: String + } + + type Query { + node: Node! + } + `); + + const ast = parse(/* GraphQL */ ` + query GetNode { + node { + id + nullableField + } + } + `); + + expect(isUsingTypes(ast, [], schema)).toBeTruthy(); + }); + + it('should handle untyped inline fragments on unions without error', () => { + const schema = buildSchema(/* GraphQL */ ` + type A { + id: ID! + } + + type B { + id: ID! + name: String + } + + union SearchResult = A | B + + type Query { + search: [SearchResult!]! + } + `); + + const ast = parse(/* GraphQL */ ` + query Search($includeExtra: Boolean!) { + search { + ... @include(if: $includeExtra) { + ... on B { + name + } + } + } + } + `); + + expect(isUsingTypes(ast, [], schema)).toBeTruthy(); + }); }); it('Should work with __typename on fragments', () => {