Skip to content

Commit

Permalink
Resolve Promise references for interface types before handing off t…
Browse files Browse the repository at this point in the history
…o `resolveType` fn (#2556)

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).
  • Loading branch information
trevor-scheer authored May 4, 2023
1 parent eb5c104 commit 53fe448
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 4 deletions.
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

0 comments on commit 53fe448

Please sign in to comment.