Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-is-using-types-union-and-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-codegen/plugin-helpers": patch
---

fix(plugin-helpers): guard `isUsingTypes` against union types and properly resolve interface fields
33 changes: 17 additions & 16 deletions packages/utils/plugins-helpers/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import {
ASTNode,
DocumentNode,
FieldNode,
GraphQLObjectType,
GraphQLNamedType,
GraphQLOutputType,
GraphQLSchema,
InputValueDefinitionNode,
isInterfaceType,
isListType,
isNonNullType,
isObjectType,
Expand Down Expand Up @@ -89,7 +90,7 @@ export function isUsingTypes(
schema?: GraphQLSchema,
): boolean {
let foundFields = 0;
const typesStack: GraphQLObjectType[] = [];
const typesStack: Array<GraphQLNamedType | undefined> = [];

visit(document, {
SelectionSet: {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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<GraphQLNamedType | undefined>`, 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;
}
Expand Down Expand Up @@ -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) {
Expand Down
92 changes: 92 additions & 0 deletions packages/utils/plugins-helpers/tests/is-using-types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down