74 lines
7.3 KiB
Markdown
74 lines
7.3 KiB
Markdown
# Add @suaveplan/api-plugin-graphql
|
|
|
|
**Package**: `@suaveplan/api-plugin-graphql`
|
|
**Status**: PROPOSED
|
|
**Type**: new-package
|
|
|
|
## Why
|
|
|
|
`@suaveplan/api` consumers need a typed, builder-driven way to declare GraphQL APIs without authoring raw SDL. The upstream `@suaveplan/api-plugin-graphql` (suaveplan main, v1.0.4) shipped a self-contained description + resolver-composition layer with `SchemaBuilder`, `TypeBuilder`, `ResolverComposer`, and the `defineGraphQLPlugin` plugin-definition factory. Genesis must reach functional parity with the upstream surface so existing consumers can swap in without code change while picking up enterprise hardening (typed errors via `@suaveplan/error`, Zod-validated boundaries, OTel instruments, real `EventBus` event channel via `@suaveplan/events`).
|
|
|
|
## What changes
|
|
|
|
- ADDED: `defineGraphQLPlugin(options)` — factory producing a `GraphQLPluginDefinition` (name + merged `config` + lifecycle `hooks` + `getSchema()` / `getResolvers()` accessors).
|
|
- ADDED: `SchemaBuilder` — fluent builder for object / input / enum / interface / union types plus `addQuery` / `addMutation` / `addSubscription` / `build`.
|
|
- ADDED: `TypeBuilder` — chainable field declarations consumed by `SchemaBuilder`.
|
|
- ADDED: `ResolverComposer` — `use` / `useForType` / `addResolver` / `addResolvers` / `hasResolver` / `build`, composing global + per-type middleware around each resolver.
|
|
- ADDED: `requireRoles(...roles)` — auth-check middleware. Throws `GraphQLError(GRAPHQL.UNAUTHENTICATED)` when `context.user` is absent and `GraphQLError(GRAPHQL.INSUFFICIENT_ROLE)` when no role intersects.
|
|
- ADDED: `validateArgs(validator)` — argument-validation middleware. Throws `GraphQLError(GRAPHQL.VALIDATION_FAILED)` when the validator returns a non-null string.
|
|
- ADDED: `createQueryExecutedMiddleware(bus?)` — wraps a resolver in start/end timing and emits a typed `graphql:query:executed` event onto the provided `EventBus` (or the package-default `graphqlEvents` bus).
|
|
- ADDED: `graphqlEvents` — package-default `EventBus<GraphQLEventMap>` for `graphql:query:executed` listeners.
|
|
- ADDED: `GRAPHQL_CODES` + `GraphQLError` — `defineCodes("GRAPHQL", [...])` registry with 6 codes: `UNAUTHENTICATED`, `INSUFFICIENT_ROLE`, `VALIDATION_FAILED`, `INVALID_INPUT`, `TYPE_NOT_FOUND`, `DUPLICATE_RESOLVER`.
|
|
- ADDED: Zod schemas at the public boundary — `DefineGraphQLPluginOptionsSchema` validates options passed to `defineGraphQLPlugin`.
|
|
- ADDED: OTel instruments — `graphql.resolvers_executed_total{outcome}` counter and `graphql.resolver_duration_ms` histogram emitted from `createQueryExecutedMiddleware`.
|
|
- ADDED: Type exports — `SchemaDefinition`, `TypeDefinition`, `TypeKind`, `FieldDefinition`, `ScalarType`, `QueryContext`, `ResolverFn`, `ResolverInfo`, `ResolverMiddleware`, `ResolverMap`, `GraphQLPluginConfig`, `GraphQLPluginHooks`, `GraphQLQueryExecutedEvent`, `GraphQLEventMap`, `DefineGraphQLPluginOptions`, `GraphQLPluginDefinition`.
|
|
|
|
## Out of scope
|
|
|
|
- Live GraphQL execution / HTTP mount. The upstream package is a description + composition layer and does **not** consume `graphql-js` or mount a Hono route; genesis preserves that contract. A future `@suaveplan/api-plugin-graphql-executor` package (or a follow-up change) will add `graphql-js` execution + a Hono route — that work is enumerated in `enterprise/api-plugin-graphql.md` §3 and is explicitly a separate enterprise upgrade, not migration parity.
|
|
- SDL import/export, federation, persisted queries, subscriptions transport, schema-registry publishing — all enumerated as enterprise upgrades, none present in upstream.
|
|
|
|
## Source mapping
|
|
|
|
This change derives from `../suaveplan/enterprise/api-plugin-graphql.md` and the upstream sources under `../suaveplan/packages/server/api-plugin-graphql/src/`.
|
|
|
|
| Requirement | Source lines / files | Notes |
|
|
|---|---|---|
|
|
| `defineGraphQLPlugin` factory with default config | `enterprise/api-plugin-graphql.md` L13, L22; `src/plugin/plugin.ts` L83-L116 | verbatim — `basePath=/graphql`, `enableIntrospection=true`, `enablePlayground=true`, `maxDepth=10` |
|
|
| `getSchema()` / `getResolvers()` accessors | `src/plugin/plugin.ts` L109-L114 | verbatim |
|
|
| Lifecycle hooks (onInit / onReady / onClose) passthrough | `src/plugin/plugin.ts` L99-L103 | verbatim |
|
|
| `SchemaBuilder` add{Type,InputType,EnumType,InterfaceType,UnionType,Query,Mutation,Subscription} + hasType/getType/typeCount/queryCount/mutationCount/build | `src/schema-builder/schema-builder.ts` L36-L221 | verbatim |
|
|
| `TypeBuilder.field` + fieldCount + build | `src/schema-builder/type-builder.ts` L26-L63 | verbatim |
|
|
| `ResolverComposer` use / useForType / addResolver / addResolvers / hasResolver / typeCount / resolverCount / build | `src/resolver/resolver.ts` L26-L149 | verbatim |
|
|
| Middleware composition right-to-left | `src/resolver/resolver.ts` L132-L148 | verbatim |
|
|
| `requireRoles` / `validateArgs` semantics | `src/resolver/resolver.ts` L155-L186 | upgraded — replaces `throw new Error(...)` with `GraphQLError` (enterprise §3 "AuthN/AuthZ", `enterprise/api-plugin-graphql.md` L48) |
|
|
| `createQueryExecutedMiddleware` + `graphql:query:executed` event | `src/resolver/resolver.ts` L208-L234 | adapted — `EventEmitter` (upstream) → `EventBus<GraphQLEventMap>` (genesis events shape) |
|
|
| `graphqlEvents` shared bus | `src/resolver/resolver.ts` L192 | adapted — `new EventEmitter()` → `new EventBus<GraphQLEventMap>()` |
|
|
| `GraphQLQueryExecutedEvent` payload shape | `src/types/graphql.types.ts` L160-L171 | verbatim |
|
|
| All public type exports | `src/types/graphql.types.ts` full + `src/index.ts` | verbatim |
|
|
| `defineGraphQLPlugin` event shape: `GraphQLEventMap["graphql:query:executed"]` | `src/types/graphql.types.ts` L176-L178 | verbatim |
|
|
|
|
### Requirements with no source line
|
|
|
|
| Requirement | Justification |
|
|
|---|---|
|
|
| `GRAPHQL_CODES` registry + `GraphQLError` class | Genesis-wide policy (CLAUDE.md rules + AGENTS.md §13): every package extends `SuaveplanError` via `defineCodes`. Upstream threw bare `Error(...)`; flagged in `enterprise/api-plugin-graphql.md` L37, L48. |
|
|
| Zod validation at `defineGraphQLPlugin` boundary | Genesis-wide policy (AGENTS.md §13 row 2). Upstream did not validate options. |
|
|
| OTel `resolvers_executed_total` + `resolver_duration_ms` | Genesis-wide policy (AGENTS.md §13 row 5). `createQueryExecutedMiddleware` already emits the event; instruments are the OTel-side mirror. |
|
|
| 100% Istanbul coverage threshold | Genesis-wide policy. |
|
|
|
|
## Verification approach
|
|
|
|
Every requirement is verified by a concrete `it(...)` title:
|
|
|
|
- `src/plugin/plugin.test.ts` — `defineGraphQLPlugin` defaults, overrides, hooks, accessors, Zod boundary.
|
|
- `src/schema-builder/schema-builder.test.ts` — every builder method + counts.
|
|
- `src/type-builder/type-builder.test.ts` — `field` with all options + defaults.
|
|
- `src/resolver/resolver.test.ts` — `ResolverComposer`, middleware composition (global, per-type, combined), `requireRoles`, `validateArgs`, `createQueryExecutedMiddleware` event emission.
|
|
- `src/error-codes/error-codes.test.ts` — every `GRAPHQL_*` code thrown.
|
|
- `src/api-plugin-graphql.spec-conformance.test.ts` — type-level checks (`expectTypeOf`), every code thrown from a production path, OTel instruments emitted against a real `MeterProvider`.
|
|
|
|
## Migration impact
|
|
|
|
None — additive only. Genesis has no prior `@suaveplan/api-plugin-graphql` package.
|