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,64 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable `GRAPHQL.*` error-code registry for `@suaveplan/api-plugin-graphql` and the `GraphQLError` class consumers catch and discriminate on. Every failure path inside the package — Zod validation at `defineGraphQLPlugin`, role enforcement in `requireRoles`, argument validation in `validateArgs`, schema lookups, and duplicate-resolver registration — throws a `GraphQLError` carrying one of these codes.
Upstream threw bare `Error(...)` strings; policy (CLAUDE.md rules + AGENTS.md §13) requires typed errors extending `SuaveplanError`. This file replaces every upstream `throw new Error("Authentication required")` etc. with `throw new GraphQLError(GRAPHQL_CODES.UNAUTHENTICATED, ...)` so consumers can branch on `error.code` instead of `error.message`.
## Features
- 6 stable codes registered via `defineCodes("GRAPHQL", [...])`.
- `GraphQLError extends SuaveplanError<GraphQLErrorCode>` — `instanceof GraphQLError` narrows `code`.
- Optional `SuaveplanErrorOptions` (`cause`, `context`, `redact`) on every constructor call.
## Usage: Basic
```ts
import { GraphQLError, GRAPHQL_CODES } from "@suaveplan/api-plugin-graphql";
try {
plugin = defineGraphQLPlugin({ name: 1 as unknown as string, ...rest });
} catch (e) {
if (e instanceof GraphQLError && e.code === GRAPHQL_CODES.INVALID_INPUT) {
console.error("invalid options", e.cause);
}
}
```
## Usage: Advanced
```ts
import { GraphQLError, GRAPHQL_CODES } from "@suaveplan/api-plugin-graphql";
function safeAddResolver(c: ResolverComposer, t: string, f: string, fn: ResolverFn) {
if (c.hasResolver(t, f)) {
throw new GraphQLError(
GRAPHQL_CODES.DUPLICATE_RESOLVER,
`resolver already exists for ${t}.${f}`,
{ context: { typeName: t, fieldName: f } },
);
}
c.addResolver(t, f, fn);
}
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `GRAPHQL_CODES` | const | `{ UNAUTHENTICATED, INSUFFICIENT_ROLE, VALIDATION_FAILED, INVALID_INPUT, TYPE_NOT_FOUND, DUPLICATE_RESOLVER }` — each value is the string `"GRAPHQL.<NAME>"`. |
| `GraphQLErrorCode` | type | Union of the six string literal codes. |
| `GraphQLError` | class | Extends `SuaveplanError<GraphQLErrorCode>`. Constructor: `(code, message, options?)`. `error.graphqlCode` mirrors `error.code`. |
## Implementation Notes
`defineCodes` (from `@suaveplan/error`) returns a frozen const record whose value type is preserved through `(typeof GRAPHQL_CODES)[keyof typeof GRAPHQL_CODES]`. This means `error.code` is the literal union, not `string`.
The base class constructor in `@suaveplan/error` is `protected`; we widen it to `public` so callers can construct errors directly when wrapping third-party failures (e.g. mapping a foreign cause). `graphqlCode` is a strongly-typed mirror of `.code` that survives transport boundaries even if a consumer narrows on `instanceof Error` instead of `instanceof GraphQLError`.
Every code in `GRAPHQL_CODES` is thrown from at least one production code path; the `src/api-plugin-graphql.spec-conformance.test.ts` file pins this invariant.

View File

@@ -0,0 +1,77 @@
# otel/instruments
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the OpenTelemetry instruments emitted from `createQueryExecutedMiddleware`. The middleware is the only production code path that records metrics, so this file is the single source of truth for what the package observes.
Two instruments cover both the success and failure paths of every resolver wrapped in the middleware: a counter partitioned by outcome label, and a duration histogram. Tests assert against a real in-memory `MeterProvider` to satisfy AGENTS.md §13 row 5.
## Features
- `graphql.resolvers_executed_total{outcome}` — counter, label `outcome ∈ "success" | "error"`.
- `graphql.resolver_duration_ms` — histogram in milliseconds.
- Lazy global-provider binding via `metrics.getMeter(...)`, so the meter resolved at module load is whatever provider was installed at the time of the first call.
- `resetGraphQLInstruments()` test hook for re-binding against freshly installed providers.
## Usage: Basic
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { resetGraphQLInstruments } from "@suaveplan/api-plugin-graphql/otel/instruments";
const provider = new MeterProvider({ readers: [] });
metrics.setGlobalMeterProvider(provider);
resetGraphQLInstruments();
```
## Usage: Advanced
```ts
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
import {
createQueryExecutedMiddleware,
ResolverComposer,
} from "@suaveplan/api-plugin-graphql";
import { resetGraphQLInstruments } from "@suaveplan/api-plugin-graphql/otel/instruments";
const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 });
const provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
resetGraphQLInstruments();
const map = new ResolverComposer()
.useForType("Query", createQueryExecutedMiddleware())
.addResolver("Query", "ping", () => "pong")
.build();
await map.Query.ping(null, {}, { headers: {}, data: {} }, { fieldName: "ping", parentType: "Query", returnType: "String" });
await provider.forceFlush();
const points = exporter.getMetrics();
// inspect points[0].scopeMetrics[0].metrics
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `GraphQLInstruments` | interface | `{ resolversExecutedTotal: Counter, resolverDurationMs: Histogram }`. |
| `getGraphQLInstruments()` | function | Returns the cached bundle, lazily binding against the current global `MeterProvider`. |
| `resetGraphQLInstruments()` | function | Drops the cache; the next `getGraphQLInstruments()` rebinds against whatever provider is then installed. |
## Implementation Notes
The module deliberately reads `metrics.getMeter` lazily — calling it at import time would bind against the noop provider that ships in `@opentelemetry/api` before any consumer (or test setup) installs a real one. suaveplan test setup installs a real `MeterProvider` in `tests/setup/polyfills.ts`, then calls `resetGraphQLInstruments()` (or its equivalent through the test fixture) to force the first call inside a test to bind against the real provider.
Instrument naming follows the `<package>.<metric>` convention used across the API tier (`auth.*`, `plugin.*`, `graphql.*`). The `outcome` label space is intentionally small (`"success" | "error"`) — finer partitions belong on span attributes, not on counter labels, to keep cardinality bounded.

View File

@@ -0,0 +1,101 @@
# plugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares `defineGraphQLPlugin(options)`, the factory that produces a `GraphQLPluginDefinition` consumable by the Suaveplan API plugin system. The factory:
1. Zod-validates the supplied options at the boundary (single chokepoint for invalid input).
2. Merges caller-supplied values with the four package defaults (`basePath="/graphql"`, `enableIntrospection=true`, `enablePlayground=true`, `maxDepth=10`).
3. Exposes the schema + resolvers behind `getSchema()` / `getResolvers()` accessors so downstream executors (or test harnesses) can re-read the merged config without parsing the `GraphQLPluginConfig` field.
`defineGraphQLPlugin` is the only public entry point of the package. Everything else (`SchemaBuilder`, `ResolverComposer`, middleware factories) is in service of producing the `schema` and `resolvers` fields fed into this factory.
## Features
- Strict Zod validation at the boundary; failure throws `GraphQLError(GRAPHQL.INVALID_INPUT)` with `cause: ZodError`.
- Per-field defaults applied via nullish coalescing — explicit `undefined` triggers the default, explicit `false` / `0` survives.
- Lifecycle hooks passed through verbatim (caller controls them).
- Returns plain objects; no class instances; safe to JSON-serialize except for `hooks` and `schema.resolvers`.
## Usage: Basic
```ts
import {
defineGraphQLPlugin,
ResolverComposer,
SchemaBuilder,
} from "@suaveplan/api-plugin-graphql";
const schema = new SchemaBuilder()
.addType("User", (t) => t.field("id", "ID", { required: true }))
.addQuery("user", "User", {
args: [{ name: "id", type: "ID", required: true, isList: false }],
})
.build();
const resolvers = new ResolverComposer()
.addResolver("Query", "user", (_p, args) => ({ id: args.id, name: "Alice" }))
.build();
const definition = defineGraphQLPlugin({
name: "my-graphql",
version: "1.0.0",
schema,
resolvers,
});
```
## Usage: Advanced
```ts
import {
createQueryExecutedMiddleware,
defineGraphQLPlugin,
ResolverComposer,
SchemaBuilder,
} from "@suaveplan/api-plugin-graphql";
const definition = defineGraphQLPlugin({
name: "user-api",
version: "2.0.0",
basePath: "/api/graphql",
enableIntrospection: false,
enablePlayground: false,
maxDepth: 5,
schema: schemaBuilder.build(),
resolvers: composer
.useForType("Query", createQueryExecutedMiddleware())
.build(),
hooks: {
async onInit() {
console.log("graphql plugin initializing");
},
async onReady() {
console.log("graphql plugin ready");
},
async onClose() {
console.log("graphql plugin closing");
},
},
});
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `DefineGraphQLPluginOptions` | interface | Caller-facing input. `schema` / `resolvers` required; everything else optional. |
| `GraphQLPluginDefinition` | interface | Output shape: `name`, `config`, `hooks`, `getSchema()`, `getResolvers()`. |
| `defineGraphQLPlugin(options)` | function | Zod-validates `options`; throws `GraphQLError(INVALID_INPUT)` on failure. |
## Implementation Notes
The factory deliberately does **not** invoke `@suaveplan/api-plugin`'s `definePlugin()` wrapper. Upstream's contract is a plain `GraphQLPluginDefinition` (name + config + hooks + accessors); the migration preserves that contract verbatim. A future `createGraphQLPlugin()` that wraps this output in a `PluginDescriptor` and mounts a Hono route is enumerated in `enterprise/api-plugin-graphql.md` §3 as a separate enterprise upgrade and is out of scope for the migration.
`hooks` is constructed conditionally: under `exactOptionalPropertyTypes`, assigning `undefined` to an optional property is a type error, so the factory only sets `onInit` / `onReady` / `onClose` when the caller supplied a function. The unit tests pin this — `plugin.hooks.onInit` is `undefined` when no hook was supplied, not the literal `undefined` after a noisy assignment.
Defaulting uses `??` so explicit `false` / `0` (where applicable) flows through unchanged.

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.

