graphql-dependency package
Declarative dependency loading for GraphQL schemas served by @imqueue services — describe how your types relate once, at start-up, and nested data arrives in bulk instead of one service call per resolved object.
A GraphQL query spanning microservices normally degenerates into the N+1 problem: a field resolver runs once per parent object, and each run makes its own RPC. This package takes the loading out of the field resolvers. Three declarations per type, all made through Dependency, describe the graph:
- a **loader** (GraphQLDependency.defineLoader()) — how to fetch many objects of one type at once,
(context, filter, fields) =\> Promise\<T[]\>; - **requirements** (GraphQLDependency.require()) — which child types a type owns, the field each child attaches to, and which of the parent's own fields feed the child loader's filter; - optionally an **initializer**, see GraphQLDependency.defineInitializer() — an async routine that fills fields on the parent before its dependencies load, for when a dependency filter needs a value the initial result does not carry.
Then one GraphQLDependency.load() call in the top-level resolver walks the fields the client actually asked for, merges everything that needs the same type into a single filter, and calls each loader once per level.
Remarks
Every object taking part must carry an id. Loaded rows are matched back onto their parents by id and by nothing else, so load() adds id to the requested-field map at every level — mutating the map it was handed.
Work is batched per level, not globally. Sibling dependencies of one type run concurrently; the next level down waits, because a child's filter is built from values the parent level has just loaded. Within a level, an id already present in the resolution cache is dropped from the filter, and two requirements that produce the same filter share one loader call — which is what keeps a query that reaches the same type from several directions down to one round trip per distinct filter.
The resolution cache lives for the duration of a single load() call and is then discarded. Nothing is shared between requests, so no request can serve another request's stale data.
Registration, by contrast, is global and permanent: Dependency(SomeType) always returns the same instance for the same GraphQLObjectType, so the declarations belong next to the type definitions and run once at start-up.
Example
import { Dependency } from '@imqueue/graphql-dependency';
import { fieldsMap } from 'graphql-fields-list';
// at start-up, next to the type definitions
Dependency(UserType).defineLoader(async (context, filter, fields) =>
(await context.user.listUser(filter, fields)).data,
);
Dependency(CompanyType).require(UserType, () => ({
as: CompanyType.getFields().employees,
filter: {
// UserType's loader filters by companyId; feed it every id in the
// company result set
[UserType.getFields().companyId.name]:
CompanyType.getFields().id,
},
}));
// in the top-level company resolver
async function companies(source, args, context, info) {
const data = await context.company.listCompany(args);
// one bulk call fills in employees for every company at once
return Dependency(CompanyType).load(data, context, fieldsMap(info));
}
Classes
|
Class |
Description |
|---|---|
|
One GraphQL object type's place in the dependency graph — its bulk loader, its optional initializer, and the child types it owns. |
Interfaces
|
Interface |
Description |
|---|---|
|
What an initializer gives back: the extra fields to merge onto each object, keyed by that object's | |
|
How a child type's objects are found: each key names a field of the child loader's filter, and its value is the parent field supplying the values. | |
|
One relation between a parent type and a child type: where the loaded children are attached, and how they are matched to their parent. | |
|
One type's entry in the resolution cache. | |
|
Every object of one type seen so far in a request, keyed by | |
|
The loader and initializer calls already made during a request, keyed by a hash of the call's signature. |
Variables
|
Variable |
Description |
|---|---|
|
The dependency description for a GraphQL object type — an alias for GraphQLDependency.create(), and the intended way to reach every method on this package's API. |
Type Aliases
|
Type Alias |
Description |
|---|---|
|
An async routine that fills extra fields onto a type's own objects before its dependencies load, registered with | |
|
A bulk fetch for one entity type, registered with | |
|
A single field supplied as a thunk, used to name the fields an initializer fills. Deferred for the same reason as DependencyOptionsGetter. | |
|
A relation supplied as a thunk, which is how | |
|
Everything one |
Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.