# 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` 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` (genesis events shape) | | `graphqlEvents` shared bus | `src/resolver/resolver.ts` L192 | adapted — `new EventEmitter()` → `new EventBus()` | | `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.