Skip to content

Commit

Permalink
fix: Node subschema pattern (#6126)
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed May 2, 2024
1 parent 1ebf5e5 commit 680351e
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 28 deletions.
68 changes: 68 additions & 0 deletions .changeset/healthy-news-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
"@graphql-tools/federation": patch
"@graphql-tools/delegate": patch
"@graphql-tools/stitch": patch
---

When there is a Node subschema, and others to resolve the rest of the entities by using a union resolver as in Federation like below, it was failing. This version fixes that issue.

```graphql
query {
node(id: "1") {
id # Fetches from Node
... on User {
name # Fetches from User
}
}
}
```

```graphql
type Query {
node(id: ID!): Node
}

interface Node {
id: ID!
}

type User implements Node {
id: ID!
}

type Post implements Node {
id: ID!
}
```

```graphql
# User subschema
scalar _Any
type Query {
_entities(representations: [_Any!]!): [_Entity]!
}
union _Entity = User
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
```

```graphql
# Post subschema
scalar _Any
union _Entity = Post
type Query {
_entities(representations: [_Any!]!): [_Entity]!
}
interface Node {
id: ID!
}
type Post implements Node {
id: ID!
title: String!
}
```
1 change: 0 additions & 1 deletion packages/delegate/src/delegateToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export function delegateToSchema<
context,
info,
});

return delegateRequest({
...options,
request,
Expand Down
33 changes: 19 additions & 14 deletions packages/delegate/src/prepareGatewayDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
GraphQLSchema,
InlineFragmentNode,
isAbstractType,
isCompositeType,
isInterfaceType,
isLeafType,
Kind,
SelectionNode,
SelectionSetNode,
Expand Down Expand Up @@ -415,9 +415,12 @@ function wrapConcreteTypes(
): DocumentNode {
const namedType = getNamedType(returnType);

if (!isCompositeType(namedType)) {
if (isLeafType(namedType)) {
return document;
}
const possibleTypes = isAbstractType(namedType)
? targetSchema.getPossibleTypes(namedType)
: [namedType];

const rootTypeNames = getRootTypeNames(targetSchema);

Expand Down Expand Up @@ -445,24 +448,26 @@ function wrapConcreteTypes(
const fieldType = typeInfo.getType();
if (fieldType) {
const fieldNamedType = getNamedType(fieldType);
if (isAbstractType(fieldNamedType) && fieldNamedType.name !== namedType.name) {
if (
isAbstractType(fieldNamedType) &&
fieldNamedType.name !== namedType.name &&
possibleTypes.length > 0
) {
return {
...node,
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [
{
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: namedType.name,
},
selections: possibleTypes.map(possibleType => ({
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: possibleType.name,
},
selectionSet: node.selectionSet,
},
],
selectionSet: node.selectionSet,
})),
},
};
}
Expand Down
25 changes: 14 additions & 11 deletions packages/delegate/src/resolveExternalValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import {
GraphQLOutputType,
GraphQLResolveInfo,
GraphQLSchema,
isAbstractType,
isCompositeType,
isListType,
locatedError,
} from 'graphql';
import { Maybe } from '@graphql-tools/utils';
import { annotateExternalObject, isExternalObject, mergeFields } from './mergeFields.js';
import { Subschema } from './Subschema.js';
import { StitchingInfo, SubschemaConfig } from './types.js';
import { MergedTypeInfo, StitchingInfo, SubschemaConfig } from './types.js';

export function resolveExternalValue<TContext extends Record<string, any>>(
result: any,
Expand Down Expand Up @@ -85,7 +84,6 @@ function resolveExternalObject<TContext extends Record<string, any>>(
if (!isExternalObject(object)) {
annotateExternalObject(object, unpathedErrors, subschema, Object.create(null));
}

if (skipTypeMerging || info == null) {
return object;
}
Expand All @@ -96,19 +94,24 @@ function resolveExternalObject<TContext extends Record<string, any>>(
return object;
}

const typeName = isAbstractType(type) ? object.__typename : type.name;

const mergedTypeInfo = stitchingInfo.mergedTypes[typeName];
let targetSubschemas: undefined | Array<Subschema>;

// Within the stitching context, delegation to a stitched GraphQLSchema or SubschemaConfig
// will be redirected to the appropriate Subschema object, from which merge targets can be queried.
if (mergedTypeInfo != null) {
targetSubschemas = mergedTypeInfo.targetSubschemas.get(subschema as Subschema);

let mergedTypeInfo: MergedTypeInfo | undefined;
const possibleTypeNames = [object.__typename, type.name];
for (const possibleTypeName of possibleTypeNames) {
if (
possibleTypeName != null &&
stitchingInfo.mergedTypes[possibleTypeName]?.targetSubschemas?.get(subschema as Subschema)
?.length
) {
mergedTypeInfo = stitchingInfo.mergedTypes[possibleTypeName];
break;
}
}

// If there are no merge targets from the subschema, return.
if (!targetSubschemas || !targetSubschemas.length) {
if (!mergedTypeInfo) {
return object;
}

Expand Down
46 changes: 44 additions & 2 deletions packages/federation/src/supergraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,12 +723,54 @@ export function getSubschemasFromSupergraphSdl({
}
visitTypeDefinitionsForOrphanTypes(typeNode);
});
const extendedSubgraphTypes = [...subgraphTypes, ...extraOrphanTypesForSubgraph.values()];
// We should add implemented objects from other subgraphs implemented by this interface
for (const interfaceInSubgraph of subgraphTypes) {
if (interfaceInSubgraph.kind === Kind.INTERFACE_TYPE_DEFINITION) {
for (const definitionNode of supergraphAst.definitions) {
if (
definitionNode.kind === Kind.OBJECT_TYPE_DEFINITION &&
definitionNode.interfaces?.some(
interfaceNode => interfaceNode.name.value === interfaceInSubgraph.name.value,
)
) {
const existingType = subgraphTypes.find(
typeNode => typeNode.name.value === definitionNode.name.value,
);
const iFaceNode: NamedTypeNode = {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: interfaceInSubgraph.name.value,
},
};
if (!existingType) {
extendedSubgraphTypes.push({
kind: Kind.OBJECT_TYPE_DEFINITION,
name: definitionNode.name,
fields: interfaceInSubgraph.fields,
interfaces: [iFaceNode],
});
}
if (
existingType?.kind === Kind.OBJECT_TYPE_DEFINITION &&
!existingType.interfaces?.some(
interfaceNode => interfaceNode.name.value === interfaceInSubgraph.name.value,
)
) {
(existingType as any).interfaces ||= [];
(existingType as any).interfaces.push(iFaceNode);
}
break;
}
}
}
}
let schema: GraphQLSchema;
const schemaAst: DocumentNode = {
kind: Kind.DOCUMENT,
definitions: [
...subgraphTypes,
...extraOrphanTypesForSubgraph.values(),
...extendedSubgraphTypes,
entitiesUnionTypeDefinitionNode,
anyTypeDefinitionNode,
],
Expand Down
Loading

0 comments on commit 680351e

Please sign in to comment.