-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Allow interface resolveType functions to resolve to child interfaces #3599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a30daec
d89b1ae
00dbea1
7167fcf
81c5ed9
6c8ad45
4dbfe20
5ba43d8
9e0c4d0
225cab9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,7 @@ import type { | |
| } from '../type/definition'; | ||
| import { | ||
| isAbstractType, | ||
| isInterfaceType, | ||
| isLeafType, | ||
| isListType, | ||
| isNonNullType, | ||
|
|
@@ -790,106 +791,128 @@ function completeLeafValue( | |
| */ | ||
| function completeAbstractValue( | ||
| exeContext: ExecutionContext, | ||
| returnType: GraphQLAbstractType, | ||
| abstractType: GraphQLAbstractType, | ||
| fieldNodes: ReadonlyArray<FieldNode>, | ||
| info: GraphQLResolveInfo, | ||
| path: Path, | ||
| result: unknown, | ||
| ): PromiseOrValue<ObjMap<unknown>> { | ||
| const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; | ||
| const resolveTypeFn = abstractType.resolveType ?? exeContext.typeResolver; | ||
| const contextValue = exeContext.contextValue; | ||
| const runtimeType = resolveTypeFn(result, contextValue, info, returnType); | ||
|
|
||
| if (isPromise(runtimeType)) { | ||
| return runtimeType.then((resolvedRuntimeType) => | ||
| completeObjectValue( | ||
| exeContext, | ||
| ensureValidRuntimeType( | ||
| resolvedRuntimeType, | ||
| exeContext, | ||
| returnType, | ||
| fieldNodes, | ||
| info, | ||
| result, | ||
| ), | ||
| fieldNodes, | ||
| info, | ||
| path, | ||
| result, | ||
| ), | ||
| ); | ||
| } | ||
| const possibleRuntimeTypeName = resolveTypeFn( | ||
| result, | ||
| contextValue, | ||
| info, | ||
| abstractType, | ||
| ); | ||
|
|
||
| return completeObjectValue( | ||
| return completeAbstractValueImpl( | ||
| exeContext, | ||
| ensureValidRuntimeType( | ||
| runtimeType, | ||
| exeContext, | ||
| returnType, | ||
| fieldNodes, | ||
| info, | ||
| result, | ||
| ), | ||
| abstractType, | ||
| possibleRuntimeTypeName, | ||
| fieldNodes, | ||
| info, | ||
| path, | ||
| result, | ||
| ); | ||
| } | ||
|
|
||
| function ensureValidRuntimeType( | ||
| runtimeTypeName: unknown, | ||
| function completeAbstractValueImpl( | ||
| exeContext: ExecutionContext, | ||
| returnType: GraphQLAbstractType, | ||
| abstractType: GraphQLAbstractType, | ||
| possibleRuntimeTypeName: PromiseOrValue<string | undefined>, | ||
| fieldNodes: ReadonlyArray<FieldNode>, | ||
| info: GraphQLResolveInfo, | ||
| path: Path, | ||
| result: unknown, | ||
| ): GraphQLObjectType { | ||
| if (runtimeTypeName == null) { | ||
| ): PromiseOrValue<ObjMap<unknown>> { | ||
| if (isPromise(possibleRuntimeTypeName)) { | ||
| return possibleRuntimeTypeName.then((resolved) => | ||
| completeAbstractValueImpl( | ||
| exeContext, | ||
| abstractType, | ||
| resolved, | ||
| fieldNodes, | ||
| info, | ||
| path, | ||
| result, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| if (possibleRuntimeTypeName == null) { | ||
| throw new GraphQLError( | ||
| `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.`, | ||
| `Abstract type "${abstractType.name}" must resolve to an Object type or an intermediate Interface type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${abstractType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, | ||
| { nodes: fieldNodes }, | ||
| ); | ||
| } | ||
|
|
||
| // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` | ||
| // TODO: remove in 17.0.0 release | ||
| if (isObjectType(runtimeTypeName)) { | ||
| if (isObjectType(possibleRuntimeTypeName)) { | ||
| throw new GraphQLError( | ||
| 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', | ||
| ); | ||
| } | ||
|
|
||
| if (typeof runtimeTypeName !== 'string') { | ||
| if (typeof possibleRuntimeTypeName !== 'string') { | ||
| throw new GraphQLError( | ||
| `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + | ||
| `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, | ||
| `Abstract type "${abstractType.name}" must resolve to an Object type or an intermediate Interface type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + | ||
| `value ${inspect(result)}, received "${inspect( | ||
| possibleRuntimeTypeName, | ||
| )}".`, | ||
| ); | ||
| } | ||
|
|
||
| const runtimeType = exeContext.schema.getType(runtimeTypeName); | ||
| if (runtimeType == null) { | ||
| const possibleRuntimeType = exeContext.schema.getType( | ||
| possibleRuntimeTypeName, | ||
| ); | ||
| if (possibleRuntimeType == null) { | ||
| throw new GraphQLError( | ||
| `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, | ||
| `Abstract type "${abstractType.name}" was resolved to a type "${possibleRuntimeTypeName}" that does not exist inside the schema.`, | ||
| { nodes: fieldNodes }, | ||
| ); | ||
| } | ||
|
|
||
| if (!isObjectType(runtimeType)) { | ||
| if (isInterfaceType(possibleRuntimeType)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about union types?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nevermind - based on feedback from WG discussion, a union type shouldn't be possible (though does this get potentially complicated by the proposal for union constraints via implementing interfaces?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, if unions ever implement interfaces, they could potentially be a link in the chain! |
||
| if (!exeContext.schema.isSubType(abstractType, possibleRuntimeType)) { | ||
| throw new GraphQLError( | ||
| `Interface type "${possibleRuntimeType.name}" is not a possible type for "${abstractType.name}".`, | ||
| { nodes: fieldNodes }, | ||
| ); | ||
| } | ||
|
|
||
| return completeAbstractValue( | ||
| exeContext, | ||
| possibleRuntimeType, | ||
| fieldNodes, | ||
| info, | ||
| path, | ||
| result, | ||
| ); | ||
| } | ||
|
|
||
| if (!isObjectType(possibleRuntimeType)) { | ||
| throw new GraphQLError( | ||
| `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, | ||
| `Abstract type "${abstractType.name}" was resolved to a non-object and non-interface type "${possibleRuntimeTypeName}".`, | ||
| { nodes: fieldNodes }, | ||
| ); | ||
| } | ||
|
|
||
| if (!exeContext.schema.isSubType(returnType, runtimeType)) { | ||
| if (!exeContext.schema.isSubType(abstractType, possibleRuntimeType)) { | ||
| throw new GraphQLError( | ||
| `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, | ||
| `Runtime Object type "${possibleRuntimeType.name}" is not a possible type for "${abstractType.name}".`, | ||
| { nodes: fieldNodes }, | ||
| ); | ||
| } | ||
|
|
||
| return runtimeType; | ||
| return completeObjectValue( | ||
| exeContext, | ||
| possibleRuntimeType, | ||
| fieldNodes, | ||
| info, | ||
| path, | ||
| result, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about only altering this error message in the case that an intermediate Interface type actually exists as a potential case?
Looking at the unit test impact, my sense is the primary downside of this change is the additional complication to this error message that makes it slightly harder to understand.
Ideally for the large majority common case where there is no intermediate interface, we don't burden the error message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Tailoring the error message to the interface at hand seems pretty reasonable.