Initial Commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,213 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-plugin-graphql (add-api-plugin-graphql)
|
||||
|
||||
**Package**: `@suaveplan/api-plugin-graphql`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: defineGraphQLPlugin produces a typed GraphQLPluginDefinition with defaulted config
|
||||
|
||||
The package SHALL expose `defineGraphQLPlugin(options)` returning a `GraphQLPluginDefinition` whose `config` field has `basePath`, `enableIntrospection`, `enablePlayground`, and `maxDepth` populated from caller-supplied values when present and from the package defaults (`/graphql`, `true`, `true`, `10`) otherwise. The returned object SHALL expose `name`, `config`, `hooks`, `getSchema()`, and `getResolvers()`.
|
||||
|
||||
#### Scenario: defaults applied when options omit config fields
|
||||
|
||||
- **WHEN** caller invokes `defineGraphQLPlugin({ name, version, schema, resolvers })` with no `basePath` / `enableIntrospection` / `enablePlayground` / `maxDepth`
|
||||
- **THEN** `definition.config.basePath === "/graphql"`, `enableIntrospection === true`, `enablePlayground === true`, `maxDepth === 10`
|
||||
|
||||
#### Scenario: caller overrides defaults
|
||||
|
||||
- **WHEN** caller passes explicit values for all four
|
||||
- **THEN** `definition.config` reflects every override verbatim
|
||||
|
||||
#### Scenario: lifecycle hooks pass through unchanged
|
||||
|
||||
- **WHEN** caller supplies `hooks: { onInit, onReady, onClose }`
|
||||
- **THEN** `definition.hooks` exposes the same callable references
|
||||
|
||||
**Verified by:** `src/plugin/plugin.test.ts::"should create a plugin with default config"` AND `src/plugin/plugin.test.ts::"should override default config values"` AND `src/plugin/plugin.test.ts::"should include lifecycle hooks"` AND `src/plugin/plugin.test.ts::"should handle missing hooks gracefully"` AND `src/plugin/plugin.test.ts::"should include onClose hook"` AND `src/plugin/plugin.test.ts::"should return schema via getSchema"` AND `src/plugin/plugin.test.ts::"should return resolvers via getResolvers"` AND `src/plugin/plugin.test.ts::"should include version in config"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: defineGraphQLPlugin Zod-validates its options at the boundary
|
||||
|
||||
The package SHALL run `DefineGraphQLPluginOptionsSchema.safeParse(options)` inside `defineGraphQLPlugin` and SHALL throw `GraphQLError(GRAPHQL.INVALID_INPUT)` carrying the `ZodError` as `cause` when validation fails.
|
||||
|
||||
#### Scenario: invalid options reject
|
||||
|
||||
- **WHEN** caller passes a value missing `name` / `version` / `schema` / `resolvers` or with a non-string `name`
|
||||
- **THEN** the call throws `GraphQLError` with `error.code === "GRAPHQL.INVALID_INPUT"` and `error.cause instanceof z.ZodError`
|
||||
|
||||
**Verified by:** `src/plugin/plugin.test.ts::"throws GRAPHQL.INVALID_INPUT when options fail Zod validation"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.INVALID_INPUT"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SchemaBuilder constructs SchemaDefinition without raw SDL
|
||||
|
||||
The package SHALL expose `SchemaBuilder` with `addType`, `addInputType`, `addEnumType`, `addInterfaceType`, `addUnionType`, `addQuery`, `addMutation`, `addSubscription`, `hasType`, `getType`, the read-only counters (`typeCount`, `queryCount`, `mutationCount`), and `build()` returning a `SchemaDefinition` whose `types` / `queries` / `mutations` / `subscriptions` arrays are independent snapshots.
|
||||
|
||||
#### Scenario: object / input / enum / interface / union types
|
||||
|
||||
- **WHEN** the caller invokes each `add*Type` method
|
||||
- **THEN** `getType(name).kind` returns the matching kind ("object" | "input" | "enum" | "interface" | "union")
|
||||
|
||||
#### Scenario: query / mutation / subscription registration
|
||||
|
||||
- **WHEN** caller invokes `addQuery` / `addMutation` / `addSubscription` with `required`, `isList`, `description`, `args` options
|
||||
- **THEN** the corresponding entry in `build().{queries,mutations,subscriptions}` carries every option verbatim, with `required` / `isList` defaulting to `false`
|
||||
|
||||
#### Scenario: description + interfaces on object type
|
||||
|
||||
- **WHEN** `addType(name, builder, { description, interfaces })` runs
|
||||
- **THEN** `getType(name)` returns `{description, interfaces}` matching the input
|
||||
|
||||
**Verified by:** `src/schema-builder/schema-builder.test.ts::"should add an object type"` AND `src/schema-builder/schema-builder.test.ts::"should support description and interfaces"` AND `src/schema-builder/schema-builder.test.ts::"should build type with correct kind"` AND `src/schema-builder/schema-builder.test.ts::"should add an input type"` AND `src/schema-builder/schema-builder.test.ts::"should support description"` AND `src/schema-builder/schema-builder.test.ts::"should add an enum type"` AND `src/schema-builder/schema-builder.test.ts::"should support description (enum)"` AND `src/schema-builder/schema-builder.test.ts::"should add an interface type"` AND `src/schema-builder/schema-builder.test.ts::"should support description (interface)"` AND `src/schema-builder/schema-builder.test.ts::"should add a union type"` AND `src/schema-builder/schema-builder.test.ts::"should support description (union)"` AND `src/schema-builder/schema-builder.test.ts::"should add a query field"` AND `src/schema-builder/schema-builder.test.ts::"should support args and description"` AND `src/schema-builder/schema-builder.test.ts::"should default required and isList to false"` AND `src/schema-builder/schema-builder.test.ts::"should add a mutation field"` AND `src/schema-builder/schema-builder.test.ts::"should support full options on mutation"` AND `src/schema-builder/schema-builder.test.ts::"should add a subscription field"` AND `src/schema-builder/schema-builder.test.ts::"should support full options on subscription"` AND `src/schema-builder/schema-builder.test.ts::"should return undefined for unknown type"` AND `src/schema-builder/schema-builder.test.ts::"should produce a complete schema definition"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: TypeBuilder chains field declarations
|
||||
|
||||
The package SHALL expose `TypeBuilder` with a `field(name, type, options?)` chainable method, a `fieldCount` getter, and a `build()` that returns a `TypeDefinition` with the configured `name`, `kind`, and an independent `fields` array. `required` and `isList` SHALL default to `false`.
|
||||
|
||||
#### Scenario: field with every option
|
||||
|
||||
- **WHEN** caller calls `field("items", "Item", { required: true, isList: true, description, defaultValue, args })`
|
||||
- **THEN** the resulting `TypeDefinition.fields[0]` carries every option verbatim
|
||||
|
||||
#### Scenario: defaults
|
||||
|
||||
- **WHEN** caller calls `field("x", "String")` with no options
|
||||
- **THEN** the resulting field has `required === false` and `isList === false`
|
||||
|
||||
**Verified by:** `src/type-builder/type-builder.test.ts::"should build fields with all options"` AND `src/type-builder/type-builder.test.ts::"should default required and isList to false"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ResolverComposer composes middleware around resolver functions
|
||||
|
||||
The package SHALL expose `ResolverComposer` with `use(mw)`, `useForType(type, mw)`, `addResolver(type, field, fn)`, `addResolvers(type, map)`, `hasResolver(type, field)`, `typeCount`, `resolverCount`, and `build()` returning a `ResolverMap`. Composition SHALL apply global middleware first, then per-type middleware, then the resolver — equivalent to right-to-left wrapping. Async resolvers SHALL be awaited transparently through the chain.
|
||||
|
||||
#### Scenario: addResolver / addResolvers / hasResolver
|
||||
|
||||
- **WHEN** caller registers resolvers via either method
|
||||
- **THEN** `hasResolver(type, field)` returns `true` for every registered slot and `false` otherwise
|
||||
|
||||
#### Scenario: global middleware chain
|
||||
|
||||
- **WHEN** two global middlewares wrap a resolver
|
||||
- **THEN** invoking the built resolver calls middleware-1, middleware-2, the resolver, in that order
|
||||
|
||||
#### Scenario: per-type middleware only applies to that type
|
||||
|
||||
- **WHEN** `useForType("Mutation", mw)` is set and both Query and Mutation resolvers run
|
||||
- **THEN** the middleware fires only for the Mutation resolver
|
||||
|
||||
#### Scenario: combined global + type middleware
|
||||
|
||||
- **WHEN** both a global and a type middleware are set
|
||||
- **THEN** global runs before type, type runs before resolver
|
||||
|
||||
#### Scenario: async resolver awaited
|
||||
|
||||
- **WHEN** the registered resolver returns a Promise
|
||||
- **THEN** `await map.Query.fieldName(...)` resolves to the awaited value
|
||||
|
||||
**Verified by:** `src/resolver/resolver.test.ts::"should add a resolver"` AND `src/resolver/resolver.test.ts::"should return false for non-existent resolver"` AND `src/resolver/resolver.test.ts::"should add multiple resolvers for a type"` AND `src/resolver/resolver.test.ts::"should build resolver map"` AND `src/resolver/resolver.test.ts::"should handle async resolvers"` AND `src/resolver/resolver.test.ts::"should apply global middleware to all resolvers"` AND `src/resolver/resolver.test.ts::"should chain multiple global middlewares"` AND `src/resolver/resolver.test.ts::"should apply type middleware only to that type"` AND `src/resolver/resolver.test.ts::"should apply global then type middleware"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: requireRoles middleware enforces authentication and role intersection
|
||||
|
||||
The package SHALL expose `requireRoles(...roles)` returning a `ResolverMiddleware` that throws `GraphQLError(GRAPHQL.UNAUTHENTICATED)` when `context.user` is absent and `GraphQLError(GRAPHQL.INSUFFICIENT_ROLE)` when the user's `roles` do not intersect the required set. When the check passes, control SHALL delegate to `next(parent, args, context, info)` unchanged.
|
||||
|
||||
#### Scenario: authenticated user with required role passes
|
||||
|
||||
- **WHEN** `context.user.roles` contains one of the required roles
|
||||
- **THEN** the resolver runs and its return value flows through
|
||||
|
||||
#### Scenario: authenticated user with any-of roles passes
|
||||
|
||||
- **WHEN** the required set is `["admin", "editor"]` and the user has only `"editor"`
|
||||
- **THEN** the resolver runs
|
||||
|
||||
#### Scenario: missing user throws UNAUTHENTICATED
|
||||
|
||||
- **WHEN** `context.user` is undefined
|
||||
- **THEN** the middleware throws `GraphQLError` with `code === "GRAPHQL.UNAUTHENTICATED"`
|
||||
|
||||
#### Scenario: insufficient role throws INSUFFICIENT_ROLE
|
||||
|
||||
- **WHEN** `context.user.roles` shares no element with the required set
|
||||
- **THEN** the middleware throws `GraphQLError` with `code === "GRAPHQL.INSUFFICIENT_ROLE"`
|
||||
|
||||
**Verified by:** `src/resolver/resolver.test.ts::"should pass when user has required role"` AND `src/resolver/resolver.test.ts::"should pass when user has any of required roles"` AND `src/resolver/resolver.test.ts::"should throw GRAPHQL.UNAUTHENTICATED when user is not authenticated"` AND `src/resolver/resolver.test.ts::"should throw GRAPHQL.INSUFFICIENT_ROLE when user lacks required roles"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.UNAUTHENTICATED"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.INSUFFICIENT_ROLE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: validateArgs middleware enforces argument validation
|
||||
|
||||
The package SHALL expose `validateArgs(validator)` returning a `ResolverMiddleware` that calls `validator(args)`. When the validator returns a non-null string, the middleware SHALL throw `GraphQLError(GRAPHQL.VALIDATION_FAILED)` carrying the validator's message; when it returns `null`, control SHALL pass to `next`.
|
||||
|
||||
#### Scenario: validator returns null
|
||||
|
||||
- **WHEN** the validator returns `null`
|
||||
- **THEN** the resolver runs and its return value flows through
|
||||
|
||||
#### Scenario: validator returns an error string
|
||||
|
||||
- **WHEN** the validator returns `"name is required"`
|
||||
- **THEN** the middleware throws `GraphQLError` with `code === "GRAPHQL.VALIDATION_FAILED"` and `.message` containing the validator's string
|
||||
|
||||
**Verified by:** `src/resolver/resolver.test.ts::"should pass when validation returns null"` AND `src/resolver/resolver.test.ts::"should throw GRAPHQL.VALIDATION_FAILED when validation returns error string"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.VALIDATION_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: createQueryExecutedMiddleware emits graphql:query:executed event and OTel instruments
|
||||
|
||||
The package SHALL expose `createQueryExecutedMiddleware(bus?)` returning a `ResolverMiddleware` that times resolver execution and emits a typed `graphql:query:executed` event onto the supplied `EventBus<GraphQLEventMap>` (or `graphqlEvents` when no bus is supplied). The payload SHALL include `operationName` (`info.fieldName`), `operationType` (`info.parentType`), `durationMs`, `hasErrors`, and `timestamp`. The middleware SHALL increment `graphql.resolvers_executed_total{outcome}` and record `graphql.resolver_duration_ms` for every invocation regardless of success or failure.
|
||||
|
||||
#### Scenario: successful invocation emits event with hasErrors=false
|
||||
|
||||
- **WHEN** the wrapped resolver returns normally
|
||||
- **THEN** a `graphql:query:executed` event is observed with `hasErrors === false`, `operationName === info.fieldName`, and `durationMs >= 0`
|
||||
|
||||
#### Scenario: thrown resolver emits event with hasErrors=true and re-throws
|
||||
|
||||
- **WHEN** the wrapped resolver throws
|
||||
- **THEN** the same event type fires with `hasErrors === true` AND the original error propagates
|
||||
|
||||
#### Scenario: OTel instruments record under real MeterProvider
|
||||
|
||||
- **WHEN** a real in-memory `MeterProvider` is installed and the middleware runs once successfully and once with a throw
|
||||
- **THEN** `graphql.resolvers_executed_total` exposes data points with `outcome=success` and `outcome=error` AND `graphql.resolver_duration_ms` exposes histogram data points
|
||||
|
||||
**Verified by:** `src/resolver/resolver.test.ts::"createQueryExecutedMiddleware emits graphql:query:executed on success"` AND `src/resolver/resolver.test.ts::"createQueryExecutedMiddleware emits graphql:query:executed and re-throws on error"` AND `src/resolver/resolver.test.ts::"createQueryExecutedMiddleware honors a caller-supplied EventBus"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"graphql.resolvers_executed_total + resolver_duration_ms recorded via real MeterProvider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: error-codes registry exposes every GRAPHQL.* code as a thrown failure
|
||||
|
||||
The package SHALL define `GRAPHQL_CODES = defineCodes("GRAPHQL", [...])` with the six codes `UNAUTHENTICATED`, `INSUFFICIENT_ROLE`, `VALIDATION_FAILED`, `INVALID_INPUT`, `TYPE_NOT_FOUND`, `DUPLICATE_RESOLVER`. Every code SHALL be thrown from at least one production path. `GraphQLError extends SuaveplanError<GraphQLErrorCode>`.
|
||||
|
||||
#### Scenario: every code is reachable
|
||||
|
||||
- **WHEN** the conformance test triggers each documented failure path
|
||||
- **THEN** every `GRAPHQL.*` code appears as the `code` field on a caught `GraphQLError`
|
||||
|
||||
**Verified by:** `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.UNAUTHENTICATED"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.INSUFFICIENT_ROLE"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.VALIDATION_FAILED"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.INVALID_INPUT"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.TYPE_NOT_FOUND"` AND `src/api-plugin-graphql.spec-conformance.test.ts::"GRAPHQL.DUPLICATE_RESOLVER"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: type surface is preserved and the public API is generic-safe
|
||||
|
||||
The package SHALL export every type declared by upstream verbatim: `SchemaDefinition`, `TypeDefinition`, `TypeKind`, `FieldDefinition`, `ScalarType`, `QueryContext`, `ResolverFn`, `ResolverInfo`, `ResolverMiddleware`, `ResolverMap`, `GraphQLPluginConfig`, `GraphQLPluginHooks`, `GraphQLQueryExecutedEvent`, `GraphQLEventMap`, `DefineGraphQLPluginOptions`, `GraphQLPluginDefinition`. The `EventBus<GraphQLEventMap>` typing SHALL flow `graphql:query:executed` payloads through `bus.on(...)` listeners without casts.
|
||||
|
||||
#### Scenario: type-level event payload narrows
|
||||
|
||||
- **WHEN** `expectTypeOf` introspects `graphqlEvents.on("graphql:query:executed", listener)`
|
||||
- **THEN** `listener` is typed `(payload: GraphQLQueryExecutedEvent) => void | Promise<void>`
|
||||
|
||||
**Verified by:** `src/api-plugin-graphql.spec-conformance.test.ts::"public types are generic-safe and event payload is typed"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
59
openspec/archive/2026-05-18-add-api-plugin-graphql/tasks.md
Normal file
59
openspec/archive/2026-05-18-add-api-plugin-graphql/tasks.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Tasks — add @suaveplan/api-plugin-graphql
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `../suaveplan/enterprise/api-plugin-graphql.md` end-to-end
|
||||
- [x] 1.2 Read genesis `openspec/specs/` (no prior `api-plugin-graphql` capability)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` §§ 2a, 6a, 7, 12, 13, 14
|
||||
- [x] 1.4 Draft `proposal.md` from template
|
||||
- [x] 1.5 Fill `## Source mapping` — every requirement cites file/line or appears under "no source line" with justification
|
||||
- [x] 1.6 Draft `specs/api-plugin-graphql/spec.md` from template
|
||||
- [x] 1.7 Every requirement carries `**Verified by:**` lines naming concrete `it(...)` titles
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 `src/types/graphql.types.ts` — full type surface ported from upstream
|
||||
- [x] 2.2 `src/error-codes/error-codes.ts` — `GRAPHQL_CODES` via `defineCodes`, `GraphQLError` class
|
||||
- [x] 2.3 `src/schemas/options.ts` — Zod schema for `DefineGraphQLPluginOptions`
|
||||
- [x] 2.4 `src/type-builder/type-builder.ts` — `TypeBuilder`
|
||||
- [x] 2.5 `src/schema-builder/schema-builder.ts` — `SchemaBuilder`
|
||||
- [x] 2.6 `src/resolver/resolver.ts` — `ResolverComposer`, `requireRoles`, `validateArgs`, `createQueryExecutedMiddleware`, `graphqlEvents`
|
||||
- [x] 2.7 `src/otel/instruments.ts` — lazy-bound `MeterProvider` instruments
|
||||
- [x] 2.8 `src/plugin/plugin.ts` — `defineGraphQLPlugin` with Zod validation at boundary
|
||||
- [x] 2.9 `src/index.ts` — explicit named re-exports + side-effect `./error-codes/error-codes.js` import
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests beside every implementation file
|
||||
- [x] 3.2 `src/api-plugin-graphql.spec-conformance.test.ts` — type-level + every error code + OTel emission
|
||||
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for async)
|
||||
- [x] 3.4 Test files import only from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
|
||||
- [x] 4.2 README at package root
|
||||
|
||||
## 5. Pre-archive verification (5 gates)
|
||||
|
||||
- [x] 5.1 `bun run check`
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin-graphql`
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-plugin-graphql`
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-plugin-graphql`
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-plugin-graphql`
|
||||
|
||||
## 6. Pre-archive checklist (AGENTS.md §13)
|
||||
|
||||
- [x] 6.1 Generics threaded; `expectTypeOf` assertions on literal-type claims
|
||||
- [x] 6.2 Zod schemas wired at the declared boundary
|
||||
- [x] 6.3 Every `GRAPHQL_*` code thrown by ≥1 production path
|
||||
- [x] 6.4 Strict-mode behaviors tested (auth pass + fail; validate pass + fail)
|
||||
- [x] 6.5 OTel instruments emitted from production paths and asserted with real in-memory `MeterProvider`
|
||||
- [x] 6.6 No retroactive tick — every box reflects work done in this change
|
||||
- [x] 6.7 No `export *` in any feature-module barrel
|
||||
- [x] 6.8 No direct `vitest`/`bun:test` imports in test files
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-plugin-graphql/spec.md`
|
||||
- [x] 7.2 Move change folder to `openspec/archive/2026-05-18-add-api-plugin-graphql/`
|
||||
Reference in New Issue
Block a user