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; throwsDUPLICATE_RESOLVERon collision.addResolvers(typeName, fnsByName)— register many at once.hasResolver/typeCount/resolverCount— introspection.build()— produce the composedResolverMap.requireRoles(...roles)— middleware that throwsUNAUTHENTICATED/INSUFFICIENT_ROLEasGraphQLError.validateArgs(validator)— middleware that throwsVALIDATION_FAILEDwhen the validator returns a string.graphqlEvents— package-defaultEventBus<GraphQLEventMap>.createQueryExecutedMiddleware(bus?)— records OTel instruments and emitsgraphql: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.