first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user