Summary
This issue is a bit in the weeds, but I'll do my best to give some background context. We're making use of GQL federation to stitch together a few sub-graphs. For purposes of this example say we have two. A parent that has the majority of the fields, then another sub-graph that has a specific field say some_relationship. To handle the specific field, we defined a NestJS resolver class that has a method with the @ResolveField directive, which it properly stitches it into the primary User type.
Given a query like:
{
users(where: {id: {_in: [10, 13]}}) {
id
full_name
some_relationship {
id
}
}
}
I noticed that a SQL query was being made twice, once for each ID when I would have expected the dataloader to batch them together. I think it has something to do with some race condition where concurrent requests get different DataLoader instances.
Here's an example test case that better illustrates the issue. I needed to install @nestjs/apollo, @apollo/server, @as-integrations/express5 as dev deps to make this work.
import request from "supertest";
import { describe } from "vitest";
import { Injectable } from "@nestjs/common";
import { ApolloDriver, type ApolloDriverConfig } from "@nestjs/apollo";
import { Field, GraphQLModule, Int, ObjectType, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql";
import { type NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { DataloaderFactory, DataloaderModule, Loader, type LoaderFrom } from "@strv/nestjs-dataloader";
@ObjectType()
class Item {
@Field(() => Int) id!: number;
@Field() name!: string;
}
@ObjectType()
class ParentType {
@Field(() => Int) id!: number;
}
@Injectable()
class ItemsLoaderFactory extends DataloaderFactory<number, Item> {
static calls: number[][] = [];
load = async (ids: number[]) => {
ItemsLoaderFactory.calls.push(ids);
return ids.map((id) => ({ id, name: `item-${id}` }));
};
id = (item: Item) => item.id;
}
@Resolver(() => ParentType)
class ParentResolver {
@Query(() => [ParentType])
parents() {
return [{ id: 1 }, { id: 2 }];
}
@ResolveField(() => Item)
async item(@Parent() parent: ParentType, @Loader(ItemsLoaderFactory) loader: LoaderFrom<ItemsLoaderFactory>) {
return await loader.load(parent.id);
}
}
describe("@Loader() under concurrent field resolution", (it) => {
it("batches sibling field resolutions into a single factory call", async (t) => {
ItemsLoaderFactory.calls = [];
const module = await Test.createTestingModule({
imports: [
DataloaderModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true,
}),
],
providers: [ItemsLoaderFactory, ParentResolver],
}).compile();
const app = await module.createNestApplication<NestExpressApplication>().init();
t.onTestFinished(async () => await app.close());
const response = await request(app.getHttpServer())
.post("/graphql")
.send({ query: "{ parents { id item { id name } } }" });
t.expect(response.status).toBe(200);
t.expect(response.body.errors).toBeUndefined();
t.expect(response.body.data).toEqual({
parents: [
{ id: 1, item: { id: 1, name: "item-1" } },
{ id: 2, item: { id: 2, name: "item-2" } },
],
});
t.expect(ItemsLoaderFactory.calls).toEqual([[1, 2]]);
});
});
Where the output is:
FAIL test/FieldResolverBatching.test.ts > @Loader() under concurrent field resolution > batches sibling field resolutions into a single factory call
AssertionError: expected [ [ 1 ], [ 2 ] ] to deeply equal [ [ 1, 2 ] ]
- Expected
+ Received
[
[
1,
+ ],
+ [
2,
],
]
Summary
This issue is a bit in the weeds, but I'll do my best to give some background context. We're making use of GQL federation to stitch together a few sub-graphs. For purposes of this example say we have two. A parent that has the majority of the fields, then another sub-graph that has a specific field say
some_relationship. To handle the specific field, we defined a NestJS resolver class that has a method with the@ResolveFielddirective, which it properly stitches it into the primaryUsertype.Given a query like:
{ users(where: {id: {_in: [10, 13]}}) { id full_name some_relationship { id } } }I noticed that a SQL query was being made twice, once for each ID when I would have expected the dataloader to batch them together. I think it has something to do with some race condition where concurrent requests get different
DataLoaderinstances.Here's an example test case that better illustrates the issue. I needed to install
@nestjs/apollo,@apollo/server,@as-integrations/express5as dev deps to make this work.Where the output is:
FAIL test/FieldResolverBatching.test.ts > @Loader() under concurrent field resolution > batches sibling field resolutions into a single factory call AssertionError: expected [ [ 1 ], [ 2 ] ] to deeply equal [ [ 1, 2 ] ] - Expected + Received [ [ 1, + ], + [ 2, ], ]