first commit
This commit is contained in:
107
docs/api-stack/api/api-graphql-ui-graphiql/README.md
Normal file
107
docs/api-stack/api/api-graphql-ui-graphiql/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# @suaveplan/api-graphql-ui-graphiql — GraphiQL UI mount for the Suaveplan API plugin system. Exposes createGraphiqlUi() — a factory returning a RouterPlugin that registers GET /graphql/ui (or any configured endpoint) and serves a self-contained HTML shell + bundled assets with a strict CSP. Default GraphQL explorer when operators want GraphiQL; sibling api-graphql-ui-altair covers the Altair alternative.
|
||||
|
||||
GraphiQL UI mount for the Suaveplan API plugin system. Stage 7 of
|
||||
the umbrella plan.
|
||||
|
||||
`createGraphiqlUi(opts)` returns a `RouterPlugin` that registers
|
||||
`GET /graphql/ui` (or any configured endpoint) on a host
|
||||
`@suaveplan/api-router` `Router`. The endpoint serves a self-contained
|
||||
HTML shell plus bundled JS/CSS assets — no CDN fetches.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
|
||||
- [assets](./src/assets/assets.md) — Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell references two assets (`graphiql.js`, `graphiq…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the GraphiQL UI plugin. Every code in the regis…
|
||||
- [factory](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
|
||||
- [create-graphiql-ui](./src/factory/create-graphiql-ui.md) — Public entry point of the package. `createGraphiqlUi(opts)` returns a `RouterPlugin` — a plain value with `name`, `vers…
|
||||
- [headers](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
|
||||
- [csp](./src/headers/csp.md) — Owns the `Content-Security-Policy` header values emitted on the GraphiQL HTML response. The policy is hard-coded to a p…
|
||||
- [html](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
|
||||
- [render-shell](./src/html/render-shell.md) — Renders the GraphiQL HTML shell. The output embeds the configured GraphQL endpoint, subscription endpoint, theme, and e…
|
||||
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
|
||||
- [instruments](./src/instruments/instruments.md) — Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI plugin. A single `Counter` (`graphql.ui.requests_to…
|
||||
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
|
||||
- [options](./src/options/options.md) — Zod-validated configuration surface for `createGraphiqlUi`. Owns `GraphiqlUiOptions` (resolved shape with every default…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Why opt-in
|
||||
|
||||
Per the design doc (`docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`
|
||||
§10.6) the API foundation reserves `GET /graphql/ui` for an interactive
|
||||
explorer but does not bundle one by default — UI choice is operator-
|
||||
chosen so the runtime image stays minimal for production deployments
|
||||
that don't need it.
|
||||
|
||||
Operators wire ONE of:
|
||||
|
||||
- `@suaveplan/api-graphql-ui-graphiql` — canonical GraphiQL (this
|
||||
package).
|
||||
- `@suaveplan/api-graphql-ui-altair` — Altair alternative.
|
||||
|
||||
The GraphQL engine adapters (`api-graphql-yoga`, `api-graphql-apollo`)
|
||||
return 404 at `/graphql/ui` when no UI plugin is wired.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { Router } from "@suaveplan/api-router";
|
||||
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const router = new Router({ plugin: "host" });
|
||||
createGraphiqlUi({}).register(router);
|
||||
// GET /graphql/ui -> the explorer
|
||||
// GET /graphql/ui/assets/graphiql.<hash>.js -> bundled JS
|
||||
// GET /graphql/ui/assets/graphiql.<hash>.css -> bundled CSS
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Option | Default | Notes |
|
||||
|---|---|---|
|
||||
| `uiEndpoint` | `/graphql/ui` | Must start with `/`. |
|
||||
| `graphqlEndpoint` | `/graphql` | Embedded into the explorer config. |
|
||||
| `subscriptionEndpoint` | `/graphql/ws` | Embedded into the explorer config. |
|
||||
| `theme` | `"system"` | `"light"` / `"dark"` / `"system"`. |
|
||||
| `defaultQuery` | `""` | HTML-escaped before embedding. |
|
||||
| `defaultVariableEditorOpen` | `false` | Opens the variables pane on load. |
|
||||
| `editorOptions` | `{}` | CodeMirror passthroughs. |
|
||||
| `meterProvider` | OTel global | OTel `MeterProvider` for the counter. |
|
||||
|
||||
## Security posture
|
||||
|
||||
- **Bundled assets only.** Every `src` / `href` in the rendered HTML
|
||||
points inside the package's own `/<uiEndpoint>/assets/` namespace.
|
||||
Cross-checked by `src/html/render-shell.test.ts`.
|
||||
- **Strict CSP.** `default-src 'self'; style-src 'self' 'unsafe-inline';
|
||||
script-src 'self'; img-src 'self' data:; connect-src 'self'`.
|
||||
Inline scripts are NOT permitted. Cross-checked by
|
||||
`src/headers/csp.test.ts`.
|
||||
- **Cache-immutable assets.** Bundled JS/CSS carries
|
||||
`Cache-Control: public, max-age=31536000, immutable`.
|
||||
- **Stale-hash tolerance.** Hashed filenames resolve any matching
|
||||
stem+ext to the current asset bytes — rolling deploys don't
|
||||
404 in-flight sessions.
|
||||
|
||||
## OTel
|
||||
|
||||
- `graphql.ui.requests_total { ui: "graphiql" }` — counter incremented
|
||||
on each successful HTML response. Asset hits are NOT counted.
|
||||
|
||||
## Error codes
|
||||
|
||||
- `GRAPHQL_UI_GRAPHIQL.ASSET_MISSING` — asset lookup failure.
|
||||
- `GRAPHQL_UI_GRAPHIQL.ENDPOINT_COLLISION` — `<uiEndpoint>` already
|
||||
registered by another plugin.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-graphql-ui-graphiql
|
||||
```
|
||||
|
||||
100% statements / branches / functions / lines required on touched
|
||||
modules (`provider: istanbul`).
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# `assets`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundled static-asset registry for the GraphiQL UI plugin. The HTML shell
|
||||
references two assets (`graphiql.js`, `graphiql.css`) via hashed filenames
|
||||
under `<uiEndpoint>/assets/`. Hash bytes come from FNV-1a-32 of the asset
|
||||
content — non-cryptographic, used purely for cache-busting on deploy.
|
||||
|
||||
The asset payloads are shipped inline as TypeScript template strings so
|
||||
the package has zero filesystem requirements at runtime. Operators who
|
||||
want a richer GraphiQL distribution (the full React app) can swap
|
||||
`GRAPHIQL_ASSETS` content on a fork without touching the rest of the
|
||||
plugin.
|
||||
|
||||
## Features
|
||||
|
||||
- `GRAPHIQL_ASSETS` — frozen `Map<name, asset>` exposing every shipped
|
||||
static.
|
||||
- `hashedAssetPath(name)` — builds `<stem>.<hash>.<ext>` for embedding in
|
||||
the HTML shell.
|
||||
- `resolveAsset(filename)` — server-side lookup. Tolerates stale hashes
|
||||
during rolling deploys (resolves any matching stem+ext to the current
|
||||
asset).
|
||||
- `fnv1a32(input)` — public so callers can hash their own additions in
|
||||
consistent shape.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { GRAPHIQL_ASSETS, resolveAsset } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
for (const asset of GRAPHIQL_ASSETS.values()) {
|
||||
console.log(asset.name, asset.hash, asset.contentType);
|
||||
}
|
||||
const css = resolveAsset("graphiql.css");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { fnv1a32, hashedAssetPath, resolveAsset } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
// Build the served filename for the JS asset.
|
||||
const path = `/graphql/ui/assets/${hashedAssetPath("graphiql.js")}`;
|
||||
|
||||
// Verify integrity in a custom asset middleware.
|
||||
const asset = resolveAsset("graphiql.js");
|
||||
if (fnv1a32(asset.content) !== asset.hash) {
|
||||
throw new Error("asset content drift");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface GraphiqlAsset { name, content, contentType, hash }`.
|
||||
- `GRAPHIQL_ASSETS: ReadonlyMap<string, GraphiqlAsset>`.
|
||||
- `fnv1a32(input: string): string` — 8-char lowercase hex hash.
|
||||
- `hashedAssetPath(name: string): string` — throws `ASSET_MISSING` if
|
||||
`name` is not registered.
|
||||
- `resolveAsset(filename: string): GraphiqlAsset` — throws
|
||||
`ASSET_MISSING` on lookup failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The stale-hash tolerance is intentional. Picture a CDN-cached `index.html`
|
||||
served before a deploy with the new asset bundle — that `index.html`
|
||||
references `graphiql.<old-hash>.js`. Returning 404 on the old hash would
|
||||
break the in-flight session for ~CDN-TTL seconds. The resolver instead
|
||||
serves the new bytes under the old name, traded against a slightly larger
|
||||
attack surface (an adversary who knows the asset name+ext can fetch the
|
||||
asset at any hash). The Cache-Control header on the response is
|
||||
`immutable` for the matched name, so the browser still caches the
|
||||
content correctly.
|
||||
@@ -0,0 +1,85 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the stable `GRAPHQL_UI_GRAPHIQL.*` error-code registry consumed by the
|
||||
GraphiQL UI plugin. Every code in the registry is thrown by a production
|
||||
path in this package — the `error-codes.spec-conformance.test.ts` walks
|
||||
the registry against the source tree to prove that no code is unused and
|
||||
that no production throw site references an unregistered code.
|
||||
|
||||
The module also exports `GraphqlUiGraphiqlError`, a concrete
|
||||
`SuaveplanError` subclass with a `graphiqlCode` mirror property. Callers
|
||||
can discriminate failures from this package via `instanceof` or by
|
||||
checking the stable `error.code` field; the mirror property exists so
|
||||
TypeScript narrows to the narrow union without the consumer importing
|
||||
the `GraphqlUiGraphiqlCode` alias.
|
||||
|
||||
## Features
|
||||
|
||||
- Two-code registry — `ASSET_MISSING`, `ENDPOINT_COLLISION`.
|
||||
- `defineCodes("GRAPHQL_UI_GRAPHIQL", …)` namespace.
|
||||
- `GraphqlUiGraphiqlError` extends `SuaveplanError` with a public
|
||||
constructor (required by the repo's `check-error-subclass-constructors`
|
||||
lint).
|
||||
- Frozen registry — `Object.isFrozen(GRAPHQL_UI_GRAPHIQL_CODES) === true`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
GRAPHQL_UI_GRAPHIQL_CODES,
|
||||
GraphqlUiGraphiqlError,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
throw new GraphqlUiGraphiqlError(
|
||||
GRAPHQL_UI_GRAPHIQL_CODES.ASSET_MISSING,
|
||||
"Bundled asset \"missing.js\" is not registered"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
GRAPHQL_UI_GRAPHIQL_CODES,
|
||||
GraphqlUiGraphiqlError,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
try {
|
||||
mountGraphiqlUi();
|
||||
} catch (err) {
|
||||
if (err instanceof GraphqlUiGraphiqlError) {
|
||||
if (err.code === GRAPHQL_UI_GRAPHIQL_CODES.ENDPOINT_COLLISION) {
|
||||
// Operator-facing: log the existing plugin from err.context.
|
||||
log.error({
|
||||
msg: "graphiql collision",
|
||||
existing: err.context?.existing,
|
||||
});
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `GRAPHQL_UI_GRAPHIQL_CODES` — frozen `Record<string, string>` of fully
|
||||
qualified codes.
|
||||
- `GraphqlUiGraphiqlCode` — union of every value resolved through the
|
||||
registry.
|
||||
- `class GraphqlUiGraphiqlError extends SuaveplanError<GraphqlUiGraphiqlCode>`
|
||||
— public constructor `(code, message, options?)` mirrors
|
||||
`SuaveplanError`'s signature.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The class name (`GraphqlUiGraphiqlError`) is intentionally explicit so
|
||||
stack traces and logs distinguish errors from this package vs the sibling
|
||||
`api-graphql-ui-altair`. The public constructor is required by the
|
||||
project-wide `check-error-subclass-constructors` lint added in
|
||||
`cd454b3924`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# `factory/create-graphiql-ui`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Public entry point of the package. `createGraphiqlUi(opts)` returns a
|
||||
`RouterPlugin` — a plain value with `name`, `version`, parsed `options`,
|
||||
the OTel instrument bundle, and a `register(router)` method that mounts
|
||||
two routes on a host `Router`:
|
||||
|
||||
- `GET <uiEndpoint>` — serves the GraphiQL HTML shell with the configured
|
||||
CSP header.
|
||||
- `GET <uiEndpoint>/assets/:filename` — serves bundled static assets with
|
||||
an `immutable` Cache-Control header.
|
||||
|
||||
The plugin is opt-in: the host's plugin wiring decides whether to call
|
||||
`register(router)`. Mount-time validation runs Zod options parsing AND
|
||||
checks the host router for an existing route at the same path; collision
|
||||
raises `GRAPHQL_UI_GRAPHIQL.ENDPOINT_COLLISION` so operators see it at
|
||||
boot.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-call factory — `createGraphiqlUi(input)` returns a frozen
|
||||
`RouterPlugin`.
|
||||
- Endpoint-collision detection at register time via
|
||||
`router.collidesWith` plus a defence-in-depth catch of
|
||||
`ROUTER.ROUTE_COLLISION` from the `router.get` call itself.
|
||||
- Stale-hash-tolerant asset routing — the same `:filename` param matches
|
||||
every served permutation of the asset name.
|
||||
- Counter increments only on HTML hits, never on asset hits.
|
||||
- Custom OTel `MeterProvider` accepted via the optional `meterProvider`
|
||||
field.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Router } from "@suaveplan/api-router";
|
||||
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const router = new Router({ plugin: "host" });
|
||||
createGraphiqlUi({}).register(router);
|
||||
// GET /graphql/ui now serves the explorer.
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { Router } from "@suaveplan/api-router";
|
||||
import { createGraphiqlUi } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const meterProvider = new MeterProvider({ /* ... */ });
|
||||
const plugin = createGraphiqlUi({
|
||||
uiEndpoint: "/api/explorer",
|
||||
graphqlEndpoint: "/api/gql",
|
||||
subscriptionEndpoint: "/api/gql/ws",
|
||||
theme: "dark",
|
||||
defaultQuery: "{ viewer { id } }",
|
||||
defaultVariableEditorOpen: true,
|
||||
editorOptions: { lineWrapping: true },
|
||||
meterProvider,
|
||||
});
|
||||
|
||||
const router = new Router({ plugin: "host" });
|
||||
plugin.register(router);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RouterPlugin { name; version; options; instruments;
|
||||
register(router) }`.
|
||||
- `interface CreateGraphiqlUiInput extends GraphiqlUiOptionsInput {
|
||||
meterProvider?: MeterProvider }`.
|
||||
- `interface RouterResponseValue { status; headers; body }` — the shape
|
||||
returned by the route handlers.
|
||||
- `type GraphiqlRouteHandler` — the handler signature consumed by the
|
||||
in-memory `Router`.
|
||||
- `createGraphiqlUi(input?): RouterPlugin`.
|
||||
- `listGraphiqlAssetNames(): readonly string[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The handler signature is intentionally adapter-agnostic — it accepts a
|
||||
plain `{ method, path }` request and returns `{ status, headers, body }`.
|
||||
Adapter routers (`api-router-hono`, worker bridges) wrap this with their
|
||||
native request context. The in-memory `Router` from `@suaveplan/api-router`
|
||||
uses the same shape directly, which is what the package's unit tests
|
||||
exercise. The `RouterError` -> `GraphqlUiGraphiqlError` translation in
|
||||
`register()` exists because `router.collidesWith` is best-effort (it
|
||||
only checks the UI endpoint, not the asset sub-route); the second
|
||||
`router.get` for assets may still throw `ROUTE_COLLISION` if some other
|
||||
plugin owns the asset prefix.
|
||||
@@ -0,0 +1,64 @@
|
||||
# `headers/csp`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the `Content-Security-Policy` header values emitted on the GraphiQL
|
||||
HTML response. The policy is hard-coded to a posture that's safe for
|
||||
production deployments: `default-src 'self'`, `script-src 'self'`,
|
||||
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`, and
|
||||
`connect-src 'self'`.
|
||||
|
||||
`'unsafe-inline'` is granted to `style-src` because GraphiQL's bundled
|
||||
CSS ships inline `<style>` declarations. It is NOT granted to
|
||||
`script-src` — the bundled bootstrap is loaded from `/graphql/ui/assets/`
|
||||
with a hashed filename, never inlined.
|
||||
|
||||
## Features
|
||||
|
||||
- `CSP_HEADER_VALUE` — single canonical value used on every HTML response.
|
||||
- `buildCspHeader()` — returns the `{ name, value }` pair so future
|
||||
per-environment relaxations have a single touch-point.
|
||||
- `cspAllowsInlineScripts(value)` — predicate used by the conformance
|
||||
test to guard against accidental `'unsafe-inline'` creep into
|
||||
`script-src`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildCspHeader } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const { name, value } = buildCspHeader();
|
||||
// name === "Content-Security-Policy"
|
||||
// value === "default-src 'self'; style-src 'self' 'unsafe-inline'; ..."
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { cspAllowsInlineScripts } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
if (cspAllowsInlineScripts(myCspValue)) {
|
||||
throw new Error("script-src must NOT include 'unsafe-inline'");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CSP_HEADER_NAME: "Content-Security-Policy"`.
|
||||
- `CSP_HEADER_VALUE: string` — the canonical value emitted on the
|
||||
HTML response.
|
||||
- `buildCspHeader(): { name: string; value: string }` — returns the
|
||||
name/value pair.
|
||||
- `cspAllowsInlineScripts(value): boolean` — predicate matching
|
||||
`'unsafe-inline'` inside a `script-src` directive.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Inline-style allowance is documented in the module header so future
|
||||
maintainers don't tighten the policy without understanding why. The
|
||||
predicate operates on the raw header string rather than parsing the CSP
|
||||
grammar — sufficient for the conformance check.
|
||||
@@ -0,0 +1,77 @@
|
||||
# `html/render-shell`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Renders the GraphiQL HTML shell. The output embeds the configured GraphQL
|
||||
endpoint, subscription endpoint, theme, and editor passthroughs into a
|
||||
`<script id="graphiql-config" type="application/json">` block — NOT inline
|
||||
JavaScript — which the bundled `graphiql.js` parses at boot. This split
|
||||
keeps `script-src 'self'` viable.
|
||||
|
||||
The shell is intentionally minimalist. The bundled bootstrap drives the
|
||||
explorer end-to-end (text area for the query, JSON pre for the response).
|
||||
Operators who want the full React-based GraphiQL distribution can swap
|
||||
out the asset payload on a fork.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-call render — `renderShell(options)` returns the full document.
|
||||
- Defensive escaping — every caller-supplied string (default query,
|
||||
endpoints, theme, editor options blob) is HTML-escaped.
|
||||
- No external host references — every `src` / `href` points inside
|
||||
`<uiEndpoint>/assets/`.
|
||||
- Asset paths embed FNV-1a hashes for cache-busting.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseGraphiqlUiOptions,
|
||||
renderShell,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const html = renderShell(parseGraphiqlUiOptions({}));
|
||||
// html starts with <!DOCTYPE html> and references /graphql/ui/assets/...
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
parseGraphiqlUiOptions,
|
||||
renderShell,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const html = renderShell(
|
||||
parseGraphiqlUiOptions({
|
||||
uiEndpoint: "/explorer",
|
||||
graphqlEndpoint: "/api/v2/gql",
|
||||
subscriptionEndpoint: "/api/v2/gql/ws",
|
||||
theme: "dark",
|
||||
defaultQuery: "{ viewer { id name } }",
|
||||
editorOptions: { lineWrapping: true, tabSize: 2 },
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `renderShell(options: GraphiqlUiOptions): string` — produces the
|
||||
fully-substituted HTML document.
|
||||
- `escapeHtml(input: string): string` — replaces `&`, `<`, `>`, `"`,
|
||||
and `'` with HTML entities.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The config block uses `application/json` so the bootstrap can call
|
||||
`JSON.parse(node.textContent)` without sniffing or eval. Escaping the
|
||||
JSON via `escapeHtml` is technically over-protective (JSON inside a
|
||||
`<script type="application/json">` block doesn't need entity encoding
|
||||
of `<`/`>`/`&` for syntax correctness, but it does need it for
|
||||
defense-in-depth when the JSON contains a `</script>` substring) — we
|
||||
err on the safe side and run every embedded string through
|
||||
`escapeHtml`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# `instruments`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the OpenTelemetry instrument bundle emitted by the GraphiQL UI
|
||||
plugin. A single `Counter` (`graphql.ui.requests_total`) is constructed
|
||||
at mount time and incremented on each successful HTML response. The
|
||||
counter is always labelled `{ ui: "graphiql" }` so dashboards can sum
|
||||
across sibling UI plugins (`api-graphql-ui-altair` etc.) without
|
||||
collision.
|
||||
|
||||
Asset sub-requests (`<uiEndpoint>/assets/...`) do NOT increment this
|
||||
counter — they are covered by the host router's standard request
|
||||
metrics.
|
||||
|
||||
## Features
|
||||
|
||||
- Single `Counter` instrument — `graphql.ui.requests_total`.
|
||||
- Optional `meterProvider` parameter — defaults to the OTel global
|
||||
(which falls back to a no-op when no SDK is installed).
|
||||
- Frozen instrument bundle — meter + counter created once at plugin
|
||||
mount time.
|
||||
- `recordUiRequest(instruments)` helper — applies the `{ ui: "graphiql" }`
|
||||
label automatically so call sites never get it wrong.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createGraphiqlInstruments,
|
||||
recordUiRequest,
|
||||
} from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const instruments = createGraphiqlInstruments();
|
||||
recordUiRequest(instruments);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
import { createGraphiqlInstruments } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const meterProvider = new MeterProvider({ /* readers, views, ... */ });
|
||||
const instruments = createGraphiqlInstruments({ meterProvider });
|
||||
// instruments.meter / instruments.requestsTotal are now wired to the
|
||||
// caller-controlled provider rather than the OTel global.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface GraphiqlInstrumentsConfig { meterProvider?: MeterProvider }`.
|
||||
- `interface GraphiqlInstruments { meter: Meter; requestsTotal: Counter }`
|
||||
— frozen at construction.
|
||||
- `createGraphiqlInstruments(config?)` — returns the frozen bundle.
|
||||
- `recordUiRequest(instruments)` — increments the counter with the
|
||||
canonical `{ ui: "graphiql" }` label.
|
||||
- `UI_LABEL_VALUE: "graphiql"` — the literal used in every observation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`UI_LABEL_VALUE` is exported as a named constant so the sibling Altair
|
||||
package can reference it via a `not-equal` predicate in shared tests
|
||||
("the two UI plugins MUST emit distinct `ui` labels"). The counter name
|
||||
is shared across UI plugins by design — operators get a single rollup of
|
||||
"explorer requests" without per-UI configuration. The Stage-5
|
||||
testing-kit's in-memory `MeterProvider` is used in tests; production
|
||||
operators register their preferred SDK once at process boot.
|
||||
@@ -0,0 +1,81 @@
|
||||
# `options`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod-validated configuration surface for `createGraphiqlUi`. Owns
|
||||
`GraphiqlUiOptions` (resolved shape with every default applied),
|
||||
`GraphiqlUiOptionsInput` (caller-facing shape where every field is
|
||||
optional), and `parseGraphiqlUiOptions` (the boundary parser).
|
||||
|
||||
The schema is `.strict()` so typos in caller-supplied option keys throw
|
||||
at parse time rather than silently no-op'ing — empirically the most
|
||||
common operator mistake when wiring a new UI plugin.
|
||||
|
||||
## Features
|
||||
|
||||
- Three endpoint paths (`uiEndpoint`, `graphqlEndpoint`,
|
||||
`subscriptionEndpoint`) — each must start with `/`.
|
||||
- Theme literal — `"light"` / `"dark"` / `"system"`, default `"system"`.
|
||||
- `defaultQuery` — pre-fills the editor textarea.
|
||||
- `defaultVariableEditorOpen` — opens the variables pane on first render.
|
||||
- `editorOptions` — open passthrough record forwarded to the bundled
|
||||
bootstrap (CodeMirror options).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseGraphiqlUiOptions } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const options = parseGraphiqlUiOptions({});
|
||||
// options.uiEndpoint === "/graphql/ui"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { parseGraphiqlUiOptions } from "@suaveplan/api-graphql-ui-graphiql";
|
||||
|
||||
const options = parseGraphiqlUiOptions({
|
||||
uiEndpoint: "/api/explorer",
|
||||
graphqlEndpoint: "/api/gql",
|
||||
subscriptionEndpoint: "/api/gql/ws",
|
||||
theme: "dark",
|
||||
defaultQuery: "{ viewer { id } }",
|
||||
defaultVariableEditorOpen: true,
|
||||
editorOptions: { lineWrapping: true, tabSize: 2 },
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `graphiqlUiOptionsSchema` — strict Zod object with defaults baked in.
|
||||
- `GraphiqlUiOptions` — `z.infer<typeof graphiqlUiOptionsSchema>`.
|
||||
- `GraphiqlUiOptionsInput` — `z.input<typeof graphiqlUiOptionsSchema>`.
|
||||
- `parseGraphiqlUiOptions(input?)` — throws `ZodError` on invalid input;
|
||||
returns `GraphiqlUiOptions` with every default applied.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The endpoint paths use `.startsWith("/")` rather than a regex so the
|
||||
error message remains operator-friendly. The schema does NOT validate
|
||||
whether `subscriptionEndpoint` is reachable — subscription wiring is
|
||||
the engine adapter's responsibility. Strict mode is on, so a stray
|
||||
`graphqlEnpdoint` (typo) becomes a hard parse failure.
|
||||
|
||||
`GraphiqlUiOptionsInput` carries every field as optional with the
|
||||
defaults baked in via Zod's `.default(...)`. The factory uses this
|
||||
shape on its public surface so callers always receive a fully-resolved
|
||||
`GraphiqlUiOptions` regardless of how many fields they supplied.
|
||||
|
||||
The `theme` field intentionally uses a discriminated union of three
|
||||
literals rather than a string-with-enum because Zod's strict literal
|
||||
union narrows TypeScript correctly while preserving exhaustiveness
|
||||
checks at downstream call sites — important for the HTML shell, which
|
||||
emits a `data-theme="<value>"` attribute. The `editorOptions` record is
|
||||
typed as `Record<string, unknown>` so it can accept any CodeMirror
|
||||
configuration without churn on the schema when new editor knobs land
|
||||
upstream.
|
||||
Reference in New Issue
Block a user