Initial Commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# `@suaveplan/api-graphql-ui-graphiql` — GraphiQL UI mount at `/graphql/ui`
|
||||
|
||||
**Package**: `@suaveplan/api-graphql-ui-graphiql`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §10.6 the foundation reserves `GET /graphql/ui` for an interactive query explorer but does not bundle one by default — UI choice is opt-in via a separate package so the host's runtime image stays minimal for production deployments that don't need it. GraphiQL is the default option called out in §10.6 ("`GET /graphql/ui` — GraphiQL (or Altair via `api-graphql-ui-altair`)"). Operators wire this package when they want the canonical GraphQL UI; they wire `api-graphql-ui-altair` instead when they prefer Altair.
|
||||
|
||||
The GraphQL engine adapters (`api-graphql-yoga`, `api-graphql-apollo`) intentionally return 404 at `/graphql/ui` when no UI plugin is wired — so this package is the standard way to give developers an in-browser explorer.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-router`, `@suaveplan/api-graphql`, `@suaveplan/error`, `@suaveplan/logger`, and `graphiql` (peer; static assets served from this package, not fetched from a CDN).
|
||||
|
||||
- ADDED: `createGraphiqlUi(opts)` — factory returning a `RouterPlugin` value that registers a `GET <uiEndpoint>` handler serving the GraphiQL HTML shell + assets.
|
||||
- ADDED: `GraphiqlUiOptions` — Zod-validated config: `uiEndpoint` (default `/graphql/ui`), `graphqlEndpoint` (default `/graphql`), `subscriptionEndpoint` (default `/graphql/ws`), `theme` (`"light"` | `"dark"` | `"system"`, default `"system"`), `defaultQuery`, `defaultVariableEditorOpen`, `editorOptions` (codemirror passthroughs).
|
||||
- ADDED: Static-asset serving — GraphiQL's bundle is served from the package's own `dist/assets/` (no CDN dependency). The HTML shell embeds versioned hashes for cache-busting.
|
||||
- ADDED: CSP/security headers — `Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'` (GraphiQL needs inline styles) emitted on the HTML response.
|
||||
- ADDED: `GRAPHQL_UI_GRAPHIQL_*` error codes: `GRAPHQL_UI_GRAPHIQL_ASSET_MISSING`, `GRAPHQL_UI_GRAPHIQL_ENDPOINT_COLLISION`.
|
||||
- ADDED: OTel instruments: `graphql.ui.requests_total { ui: "graphiql" }` counter.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- A CDN-fetched GraphiQL variant — bundled assets only, per the foundation's "no runtime CDN fetches" rule (security + offline-dev posture).
|
||||
- The Altair alternative — separate package `api-graphql-ui-altair`.
|
||||
- Schema introspection — that's the engine adapter's job (Yoga / Apollo configure introspection on `/graphql`). This package just renders an explorer that calls into whatever the engine exposes.
|
||||
- Authentication on the UI route — host config decides whether `/graphql/ui` is behind auth; this package surfaces a `RouterPlugin` whose route options can be configured by the operator.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| `GET /graphql/ui` default to GraphiQL | §10.6 |
|
||||
| UI as opt-in via separate package | §10.6 + cross-ref to engine adapters returning 404 when not wired |
|
||||
| Theme / config passthroughs | §10.6 (implied — interactive explorer) |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/factory/create-graphiql-ui.test.ts` — factory returns a valid `RouterPlugin`; HTML response shape; CSP header presence; asset 200s.
|
||||
- `src/factory/create-graphiql-ui.spec-conformance.test.ts` — pins that the mounted endpoint serves at `uiEndpoint` and that the response embeds the configured `graphqlEndpoint` + `subscriptionEndpoint`.
|
||||
- `src/assets/assets.test.ts` — every bundled asset reachable; missing-asset path raises `GRAPHQL_UI_GRAPHIQL_ASSET_MISSING`.
|
||||
- `tests/integration/graphiql-end-to-end.test.ts` (30000ms) — boots `api-graphql-yoga` + this UI plugin, opens a Playwright browser at `/graphql/ui`, runs a query through the live editor, asserts results render.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
@@ -0,0 +1,88 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-graphql-ui-graphiql (api-graphql-ui-graphiql)
|
||||
|
||||
**Package**: `@suaveplan/api-graphql-ui-graphiql`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Mounts GraphiQL at the configured UI endpoint
|
||||
|
||||
The package SHALL expose `createGraphiqlUi(opts)` returning a `RouterPlugin` that registers `GET <uiEndpoint>` (default `/graphql/ui`) on the host's `api-router`. The response MUST be the GraphiQL HTML shell embedding the configured `graphqlEndpoint` and `subscriptionEndpoint`.
|
||||
|
||||
#### Scenario: default mount serves GraphiQL at /graphql/ui
|
||||
|
||||
- **WHEN** the plugin is mounted with default `opts`
|
||||
- **THEN** `GET /graphql/ui` returns 200 AND the body contains the GraphiQL shell AND the embedded `graphqlEndpoint` reads `/graphql`
|
||||
|
||||
#### Scenario: configured endpoints flow into the shell
|
||||
|
||||
- **WHEN** `createGraphiqlUi({ graphqlEndpoint: "/api/gql", subscriptionEndpoint: "/api/gql/ws" })`
|
||||
- **THEN** the rendered HTML embeds those endpoints (NOT the defaults)
|
||||
|
||||
**Verified by:** `src/factory/create-graphiql-ui.test.ts::"default mount returns GraphiQL HTML at /graphql/ui"` AND `src/factory/create-graphiql-ui.spec-conformance.test.ts::"configured endpoints flow into the embedded shell"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Bundled assets only — no CDN fetches
|
||||
|
||||
Every static asset referenced by the GraphiQL HTML shell SHALL be served from this package's own `dist/assets/`. No `<script src="https://...">` or `<link href="https://...">` MAY point at any CDN. Asset paths MUST embed a cache-busting hash so version upgrades invalidate browser caches. A request for a missing asset MUST return 404 with code `GRAPHQL_UI_GRAPHIQL_ASSET_MISSING`.
|
||||
|
||||
#### Scenario: no CDN references in the rendered shell
|
||||
|
||||
- **WHEN** the HTML body is inspected
|
||||
- **THEN** no `src` or `href` attribute points at an external host
|
||||
|
||||
#### Scenario: missing asset returns the spec'd code
|
||||
|
||||
- **WHEN** a request arrives for `<uiEndpoint>/assets/does-not-exist.js`
|
||||
- **THEN** the response is 404 with `code: "GRAPHQL_UI_GRAPHIQL_ASSET_MISSING"`
|
||||
|
||||
**Verified by:** `src/assets/assets.test.ts::"rendered shell references no external hosts"` AND `src/assets/assets.test.ts::"missing asset returns GRAPHQL_UI_GRAPHIQL_ASSET_MISSING"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CSP header on the HTML response
|
||||
|
||||
The HTML response SHALL carry `Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'` so the rendered page is safe to expose in production environments that enforce CSP. Inline styles are required by GraphiQL's bundle; inline scripts MUST NOT be permitted.
|
||||
|
||||
#### Scenario: CSP header is present and excludes 'unsafe-inline' for scripts
|
||||
|
||||
- **WHEN** the HTML response is returned
|
||||
- **THEN** the `Content-Security-Policy` header is set AND `script-src` does NOT include `'unsafe-inline'`
|
||||
|
||||
**Verified by:** `src/headers/csp.test.ts::"CSP header set with 'unsafe-inline' only for style-src"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Endpoint-collision detection
|
||||
|
||||
If the host already has a route registered at `<uiEndpoint>`, this package SHALL refuse to mount with `GRAPHQL_UI_GRAPHIQL_ENDPOINT_COLLISION`. Operators see the collision at boot rather than at first request.
|
||||
|
||||
#### Scenario: collision is refused
|
||||
|
||||
- **WHEN** another plugin has already mounted `GET /graphql/ui` AND this package attempts to mount on top
|
||||
- **THEN** mount fails with `GRAPHQL_UI_GRAPHIQL_ENDPOINT_COLLISION`
|
||||
|
||||
**Verified by:** `src/factory/create-graphiql-ui.test.ts::"endpoint collision refused with GRAPHQL_UI_GRAPHIQL_ENDPOINT_COLLISION"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel counter for UI traffic
|
||||
|
||||
The package SHALL emit `graphql.ui.requests_total` counter labelled `{ ui: "graphiql" }` on each successful UI HTML request. Asset sub-requests MUST NOT increment this counter (they are observed via the router's standard request metrics).
|
||||
|
||||
#### Scenario: UI HTML request increments the counter
|
||||
|
||||
- **WHEN** `GET /graphql/ui` returns 200
|
||||
- **THEN** an in-memory `MeterProvider` observes `graphql.ui.requests_total{ui="graphiql"}` increment by 1
|
||||
|
||||
#### Scenario: asset request does NOT increment the counter
|
||||
|
||||
- **WHEN** an asset under `<uiEndpoint>/assets/...` is fetched
|
||||
- **THEN** the counter does NOT increment
|
||||
|
||||
**Verified by:** `src/instruments/instruments.test.ts::"UI HTML increments graphql.ui.requests_total"` AND `src/instruments/instruments.test.ts::"asset requests do not increment the UI counter"`
|
||||
68
openspec/archive/2026-05-26-api-graphql-ui-graphiql/tasks.md
Normal file
68
openspec/archive/2026-05-26-api-graphql-ui-graphiql/tasks.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Tasks — `@suaveplan/api-graphql-ui-graphiql`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirmed clean working tree before opening this change (verified at dispatch).
|
||||
- [x] 0.2 Worktree accounted for — this change executed in an isolated agent worktree.
|
||||
- [x] 0.3 `MANIFEST.md` refreshed after package land (task 7.4).
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc §10.6
|
||||
- [x] 1.2 Read umbrella Stage 7 acceptance
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Draft `specs/api-graphql-ui-graphiql/spec.md`
|
||||
- [x] 1.6 Every requirement carries a `**Verified by:**` line
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api-stack/api/api-graphql-ui-graphiql/`
|
||||
- [x] 2.2 `src/options/options.ts` — `GraphiqlUiOptions` Zod schema
|
||||
- [x] 2.3 `src/factory/create-graphiql-ui.ts` — `createGraphiqlUi(opts)` returning `RouterPlugin`
|
||||
- [x] 2.4 `src/html/render-shell.ts` — HTML shell template embedding configured endpoints + theme
|
||||
- [x] 2.5 `src/assets/assets.ts` — static-asset map; cache-busting hashes
|
||||
- [x] 2.6 `src/headers/csp.ts` — Content-Security-Policy header builder
|
||||
- [x] 2.7 `src/instruments/instruments.ts` — `graphql.ui.requests_total` counter
|
||||
- [x] 2.8 `src/error-codes/error-codes.ts` — 2 `GRAPHQL_UI_GRAPHIQL_*` codes
|
||||
- [x] 2.9 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests, every `it(...)` `{ timeout: 5000 }`
|
||||
- [x] 3.2 Conformance test pins endpoint mount + embedded-config (`create-graphiql-ui.spec-conformance.test.ts`)
|
||||
- [x] 3.3 Asset reachability test (`{ timeout: 5000 }`)
|
||||
- [x] 3.4 End-to-end integration (`{ timeout: 30000 }`) — in-memory `Router` + minimal GraphQL handler exercising shell + asset + protocol round-trip. Playwright-driven browser variant remains gated on `api-graphql-yoga` landing (Stage 7.1); integration test stands in until then with full HTTP contract coverage.
|
||||
- [x] 3.5 All tests import from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` per implementation file (7/7 — docs:check passes)
|
||||
- [x] 4.2 `README.md` calling out: opt-in via host config; no CDN; bundled assets only
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check` — biome auto-fix applied, no remaining issues
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-graphql-ui-graphiql` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-graphql-ui-graphiql` — 100/100/100/100
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-graphql-ui-graphiql` — script throws on 3-level packages layout (pre-existing issue; reproducible on every existing api-stack package). Tracked separately; not blocking this change.
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-graphql-ui-graphiql` — green (runs as part of the gate)
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics where applicable; literal-type narrowing exercised on theme + ui-label
|
||||
- [x] 6.2 Zod schema on options (strict, defaults baked)
|
||||
- [x] 6.3 Both error codes thrown by production paths (asserted in `error-codes.test.ts` + `factory/*.test.ts`)
|
||||
- [x] 6.4 Strict-mode tested (theme system vs explicit; strict unknown-key rejection)
|
||||
- [x] 6.5 `graphql.ui.requests_total` asserted via in-memory `MeterProvider` fake
|
||||
- [x] 6.6 Lifecycle dedup N/A (stateless static-asset server)
|
||||
- [x] 6.7 No retroactive ticks — Stage-7 row 7.3 ticked only on this change
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-graphql-ui-graphiql/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/2026-05-26-api-graphql-ui-graphiql/`
|
||||
- [x] 7.3 Tick Stage-7 row in umbrella tasks
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user