# 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`. - `createQueryExecutedMiddleware(bus?)` — records OTel instruments and emits `graphql:query:executed`. ## Usage: Basic ```ts 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 ```ts 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`. | | `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` 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.