View File

@@ -0,0 +1,85 @@
# schema-builder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares `SchemaBuilder`, the fluent builder for GraphQL schema definitions. Consumers describe types, queries, mutations, and subscriptions via chained method calls; `build()` returns a fully resolved `SchemaDefinition` consumed by `defineGraphQLPlugin` (or by any future executor).
`SchemaBuilder` is the canonical authoring surface — callers should not construct `SchemaDefinition` literals by hand. The builder enforces sensible defaults (`required: false`, `isList: false`), maintains insertion order for downstream determinism, and round-trips description/interfaces metadata correctly.
## Features
- `addType` / `addInputType` / `addEnumType` / `addInterfaceType` / `addUnionType` — every GraphQL type kind except `scalar` (consumers register scalars as plain strings on field types).
- `addQuery` / `addMutation` / `addSubscription` — root-field declarations with optional `required`, `isList`, `description`, `args`.
- `hasType` / `getType` — lookup helpers.
- `typeCount` / `queryCount` / `mutationCount` — read-only counters.
- `build()` — snapshot into an independent `SchemaDefinition`.
## Usage: Basic
```ts
import { SchemaBuilder } from "@suaveplan/api-plugin-graphql";
const schema = new SchemaBuilder()
.addType("User", (t) =>
t.field("id", "ID", { required: true })
.field("name", "String", { required: true })
)
.addQuery("user", "User", {
args: [{ name: "id", type: "ID", required: true, isList: false }],
})
.build();
```
## Usage: Advanced
```ts
import { SchemaBuilder } from "@suaveplan/api-plugin-graphql";
const schema = new SchemaBuilder()
.addEnumType("Role", ["ADMIN", "USER", "GUEST"], "Authorization role")
.addInterfaceType("Node", (t) => t.field("id", "ID", { required: true }))
.addType("Account", (t) =>
t.field("id", "ID", { required: true }).field("role", "Role", { required: true }),
{ description: "User account", interfaces: ["Node"] }
)
.addUnionType("SearchHit", ["Account", "Post"], "Polymorphic search result")
.addInputType("CreateAccountInput", (t) =>
t.field("name", "String", { required: true })
.field("role", "Role", { required: true })
)
.addMutation("createAccount", "Account", {
required: true,
args: [{ name: "input", type: "CreateAccountInput", required: true, isList: false }],
})
.addSubscription("onAccountCreated", "Account", { required: true })
.build();
```
## API Reference
| Method | Returns | Notes |
|---|---|---|
| `addType(name, builderFn, options?)` | `this` | `options.description`, `options.interfaces`. |
| `addInputType(name, builderFn, description?)` | `this` | |
| `addEnumType(name, values, description?)` | `this` | `values` is copied. |
| `addInterfaceType(name, builderFn, description?)` | `this` | |
| `addUnionType(name, memberTypes, description?)` | `this` | `memberTypes` stored on `enumValues`. |
| `addQuery(name, returnType, options?)` | `this` | `required`/`isList`/`description`/`args`. |
| `addMutation(name, returnType, options?)` | `this` | Same options as queries. |
| `addSubscription(name, returnType, options?)` | `this` | Same options as queries. |
| `hasType(name)` | `boolean` | |
| `getType(name)` | `TypeDefinition \| undefined` | |
| `typeCount` / `queryCount` / `mutationCount` | `number` | |
| `build()` | `SchemaDefinition` | Returns an independent snapshot. |
## Implementation Notes
`addType` / `addInterfaceType` / `addInputType` re-construct the `TypeDefinition` from the `TypeBuilder` output to merge `description` and `interfaces` cleanly under `exactOptionalPropertyTypes` — TypeScript rejects assigning `undefined` to an optional property when that strict flag is on, so each merge spreads conditionally.
Union types reuse `enumValues` to carry their member-type name list. This matches upstream and keeps `TypeDefinition` flat — adding a separate `memberTypes` field would have introduced a new public type with no consumer benefit. Downstream code distinguishes via `kind === "union"`.
`build()` is the only method that exposes structure to callers; until then, the builder owns its internal `Map` / arrays. Each `build()` call returns fresh arrays so callers may mutate them safely or invoke `build()` multiple times during incremental construction.

