Skip to content
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

Resolve Promise references for interface types before handing off to resolveType fn #2556

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/weak-dryers-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@apollo/subgraph": patch
---

Resolve `Promise` references before calling `__resolveType` on interface

Since the introduction of entity interfaces, users could not return
a `Promise` from `__resolveReference` while implementing a synchronous,
custom `__resolveType` function. This change fixes/permits this use case.

Additional background / implementation details:

Returning a `Promise` from `__resolveReference` has historically never
been an issue. However, with the introduction of entity interfaces, the
calling of an interface's `__resolveType` function became a new concern.

`__resolveType` functions expect a reference (and shouldn't be concerned
with whether those references are wrapped in a `Promise`). In order to
address this, we can `await` the reference before calling the
`__resolveType` (this handles both the non-`Promise` and `Promise` case).

75 changes: 74 additions & 1 deletion gateway-js/src/__tests__/executeQueryPlan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3802,10 +3802,15 @@ describe('executeQueryPlan', () => {
s1,
}: {
s1?: {
// additional resolvers for interface I
iResolversExtra?: any,
// provide a default __resolveReference for the interface
hasIResolveReference?: boolean,
// turn an id into extra data returned by __resolveReference (if hasIResolveReference is true)
iResolveReferenceExtra?: (id: string) => { [k: string]: any },
// additional resolvers for type A
aResolversExtra?: any,
// additional resolvers for type B
bResolversExtra?: any,
}
}) => {
Expand Down Expand Up @@ -4136,7 +4141,75 @@ describe('executeQueryPlan', () => {
+ 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, '
+ 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.'
],
}])('resolving an interface @key $name', async ({s1, expectedErrors}) => {
}, {
name: 'with an async __resolveReference and a non-default __resolveType',
s1: {
iResolversExtra: {
async __resolveReference(ref: { id: string }) {
return ref.id === 'idA'
? { id: ref.id, x: 1, z: 3 }
: { id: ref.id, x: 10, w: 30 };
},
__resolveType(ref: { id: string }) {
switch (ref.id) {
case 'idA':
return 'A';
case 'idB':
return 'B';
default:
throw new Error('Unknown type: ' + ref.id);
}
},
},
},
}, {
name: 'with an async __resolveReference and a non-default async __resolveType',
s1: {
iResolversExtra: {
async __resolveReference(ref: { id: string }) {
return ref.id === 'idA'
? { id: ref.id, x: 1, z: 3 }
: { id: ref.id, x: 10, w: 30 };
},
async __resolveType(ref: { id: string }) {
switch (ref.id) {
case 'idA':
return 'A';
case 'idB':
return 'B';
default:
throw new Error('Unknown type: ' + ref.id);
}
},
},
},
}, {
name: 'with an async __resolveReference and async __isTypeOf on implementations',
s1: {
iResolversExtra: {
async __resolveReference(ref: {
id: string;
// I don't understand the TypeScript error that occurs when the
// return type is removed here (like all the others); it surfaces
// because `aResolversExtra` is defined, which I can't explain.
}): Promise<Record<string, any>> {
return ref.id === 'idA'
? { id: ref.id, x: 1, z: 3 }
: { id: ref.id, x: 10, w: 30 };
},
},
aResolversExtra: {
async __isTypeOf(ref: { id: string }) {
return ref.id === 'idA';
},
},
bResolversExtra: {
async __isTypeOf(ref: { id: string }) {
return ref.id === 'idB';
},
},
},
}])('resolving an interface @key $name', async ({ s1, expectedErrors }) => {
const tester = defineSchema({ s1 });

const { plan, response } = await tester(`
Expand Down
67 changes: 67 additions & 0 deletions subgraph-js/src/__tests__/buildSubgraphSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,73 @@ describe('buildSubgraphSchema', () => {
expect(errors).toBeUndefined();
expect((data as any)?._entities[0].name).toEqual('Apollo Gateway');
});

it('correctly resolves Promise values from `resolveReference` for `resolveType`', async () => {
const query = `#graphql
query ($representations: [_Any!]!) {
_entities(representations: $representations) {
... on Product {
name
}
}
}
`;

const variables = {
representations: [{ __typename: 'Product', id: 1 }],
};

const schema = buildSubgraphSchema([
{
typeDefs: gql`
extend schema
@link(
url: "https://specs.apollo.dev/federation/v2.4"
import: ["@key"]
)
interface Product @key(fields: "id") {
id: ID!
name: String
}
type Book implements Product @key(fields: "id") {
id: ID!
name: String
author: String!
}
`,
resolvers: {
Product: {
async __resolveReference() {
return { id: '1', name: 'My book', author: 'Author' };
},
__resolveType(ref) {
if ('author' in ref) return 'Book';
throw new Error(
'Could not resolve type, received: ' + ref.toString(),
);
},
},
},
},
]);
const { data, errors } = await graphql({
schema,
source: query,
rootValue: null,
contextValue: null,
variableValues: variables,
});
expect(errors).toBeUndefined();
expect(data).toMatchInlineSnapshot(`
Object {
"_entities": Array [
Object {
"name": "My book",
},
],
}
`);
});
});

describe('_service root field', () => {
Expand Down
6 changes: 3 additions & 3 deletions subgraph-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function ensureValidRuntimeType(
return runtimeType;
}

function withResolvedType<T>({
async function withResolvedType<T>({
type,
value,
context,
Expand All @@ -146,9 +146,9 @@ function withResolvedType<T>({
context: any,
info: GraphQLResolveInfo,
callback: (runtimeType: GraphQLObjectType) => PromiseOrValue<T>,
}): PromiseOrValue<T> {
}): Promise<T> {
const resolveTypeFn = type.resolveType ?? defaultTypeResolver;
const runtimeType = resolveTypeFn(value, context, info, type);
const runtimeType = resolveTypeFn(await value, context, info, type);
if (isPromise(runtimeType)) {
return runtimeType.then((name) => (
callback(ensureValidRuntimeType(name, info.schema, type, value))
Expand Down