Skip to content

Commit

Permalink
Skip type resolution for null (#3138)
Browse files Browse the repository at this point in the history
Addresses #3123 / #3135 by skipping type resolution for `null`
references.
  • Loading branch information
tninesling authored Sep 3, 2024
1 parent 13237dc commit bb07a37
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-pigs-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/subgraph": patch
---

When resolving references, skip type resolution if the reference resolves to null.
106 changes: 106 additions & 0 deletions subgraph-js/src/__tests__/buildSubgraphSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,112 @@ describe('buildSubgraphSchema', () => {
}
`);
});

it('skips `resolveType` if `resolveReference` returns null', async () => {
const query = `#graphql
query Product {
_entities(representations: [{ __typename: "Product", key: "unknown" }]) {
__typename
... on Product {
type
key
}
}
}
`;
const schema = buildSubgraphSchema([
{
typeDefs: gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.8"
import: ["@key"]
)
enum ProductType {
A
B
}
interface Product @key(fields: "key") {
type: ProductType!
key: String!
}
type ProductA implements Product @key(fields: "key") {
type: ProductType!
key: String!
}
type ProductB implements Product @key(fields: "key") {
type: ProductType!
key: String!
}
`,
resolvers: {
Product: {
__resolveReference: async ({ key }: { key: string }) => {
if (key === 'a') {
return {
type: 'A',
key,
};
} else if (key === 'b') {
return {
type: 'B',
key,
};
} else {
return null;
}
},
__resolveType: async (entity: { type: 'A' | 'B' }) => {
switch (entity.type) {
case 'A':
return 'ProductA';
case 'B':
return 'ProductB';
default:
throw new Error('Unknown type');
}
},
},
ProductA: {
__resolveReference: async ({ key }: { key: string }) => {
return {
type: 'ProductA',
key,
};
},
},
ProductB: {
__resolveReference: async ({ key }: { key: string }) => {
return {
type: 'ProductB',
key,
};
},
},
},
},
]);

const { data, errors } = await graphql({
schema,
source: query,
rootValue: null,
contextValue: null,
variableValues: null,
});
expect(errors).toBeUndefined();
expect(data).toMatchInlineSnapshot(`
Object {
"_entities": Array [
null,
],
}
`);
});
});

describe('_service root field', () => {
Expand Down
11 changes: 8 additions & 3 deletions subgraph-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,15 +162,20 @@ async function withResolvedType<T>({
info: GraphQLResolveInfo,
callback: (runtimeType: GraphQLObjectType) => PromiseOrValue<T>,
}): Promise<T> {
const resolvedValue = await value;
if (resolvedValue === null) {
return resolvedValue;
}

const resolveTypeFn = type.resolveType ?? defaultTypeResolver;
const runtimeType = resolveTypeFn(await value, context, info, type);
const runtimeType = resolveTypeFn(resolvedValue, context, info, type);
if (isPromise(runtimeType)) {
return runtimeType.then((name) => (
callback(ensureValidRuntimeType(name, info.schema, type, value))
callback(ensureValidRuntimeType(name, info.schema, type, resolvedValue))
));
}

return callback(ensureValidRuntimeType(runtimeType, info.schema, type, value));
return callback(ensureValidRuntimeType(runtimeType, info.schema, type, resolvedValue));
}

function definedResolveReference(type: GraphQLObjectType | GraphQLInterfaceType): GraphQLReferenceResolver<any> | undefined {
Expand Down

0 comments on commit bb07a37

Please sign in to comment.