View File

@@ -0,0 +1,62 @@
# schemas/options
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines `DefineGraphQLPluginOptionsSchema`, the Zod schema wired at the package boundary inside `defineGraphQLPlugin`. The schema enforces that the four scalar config knobs (`basePath`, `enableIntrospection`, `enablePlayground`, `maxDepth`) and the structural fields (`name`, `version`, `schema`, `resolvers`, `hooks`) are well-formed before any defaults are merged. Validation failure raises `GraphQLError(GRAPHQL.INVALID_INPUT)` carrying the `ZodError` as `cause`.
This boundary check is the only public-input gate in the package. The rest of the surface accepts strongly typed values (e.g. `SchemaBuilder.build()` already returns a `SchemaDefinition` whose structure is enforced by TypeScript); it would be redundant to re-validate those shapes deeply at runtime. The schema therefore validates `schema.types` / `queries` / `mutations` / `subscriptions` shallowly (`array(unknown)`) — the heavy lifting of structural correctness happens in `SchemaBuilder`.
## Features
- Validates required `name` / `version` strings (non-empty).
- Validates optional `basePath` (non-empty), `enableIntrospection` (bool), `enablePlayground` (bool), `maxDepth` (positive int).
- Validates the four schema container arrays exist.
- Accepts a `resolvers` record-of-records.
- Accepts an optional `hooks` object with three optional function fields.
## Usage: Basic
```ts
import { DefineGraphQLPluginOptionsSchema } from "@suaveplan/api-plugin-graphql/schemas/options";
const parsed = DefineGraphQLPluginOptionsSchema.safeParse(userInput);
if (!parsed.success) handle(parsed.error);
```
## Usage: Advanced
The schema is intentionally exported as a subpath so consumers building their own descriptors (e.g. a code-generator that emits options) can validate intermediate values without spinning up the full `defineGraphQLPlugin` factory:
```ts
import { DefineGraphQLPluginOptionsSchema } from "@suaveplan/api-plugin-graphql/schemas/options";
function buildOptionsFromConfig(c: AppConfig) {
const candidate = {
name: c.gqlName,
version: c.gqlVersion,
basePath: c.gqlBasePath,
enableIntrospection: c.env !== "production",
enablePlayground: c.env !== "production",
maxDepth: c.gqlMaxDepth,
schema: schemaBuilder.build(),
resolvers: composer.build(),
};
return DefineGraphQLPluginOptionsSchema.parse(candidate);
}
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `DefineGraphQLPluginOptionsSchema` | `z.ZodObject` | Use `.parse(input)` or `.safeParse(input)`. Invalid → `ZodError`. |
## Implementation Notes
`z.record(z.string(), z.record(z.string(), z.unknown()))` is the most accurate runtime shape — Zod cannot represent the static `ResolverFn` signature at runtime because functions cannot be deep-introspected. The `defineGraphQLPlugin` factory casts the parsed value through to the strongly-typed `DefineGraphQLPluginOptions` (defined in `plugin/plugin.ts`).
The boundary check exists once and only once at the public input. Internal helpers (`SchemaBuilder`, `ResolverComposer`) trust their inputs because their constructors / methods are typed.

View File

@@ -0,0 +1,63 @@
# type-builder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares the `TypeBuilder` class — the inner builder `SchemaBuilder` hands to user-supplied callbacks when declaring object, input, or interface types. `TypeBuilder` accumulates a list of `FieldDefinition` records and freezes them into a `TypeDefinition` via `build()`. It is the single chokepoint for adding fields to a GraphQL type in the DSL, so every default (`required: false`, `isList: false`) is enforced here.
The builder is intentionally tiny — fluent `.field(...)` chaining and a final `build()`. Heavier concerns (type-level validation, dedup, SDL emission) live elsewhere; `TypeBuilder` just accumulates structurally well-formed records.
## Features
- Chainable `field(name, type, options?)`.
- `required` and `isList` default to `false`.
- Optional `description`, `defaultValue`, `args`.
- `fieldCount` getter.
- `build()` returns a fresh array per call.
## Usage: Basic
```ts
import { TypeBuilder } from "@suaveplan/api-plugin-graphql";
const userType = new TypeBuilder("User", "object")
.field("id", "ID", { required: true })
.field("email", "String", { required: true })
.build();
```
## Usage: Advanced
```ts
import { TypeBuilder } from "@suaveplan/api-plugin-graphql";
import type { FieldDefinition } from "@suaveplan/api-plugin-graphql";
const limitArg: FieldDefinition = { name: "limit", type: "Int", required: false, isList: false };
const postType = new TypeBuilder("Post", "object")
.field("id", "ID", { required: true })
.field("title", "String", { required: true, description: "Post title" })
.field("tags", "String", { isList: true, defaultValue: [] })
.field("comments", "Comment", { isList: true, args: [limitArg] })
.build();
console.log(postType.fields.length); // 4
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `new TypeBuilder(name, kind)` | constructor | `kind` is one of `"object"`/`"input"`/`"interface"`. Other kinds (`enum`, `union`, `scalar`) bypass `TypeBuilder`. |
| `.field(name, type, options?)` | method | Chainable. Pushes a `FieldDefinition`. |
| `.fieldCount` | getter | Number of fields declared so far. |
| `.build()` | method | Returns a `TypeDefinition` snapshot. |
## Implementation Notes
The constructor signature accepts the full `TypeKind` union for forward compatibility, but `SchemaBuilder` only instantiates `TypeBuilder` with `"object"`/`"input"`/`"interface"` — the other kinds are stored as bare `TypeDefinition` records because they have no field list.
`required` and `isList` defaults match upstream behavior. Callers who want non-null by default should pass `{ required: true }` explicitly per-field — there is intentionally no schema-wide "non-null by default" knob; that would diverge from upstream.

