Files
skills/docs/api-stack/api/api-plugin-graphql/src/resolver/resolver.md
2026-05-29 08:20:56 +01:00

5.1 KiB

resolver

(no submodules)

Purpose

Implements ResolverComposer (the fluent builder for resolver maps), the two built-in middleware factories requireRoles and validateArgs, the package-default graphqlEvents EventBus, and createQueryExecutedMiddleware — the production OTel/event-emission middleware. All four pieces work together to give consumers a typed, observable resolver pipeline that can be plugged into any GraphQL executor downstream.

ResolverComposer is the workhorse: callers add resolvers per (typeName, fieldName), attach middleware globally or per type, and call build() to obtain a ResolverMap whose entries are pre-composed (middleware → resolver). The composition order is right-to-left: the first registered middleware is the outermost wrapper, so composer.use(A).use(B).addResolver(...) runs A → B → resolver on call.

Features

  • use(mw) — register a global middleware (runs on every resolver).
  • useForType(typeName, mw) — register a middleware that runs only for resolvers of that type.
  • addResolver(typeName, fieldName, fn) — register one resolver; throws DUPLICATE_RESOLVER on collision.
  • addResolvers(typeName, fnsByName) — register many at once.
  • hasResolver / typeCount / resolverCount — introspection.
  • build() — produce the composed ResolverMap.
  • requireRoles(...roles) — middleware that throws UNAUTHENTICATED / INSUFFICIENT_ROLE as GraphQLError.
  • validateArgs(validator) — middleware that throws VALIDATION_FAILED when the validator returns a string.
  • graphqlEvents — package-default EventBus<GraphQLEventMap>.
  • createQueryExecutedMiddleware(bus?) — records OTel instruments and emits graphql:query:executed.

Usage: Basic

import { ResolverComposer, requireRoles, validateArgs } from "@suaveplan/api-plugin-graphql";

const map = new ResolverComposer()
    .useForType("Mutation", requireRoles("editor", "admin"))
    .addResolver("Query", "user", (_p, args) => ({ id: args.id, name: "Alice" }))
    .addResolver(
        "Mutation",
        "createPost",
        (_p, args) => ({ id: "post:1", title: args.title })
    )
    .build();

Usage: Advanced

import {
    ResolverComposer,
    createQueryExecutedMiddleware,
    graphqlEvents,
    validateArgs,
} from "@suaveplan/api-plugin-graphql";

graphqlEvents.on("graphql:query:executed", (e) => {
    console.log(`[gql] ${e.operationType}.${e.operationName} ${e.durationMs.toFixed(2)}ms${e.hasErrors ? " (error)" : ""}`);
});

const map = new ResolverComposer()
    .useForType("Query", createQueryExecutedMiddleware())
    .useForType("Mutation", createQueryExecutedMiddleware())
    .useForType(
        "Mutation",
        validateArgs((args) => (typeof args.title !== "string" ? "title required" : null))
    )
    .addResolver("Query", "ping", () => "pong")
    .addResolver("Mutation", "createPost", async (_p, args) => ({ id: "post:1", title: args.title }))
    .build();

API Reference

Symbol Kind Notes
ResolverComposer class See methods below.
.use(mw) method Chainable.
.useForType(typeName, mw) method Chainable.
.addResolver(typeName, fieldName, fn) method Throws GraphQLError(DUPLICATE_RESOLVER) if already registered.
.addResolvers(typeName, mapByName) method Loops addResolver.
.hasResolver(typeName, fieldName) method boolean.
.typeCount / .resolverCount getter number.
.build() method Returns ResolverMap.
requireRoles(...roles) function Throws UNAUTHENTICATED / INSUFFICIENT_ROLE.
validateArgs(validator) function validator(args) => string | null. Throws VALIDATION_FAILED on non-null.
graphqlEvents const EventBus<GraphQLEventMap>.
createQueryExecutedMiddleware(bus?) function Records OTel + emits event.

Implementation Notes

composeMiddleware walks the middleware list right-to-left, capturing current in a closure so each iteration creates the next outer wrapper. The conditional if (middleware === undefined) continue keeps noUncheckedIndexedAccess happy without sacrificing branch coverage — every slot is dense at build time so the branch is unreachable in practice; the unit tests rely on observation of the composed chain, not on dead-code coverage.

createQueryExecutedMiddleware always records the duration histogram (even on error) and always emits the event in finally. This is the only path that touches getGraphQLInstruments(); the lazy binding means the meter is whatever provider is installed at first invocation. Tests install a fresh provider before exercising the middleware.

graphqlEvents is a module-scoped singleton. Callers who need isolation (e.g. parallel test workers) construct their own EventBus<GraphQLEventMap> and pass it as the bus argument to createQueryExecutedMiddleware.

requireRoles and validateArgs use GraphQLError (with context.statusCode 401/403) so an HTTP error-handler middleware downstream can map cleanly to HTTP responses without re-classifying errors by message.