diff --git a/src/operation.ts b/src/operation.ts index 47aed551..29bb2722 100644 --- a/src/operation.ts +++ b/src/operation.ts @@ -127,6 +127,7 @@ function buildDocumentNode({ models, firstCall: true, path: [], + ancestors: [], ignore, }), ], @@ -146,12 +147,14 @@ function resolveSelectionSet({ models, firstCall, path, + ancestors, ignore, }: { parent: GraphQLNamedType; type: GraphQLNamedType; models: string[]; path: string[]; + ancestors: GraphQLNamedType[]; firstCall?: boolean; ignore: Ignore; }): SelectionSetNode | undefined { @@ -160,32 +163,35 @@ function resolveSelectionSet({ return { kind: 'SelectionSet', - selections: types.map(t => { - const fields = t.getFields(); - - return { - kind: 'InlineFragment', - typeCondition: { - kind: 'NamedType', - name: { - kind: 'Name', - value: t.name, + selections: types + .filter(t => !hasCircularRef([...ancestors, t])) + .map(t => { + const fields = t.getFields(); + + return { + kind: 'InlineFragment', + typeCondition: { + kind: 'NamedType', + name: { + kind: 'Name', + value: t.name, + }, }, - }, - selectionSet: { - kind: 'SelectionSet', - selections: Object.keys(fields).map(fieldName => { - return resolveField({ - type: t, - field: fields[fieldName], - models, - path: [...path, fieldName], - ignore, - }); - }), - }, - }; - }), + selectionSet: { + kind: 'SelectionSet', + selections: Object.keys(fields).map(fieldName => { + return resolveField({ + type: t, + field: fields[fieldName], + models, + path: [...path, fieldName], + ancestors, + ignore, + }); + }), + }, + }; + }), }; } @@ -214,15 +220,24 @@ function resolveSelectionSet({ return { kind: 'SelectionSet', - selections: Object.keys(fields).map(fieldName => { - return resolveField({ - type: type, - field: fields[fieldName], - models, - path: [...path, fieldName], - ignore, - }); - }), + selections: Object.keys(fields) + .filter( + fieldName => + !hasCircularRef([ + ...ancestors, + getNamedType(fields[fieldName].type), + ]) + ) + .map(fieldName => { + return resolveField({ + type: type, + field: fields[fieldName], + models, + path: [...path, fieldName], + ancestors, + ignore, + }); + }), }; } } @@ -281,12 +296,14 @@ function resolveField({ models, firstCall, path, + ancestors, ignore, }: { type: GraphQLObjectType; field: GraphQLField; models: string[]; path: string[]; + ancestors: GraphQLNamedType[]; firstCall?: boolean; ignore: Ignore; }): SelectionNode { @@ -331,6 +348,7 @@ function resolveField({ models, firstCall, path: [...path, field.name], + ancestors: [...ancestors, type], ignore, }), arguments: args, @@ -346,3 +364,7 @@ function resolveField({ arguments: args, }; } + +function hasCircularRef(types: GraphQLNamedType[]): boolean { + return types.some((t, i) => types.indexOf(t) !== i); +} diff --git a/tests/operation.spec.ts b/tests/operation.spec.ts index 85f222fb..f515b11e 100644 --- a/tests/operation.spec.ts +++ b/tests/operation.spec.ts @@ -1,4 +1,4 @@ -import { print, parse, DocumentNode } from 'graphql'; +import { print, parse, DocumentNode, buildSchema } from 'graphql'; import gql from 'graphql-tag'; import { schema, models } from './schema'; @@ -273,3 +273,44 @@ test('should work with Subscription', async () => { `) ); }); + +test('should work with circular ref', async () => { + const document = buildOperation({ + schema: buildSchema(` + type A { + b: B + } + + type B { + c: C + } + + type C { + end: String + a: A + } + + type Query { + a: A + } + `), + kind: 'query', + field: 'a', + models, + ignore: [], + })!; + + expect(clean(document)).toEqual( + clean(` + query aQuery { + a { + b { + c { + end + } + } + } + } + `) + ); +});