feat(@nestjs/graphql): add @BatchResolveField() for DataLoader-backed batching - #4123
Open
Manuel-Antunes wants to merge 1 commit into
Open
Manuel-Antunes wants to merge 1 commit into
Manuel-Antunes wants to merge 1 commit into
Conversation
- Introduced the @BatchResolveField decorator to enable batch field resolution in GraphQL. - Added support for batch loading of related data, reducing N+1 query problems. - Created a new GraphQL schema for blog posts, authors, and comments. - Implemented PostsResolver to handle fetching posts, authors, and comments using batch resolution. - Developed BlogService to manage data retrieval and batch call recording. - Added tests for the new batch resolution functionality, ensuring correct behavior and performance. - Updated existing utilities and interfaces to support batch loading options and error handling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Spring for GraphQL ships
@BatchMapping: you annotate a controller method, it receives every parent of the current execution layer at once, and the framework wires the batching for you. The n+1 problem is solved declaratively, at the same level of abstraction you already write resolvers at.Nest has no equivalent today. Solving n+1 means hand-rolling DataLoaders: instantiating them per request, threading them through the GraphQL context or making the resolver request-scoped, and re-implementing the positional key/value mapping in every field. That is boilerplate the framework is well positioned to absorb — and the ecosystem has repeatedly said so:
The standing answer has been "use
dataloaderwith request-scoped providers". That works, but it has a real cost: making a resolver request-scoped bubbles up its entire dependency tree, which is a performance trade-off users take on solely to get a per-request loader.@BatchResolveField()gives the same guarantee with no scope change at all — the loader lives in aWeakMapkeyed by the GraphQL context object.Worth noting: this has been asked for repeatedly but, as far as I can tell from searching the repo history, never actually submitted as a PR. This is an attempt to put an implementation on the table.
What this is not
@ResolveField()is untouched; the batching branch inResolversExplorerServiceonly activates when the new metadata key is present.dataloaderis an optional peer dependency, resolved through a dynamicimport()exactly like the existingts-morphintegration. Users who never write a batch field never install it, and nothing in the published.d.tsreferences its types — the loader options are declared as a structural subset inbatch-loader-options.interface.ts.@nestjs/graphql, above the driver abstraction. E2E tests cover Apollo and Mercurius, code first and schema first.API
The method may return:
Map<Parent, T>— keyed by the parent objects (the Spring equivalent of relying onequals/hashCode)Map<K, T>with thekeyByoption — because JSMapcompares object keys by reference,keyByis how you say "look this parent up by its id", which is what your repository has probably already grouped byT[]— one entry per parent, in orderOptions are everything
@ResolveField()accepts (name,nullable,description,deprecationReason,complexity,middleware) pluskeyByanddataLoader(forwarded to the underlyingDataLoader:maxBatchSize,cache,cacheKeyFn,batchScheduleFn,cacheMap).One deliberate departure from Spring
Spring's
@BatchMappingforbids field arguments, and sends you toBatchLoaderRegistryif you need them. Here, loaders are partitioned per(request context, serialized arguments), so this batches correctly into two calls instead of erroring:{ posts { published: comments(status: APPROVED) { text } pending: comments(status: PENDING) { text } } }Implementation notes
resolvers-explorer.service.ts): batching sits between the Nest resolver context and the field middleware. Field middleware therefore still runs once per field, while the method runs once per batch. Enhancers enabled viafieldResolverEnhancerssit inside the batch and run once per batch, with the array of parents as the execution-context root. Both behaviors are covered by tests.WeakMap<contextObject, Map<argsKey, DataLoader>>in the closure, rather than writing onto the user's context object. No key collisions between resolvers, no trouble with frozen contexts, and loaders are garbage-collected with the request.Map/Promise, which says nothing about the schema field). The error is thrown at schema-build time, following the@Query()precedent, so schema first works with a bare@BatchResolveField().dataloaderis absent, the error is logged at boot and surfaced as a GraphQL error on the field, rather than failing silently.Tests
38 new tests, all green alongside the existing suite:
packages/graphql/tests/utils/batch-field-resolver.util.spec.ts(17) — batching, per-request isolation, args partitioning, memoization,maxBatchSize, whole-batch and per-parent errors, fallbackspackages/graphql/tests/decorators/batch-resolve-field.decorator.spec.ts(7) — metadata and overloadspackages/apollo/tests/e2e/(11) — code first, schema first, field middleware ordering, enhancer semantics, fast-path resolverspackages/mercurius/tests/e2e/code-first-batch.spec.ts(3) — driver independenceNote on process
CONTRIBUTING.mdasks for a[discussion]issue before a major feature. Given the four issues above already cover the request, I went straight to an implementation so there is something concrete to react to — but I am happy to move this to a discussion issue and iterate on the API (naming,keyBy, argument handling) before any of it is considered for merge.Docs PR: nestjs/docs.nestjs.com#3527