Skip to content

Commit a30daec

Browse files
committed
Allow interface resolveType functions to resolve to child interfaces
= The child interfaces must eventually resolve to a runtime object type. = Interface cycles raise a runtime error.
1 parent 540bb38 commit a30daec

File tree

2 files changed

+207
-32
lines changed

2 files changed

+207
-32
lines changed

src/execution/__tests__/abstract-test.ts

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe('Execute: Handles execution of abstract types', () => {
271271
errors: [
272272
{
273273
message:
274-
'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.',
274+
'Abstract type resolution for "Pet" for field "Query.pet" failed. Encountered abstract type "Pet" must resolve to an Object or Interface type at runtime. Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.',
275275
locations: [{ line: 3, column: 9 }],
276276
path: ['pet'],
277277
},
@@ -610,26 +610,26 @@ describe('Execute: Handles execution of abstract types', () => {
610610
}
611611

612612
expectError({ forTypeName: undefined }).toEqual(
613-
'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet". Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.',
613+
'Abstract type resolution for "Pet" for field "Query.pet" failed. Encountered abstract type "Pet" must resolve to an Object or Interface type at runtime. Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.',
614614
);
615615

616616
expectError({ forTypeName: 'Human' }).toEqual(
617-
'Abstract type "Pet" was resolved to a type "Human" that does not exist inside the schema.',
617+
'Abstract type resolution for "Pet" for field "Query.pet" failed. Encountered abstract type "Pet" was resolved to a type "Human" that does not exist inside the schema.',
618618
);
619619

620620
expectError({ forTypeName: 'String' }).toEqual(
621-
'Abstract type "Pet" was resolved to a non-object type "String".',
621+
'Abstract type resolution for "Pet" for field "Query.pet" failed. Encountered abstract type "Pet" was resolved to a non-object type "String".',
622622
);
623623

624624
expectError({ forTypeName: '__Schema' }).toEqual(
625-
'Runtime Object type "__Schema" is not a possible type for "Pet".',
625+
'Abstract type resolution for "Pet" for field "Query.pet" failed. Runtime Object type "__Schema" is not a possible type for encountered abstract type "Pet".',
626626
);
627627

628628
// FIXME: workaround since we can't inject resolveType into SDL
629629
// @ts-expect-error
630630
assertInterfaceType(schema.getType('Pet')).resolveType = () => [];
631631
expectError({ forTypeName: undefined }).toEqual(
632-
'Abstract type "Pet" must resolve to an Object type at runtime for field "Query.pet" with value { __typename: undefined }, received "[]".',
632+
'Abstract type resolution for "Pet" for field "Query.pet" with value { __typename: undefined } failed. Encountered abstract type "Pet" must resolve to an Object or Interface type at runtime, received "[]".',
633633
);
634634

635635
// FIXME: workaround since we can't inject resolveType into SDL
@@ -640,4 +640,106 @@ describe('Execute: Handles execution of abstract types', () => {
640640
'Support for returning GraphQLObjectType from resolveType was removed in [email protected] please return type name instead.',
641641
);
642642
});
643+
644+
it('hierarchical resolveType with Interfaces yields useful error', () => {
645+
const schema = buildSchema(`
646+
type Query {
647+
named: Named
648+
}
649+
650+
interface Named {
651+
name: String
652+
}
653+
654+
interface Animal {
655+
isFriendly: Boolean
656+
}
657+
658+
interface Pet implements Animal & Named {
659+
name: String
660+
isFriendly: Boolean
661+
}
662+
663+
type Cat implements Pet & Named & Animal {
664+
name: String
665+
isFriendly: Boolean
666+
}
667+
668+
type Dog implements Pet & Animal & Named {
669+
name: String
670+
isFriendly: Boolean
671+
}
672+
`);
673+
674+
const document = parse(`
675+
{
676+
named {
677+
name
678+
}
679+
}
680+
`);
681+
682+
function expectError() {
683+
const rootValue = { named: {} };
684+
const result = executeSync({ schema, document, rootValue });
685+
return {
686+
toEqual(message: string) {
687+
expectJSON(result).toDeepEqual({
688+
data: { named: null },
689+
errors: [
690+
{
691+
message,
692+
locations: [{ line: 3, column: 9 }],
693+
path: ['named'],
694+
},
695+
],
696+
});
697+
},
698+
};
699+
}
700+
701+
// FIXME: workaround since we can't inject resolveType into SDL
702+
assertInterfaceType(schema.getType('Named')).resolveType = () => 'Animal';
703+
expectError().toEqual(
704+
'Abstract type resolution for "Named" for field "Query.named" failed. Interface type "Animal" is not a subtype of encountered interface type "Named".',
705+
);
706+
707+
// FIXME: workaround since we can't inject resolveType into SDL
708+
assertInterfaceType(schema.getType('Named')).resolveType = () => 'Pet';
709+
assertInterfaceType(schema.getType('Pet')).resolveType = () => undefined;
710+
expectError().toEqual(
711+
'Abstract type resolution for "Named" for field "Query.named" failed. Encountered abstract type "Pet" must resolve to an Object or Interface type at runtime. Either the "Pet" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.',
712+
);
713+
714+
// FIXME: workaround since we can't inject resolveType into SDL
715+
assertInterfaceType(schema.getType('Pet')).resolveType = () => 'Human';
716+
expectError().toEqual(
717+
'Abstract type resolution for "Named" for field "Query.named" failed. Encountered abstract type "Pet" was resolved to a type "Human" that does not exist inside the schema.',
718+
);
719+
720+
// FIXME: workaround since we can't inject resolveType into SDL
721+
assertInterfaceType(schema.getType('Pet')).resolveType = () => 'String';
722+
expectError().toEqual(
723+
'Abstract type resolution for "Named" for field "Query.named" failed. Encountered abstract type "Pet" was resolved to a non-object type "String".',
724+
);
725+
726+
// FIXME: workaround since we can't inject resolveType into SDL
727+
assertInterfaceType(schema.getType('Pet')).resolveType = () => '__Schema';
728+
expectError().toEqual(
729+
'Abstract type resolution for "Named" for field "Query.named" failed. Runtime Object type "__Schema" is not a possible type for encountered abstract type "Pet".',
730+
);
731+
732+
// FIXME: workaround since we can't inject resolveType into SDL
733+
// @ts-expect-error
734+
assertInterfaceType(schema.getType('Pet')).resolveType = () => [];
735+
expectError().toEqual(
736+
'Abstract type resolution for "Named" for field "Query.named" with value {} failed. Encountered abstract type "Pet" must resolve to an Object or Interface type at runtime, received "[]".',
737+
);
738+
739+
// FIXME: workaround since we can't inject resolveType into SDL
740+
assertInterfaceType(schema.getType('Pet')).resolveType = () => 'Pet';
741+
expectError().toEqual(
742+
'Abstract type resolution for "Named" for field "Query.named" failed. Encountered abstract type "Pet" resolved to "Pet", causing a cycle.',
743+
);
744+
});
643745
});

src/execution/execute.ts

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
} from '../type/definition';
4040
import {
4141
isAbstractType,
42+
isInterfaceType,
4243
isLeafType,
4344
isListType,
4445
isNonNullType,
@@ -796,22 +797,20 @@ function completeAbstractValue(
796797
path: Path,
797798
result: unknown,
798799
): PromiseOrValue<ObjMap<unknown>> {
799-
const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver;
800-
const contextValue = exeContext.contextValue;
801-
const runtimeType = resolveTypeFn(result, contextValue, info, returnType);
800+
const runtimeType = resolveType(
801+
exeContext,
802+
returnType,
803+
returnType,
804+
fieldNodes,
805+
info,
806+
result,
807+
);
802808

803809
if (isPromise(runtimeType)) {
804810
return runtimeType.then((resolvedRuntimeType) =>
805811
completeObjectValue(
806812
exeContext,
807-
ensureValidRuntimeType(
808-
resolvedRuntimeType,
809-
exeContext,
810-
returnType,
811-
fieldNodes,
812-
info,
813-
result,
814-
),
813+
resolvedRuntimeType,
815814
fieldNodes,
816815
info,
817816
path,
@@ -822,32 +821,73 @@ function completeAbstractValue(
822821

823822
return completeObjectValue(
824823
exeContext,
825-
ensureValidRuntimeType(
826-
runtimeType,
827-
exeContext,
828-
returnType,
829-
fieldNodes,
830-
info,
831-
result,
832-
),
824+
runtimeType,
833825
fieldNodes,
834826
info,
835827
path,
836828
result,
837829
);
838830
}
839831

832+
function resolveType(
833+
exeContext: ExecutionContext,
834+
returnType: GraphQLAbstractType,
835+
abstractType: GraphQLAbstractType,
836+
fieldNodes: ReadonlyArray<FieldNode>,
837+
info: GraphQLResolveInfo,
838+
result: unknown,
839+
encounteredTypeNames: Set<string> = new Set(),
840+
): GraphQLObjectType | Promise<GraphQLObjectType> {
841+
const resolveTypeFn = abstractType.resolveType ?? exeContext.typeResolver;
842+
const contextValue = exeContext.contextValue;
843+
const possibleRuntimeType = resolveTypeFn(
844+
result,
845+
contextValue,
846+
info,
847+
abstractType,
848+
);
849+
850+
if (isPromise(possibleRuntimeType)) {
851+
return possibleRuntimeType.then((resolvedPossibleRuntimeType) =>
852+
ensureValidRuntimeType(
853+
resolvedPossibleRuntimeType,
854+
exeContext,
855+
returnType,
856+
abstractType,
857+
fieldNodes,
858+
info,
859+
result,
860+
encounteredTypeNames,
861+
),
862+
);
863+
}
864+
return ensureValidRuntimeType(
865+
possibleRuntimeType,
866+
exeContext,
867+
returnType,
868+
abstractType,
869+
fieldNodes,
870+
info,
871+
result,
872+
encounteredTypeNames,
873+
);
874+
}
875+
840876
function ensureValidRuntimeType(
841877
runtimeTypeName: unknown,
842878
exeContext: ExecutionContext,
843879
returnType: GraphQLAbstractType,
880+
abstractType: GraphQLAbstractType,
844881
fieldNodes: ReadonlyArray<FieldNode>,
845882
info: GraphQLResolveInfo,
846883
result: unknown,
847-
): GraphQLObjectType {
884+
encounteredTypeNames: Set<string>,
885+
): GraphQLObjectType | Promise<GraphQLObjectType> {
848886
if (runtimeTypeName == null) {
849887
throw new GraphQLError(
850-
`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`,
888+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
889+
`Encountered abstract type "${abstractType.name}" must resolve to an Object or Interface type at runtime. ` +
890+
`Either the "${abstractType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`,
851891
{ nodes: fieldNodes },
852892
);
853893
}
@@ -862,29 +902,62 @@ function ensureValidRuntimeType(
862902

863903
if (typeof runtimeTypeName !== 'string') {
864904
throw new GraphQLError(
865-
`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` +
866-
`value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`,
905+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" ` +
906+
`with value ${inspect(result)} failed. ` +
907+
`Encountered abstract type "${abstractType.name}" must resolve to an Object or Interface type at runtime, ` +
908+
`received "${inspect(runtimeTypeName)}".`,
909+
);
910+
}
911+
912+
if (encounteredTypeNames.has(runtimeTypeName)) {
913+
throw new GraphQLError(
914+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
915+
`Encountered abstract type "${abstractType.name}" resolved to "${runtimeTypeName}", causing a cycle.`,
867916
);
868917
}
918+
encounteredTypeNames.add(runtimeTypeName);
869919

870920
const runtimeType = exeContext.schema.getType(runtimeTypeName);
871921
if (runtimeType == null) {
872922
throw new GraphQLError(
873-
`Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`,
923+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
924+
`Encountered abstract type "${abstractType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`,
874925
{ nodes: fieldNodes },
875926
);
876927
}
877928

929+
if (isInterfaceType(runtimeType)) {
930+
if (!exeContext.schema.isSubType(returnType, runtimeType)) {
931+
throw new GraphQLError(
932+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
933+
`Interface type "${runtimeType.name}" is not a subtype of encountered interface type "${returnType.name}".`,
934+
{ nodes: fieldNodes },
935+
);
936+
}
937+
938+
return resolveType(
939+
exeContext,
940+
returnType,
941+
runtimeType,
942+
fieldNodes,
943+
info,
944+
result,
945+
encounteredTypeNames,
946+
);
947+
}
948+
878949
if (!isObjectType(runtimeType)) {
879950
throw new GraphQLError(
880-
`Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`,
951+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
952+
`Encountered abstract type "${abstractType.name}" was resolved to a non-object type "${runtimeTypeName}".`,
881953
{ nodes: fieldNodes },
882954
);
883955
}
884956

885957
if (!exeContext.schema.isSubType(returnType, runtimeType)) {
886958
throw new GraphQLError(
887-
`Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`,
959+
`Abstract type resolution for "${returnType.name}" for field "${info.parentType.name}.${info.fieldName}" failed. ` +
960+
`Runtime Object type "${runtimeType.name}" is not a possible type for encountered abstract type "${abstractType.name}".`,
888961
{ nodes: fieldNodes },
889962
);
890963
}

0 commit comments

Comments
 (0)