View File

@@ -0,0 +1,73 @@
# graphql.types
## Purpose
Defines the full public type surface for `@suaveplan/api-plugin-graphql`. Every other module in the package consumes its symbols from this file, so the file behaves as the contract between schema construction, resolver composition, plugin definition, and event emission.
These types are intentionally description-layer only: they capture the **shape** of a GraphQL API (types, fields, queries, mutations, subscriptions, resolvers, middleware, plugin config) without invoking `graphql-js` or any executor. The package preserves the upstream contract verbatim so consumers can swap implementations without code change.
## Features
- `SchemaDefinition`, `TypeDefinition`, `FieldDefinition`, `TypeKind`, `ScalarType` — describe the schema graph.
- `QueryContext`, `ResolverFn`, `ResolverInfo`, `ResolverMap`, `ResolverMiddleware` — describe resolver runtime.
- `GraphQLPluginConfig`, `GraphQLPluginHooks` — describe the plugin descriptor.
- `GraphQLQueryExecutedEvent` + `GraphQLEventMap` — describe the observability channel.
## Usage: Basic
```ts
import type { SchemaDefinition, ResolverMap } from "@suaveplan/api-plugin-graphql";
const schema: SchemaDefinition = {
types: [{ name: "User", kind: "object", fields: [{ name: "id", type: "ID", required: true, isList: false }] }],
queries: [{ name: "user", type: "User", required: false, isList: false }],
mutations: [],
subscriptions: [],
};
const resolvers: ResolverMap = {
Query: { user: (_p, args) => ({ id: args.id }) },
};
```
## Usage: Advanced
```ts
import type { ResolverMiddleware, QueryContext, ResolverInfo } from "@suaveplan/api-plugin-graphql";
const trace: ResolverMiddleware = async (next, p, a, c: QueryContext, i: ResolverInfo) => {
const t0 = performance.now();
try {
return await next(p, a, c, i);
} finally {
console.log(`${i.parentType}.${i.fieldName} ${(performance.now() - t0).toFixed(2)}ms`);
}
};
```
## API Reference
| Symbol | Kind | Notes |
|---|---|---|
| `GraphQLPluginConfig` | interface | Output of `defineGraphQLPlugin`, includes `name`, `version`, `basePath`, `enableIntrospection`, `enablePlayground`, `maxDepth`, `schema`, `resolvers`. |
| `ScalarType` | union | `"String"`/`"Int"`/`"Float"`/`"Boolean"`/`"ID"`/`"DateTime"`/`"JSON"`. |
| `FieldDefinition` | interface | `name`, `type`, `required`, `isList`, optional `description`, `defaultValue`, `args`. |
| `TypeDefinition` | interface | `name`, `kind`, `fields`, optional `enumValues`, `description`, `interfaces`. |
| `TypeKind` | union | `"object"`/`"input"`/`"enum"`/`"interface"`/`"union"`/`"scalar"`. |
| `QueryContext` | interface | `user?`, `headers`, `data`. |
| `ResolverFn` | type | `(parent, args, context, info) => unknown \| Promise<unknown>`. |
| `ResolverInfo` | interface | `fieldName`, `parentType`, `returnType`. |
| `ResolverMap` | type | `Record<typeName, Record<fieldName, ResolverFn>>`. |
| `ResolverMiddleware` | type | wraps `ResolverFn` with a `next` parameter. |
| `SchemaDefinition` | interface | `types`/`queries`/`mutations`/`subscriptions`. |
| `GraphQLPluginHooks` | interface | `onInit?`/`onReady?`/`onClose?`. |
| `GraphQLQueryExecutedEvent` | interface | `operationName`/`operationType`/`durationMs`/`hasErrors`/`timestamp`. |
| `GraphQLEventMap` | interface | `"graphql:query:executed": GraphQLQueryExecutedEvent`. |
## Implementation Notes
The interfaces are `readonly` where upstream was `readonly`; `GraphQLQueryExecutedEvent` is intentionally writable because it is constructed once per resolver invocation and immediately handed to the event bus.
`GraphQLEventMap` is consumed by `EventBus<GraphQLEventMap>` from `@suaveplan/events`, providing typed `bus.on("graphql:query:executed", payload => …)`.
This file ships **only** type declarations — no runtime emit. The build's `tsup`/`vite` output strips this module entirely.