first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# resolver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```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<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.