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

GraphQLDependency

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

DataInitializerResult

What an initializer gives back: the extra fields to merge onto each object, keyed by that object's id.

DependencyFilterOptions

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.

DependencyOptions

One relation between a parent type and a child type: where the loaded children are attached, and how they are matched to their parent.

ResolutionCacheData

One type's entry in the resolution cache.

ResolutionCacheDataMap

Every object of one type seen so far in a request, keyed by id.

ResolutionCallsMap

The loader and initializer calls already made during a request, keyed by a hash of the call's signature.

Variables

Variable

Description

Dependency

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

DataInitializer

An async routine that fills extra fields onto a type's own objects before its dependencies load, registered with defineInitializer().

DataLoader

A bulk fetch for one entity type, registered with defineLoader() and called whenever that type is needed as another type's dependency.

DependencyFieldsGetter

A single field supplied as a thunk, used to name the fields an initializer fills. Deferred for the same reason as DependencyOptionsGetter.

DependencyOptionsGetter

A relation supplied as a thunk, which is how require() takes it.

ResolutionCache

Everything one load() call has resolved, one entry per participating type.

Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.