Initial Commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
export type {
|
||||
AltairEnvironment,
|
||||
AltairUiOptions,
|
||||
AltairUiOptionsInput,
|
||||
} from "./options.js";
|
||||
export {
|
||||
altairUiOptionsSchema,
|
||||
parseAltairUiOptions,
|
||||
} from "./options.js";
|
||||
@@ -0,0 +1,74 @@
|
||||
# `options` — `AltairUiOptions` schema + parser
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`options` owns the Zod-validated configuration surface every consumer of
|
||||
the Altair UI plugin passes through. The schema enforces structural
|
||||
invariants — every endpoint string must start with `/`, theme is one of
|
||||
three literal values, top-level keys are strict so typos surface at the
|
||||
boundary — while still defaulting every field so the most common call
|
||||
site (`createAltairUi()`) is zero-config. Centralising defaulting in one
|
||||
schema means the factory, the HTML renderer, and the asset map all
|
||||
consume the same shape.
|
||||
|
||||
## Features
|
||||
|
||||
- Defaults: `uiEndpoint=/graphql/ui`, `graphqlEndpoint=/graphql`,
|
||||
`subscriptionEndpoint=/graphql/ws`, `theme=system`, `initialQuery=""`,
|
||||
empty `initialHeaders`, empty `initialEnvironments`.
|
||||
- Strict mode at every nesting level — unknown keys are rejected.
|
||||
- Rich per-environment block: `url`, `subscriptionsEndpoint`, `headers`,
|
||||
`variables`.
|
||||
- `parseAltairUiOptions(undefined)` is valid and produces the default
|
||||
bundle.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseAltairUiOptions } from "@suaveplan/api-graphql-ui-altair/options";
|
||||
|
||||
const opts = parseAltairUiOptions();
|
||||
// opts.uiEndpoint === "/graphql/ui"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { parseAltairUiOptions } from "@suaveplan/api-graphql-ui-altair/options";
|
||||
|
||||
const opts = parseAltairUiOptions({
|
||||
uiEndpoint: "/dev/ui",
|
||||
graphqlEndpoint: "/api/graphql",
|
||||
theme: "dark",
|
||||
initialEnvironments: {
|
||||
production: {
|
||||
url: "https://api.example.com/graphql",
|
||||
subscriptionsEndpoint: "wss://api.example.com/graphql",
|
||||
headers: { "x-tenant": "acme" },
|
||||
variables: { region: "us-east-1" },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `altairUiOptionsSchema: z.ZodObject` — direct Zod handle (composable
|
||||
via `.extend()` or `.partial()` for downstream wrappers).
|
||||
- `parseAltairUiOptions(input?: AltairUiOptionsInput): AltairUiOptions`
|
||||
— throws `ZodError` on shape violations.
|
||||
- `AltairUiOptions` — output type (every field defined).
|
||||
- `AltairUiOptionsInput` — input type (every field optional).
|
||||
- `AltairEnvironment` — per-environment record shape.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The Zod default record literals (`{}`) feed back into `z.input` as
|
||||
optional, so the input type stays ergonomic at the caller boundary
|
||||
while the output type guarantees every field is populated downstream.
|
||||
The schema uses the Zod 4 default-import (`import z from "zod"`) per
|
||||
the repo-wide Bun-safe convention.
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { altairUiOptionsSchema, parseAltairUiOptions } from "./options.js";
|
||||
|
||||
describe("parseAltairUiOptions", () => {
|
||||
it("applies all defaults when input is undefined", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = parseAltairUiOptions(undefined);
|
||||
expect(out.uiEndpoint).toBe("/graphql/ui");
|
||||
expect(out.graphqlEndpoint).toBe("/graphql");
|
||||
expect(out.subscriptionEndpoint).toBe("/graphql/ws");
|
||||
expect(out.theme).toBe("system");
|
||||
expect(out.initialQuery).toBe("");
|
||||
expect(out.initialHeaders).toEqual({});
|
||||
expect(out.initialEnvironments).toEqual({});
|
||||
});
|
||||
|
||||
it("applies all defaults when input is empty object", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const out = parseAltairUiOptions({});
|
||||
expect(out.uiEndpoint).toBe("/graphql/ui");
|
||||
});
|
||||
|
||||
it("accepts overrides for every field", { timeout: 5000 }, () => {
|
||||
const out = parseAltairUiOptions({
|
||||
uiEndpoint: "/dev/ui",
|
||||
graphqlEndpoint: "/api/graphql",
|
||||
subscriptionEndpoint: "/api/ws",
|
||||
theme: "dark",
|
||||
initialQuery: "{ ping }",
|
||||
initialHeaders: { "x-tenant": "acme" },
|
||||
initialEnvironments: {
|
||||
production: {
|
||||
url: "https://api.example.com/graphql",
|
||||
headers: { "x-env": "prod" },
|
||||
variables: { region: "us-east-1" },
|
||||
subscriptionsEndpoint: "wss://api.example.com/graphql",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(out.uiEndpoint).toBe("/dev/ui");
|
||||
expect(out.theme).toBe("dark");
|
||||
expect(out.initialHeaders["x-tenant"]).toBe("acme");
|
||||
expect(out.initialEnvironments["production"]?.url).toBe(
|
||||
"https://api.example.com/graphql"
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects uiEndpoint that doesn't start with /", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
parseAltairUiOptions({ uiEndpoint: "graphql/ui" })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects graphqlEndpoint that doesn't start with /", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
parseAltairUiOptions({ graphqlEndpoint: "graphql" })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects subscriptionEndpoint that doesn't start with /", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
parseAltairUiOptions({ subscriptionEndpoint: "ws" })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("rejects empty uiEndpoint", { timeout: 5000 }, () => {
|
||||
expect(() => parseAltairUiOptions({ uiEndpoint: "" })).toThrow();
|
||||
});
|
||||
|
||||
it("rejects unknown theme", { timeout: 5000 }, () => {
|
||||
// @ts-expect-error testing runtime rejection
|
||||
expect(() => parseAltairUiOptions({ theme: "midnight" })).toThrow();
|
||||
});
|
||||
|
||||
it("rejects unknown top-level keys (strict)", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error testing runtime rejection
|
||||
parseAltairUiOptions({ rogue: true })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("schema is reusable directly via parse", { timeout: 5000 }, () => {
|
||||
const out = altairUiOptionsSchema.parse({});
|
||||
expect(out.theme).toBe("system");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import z from "zod";
|
||||
|
||||
/** Per-environment configuration block — narrow projection of Altair's. */
|
||||
export interface AltairEnvironment {
|
||||
readonly url?: string;
|
||||
readonly subscriptionsEndpoint?: string;
|
||||
readonly headers?: Readonly<Record<string, string>>;
|
||||
readonly variables?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
const environmentSchema = z
|
||||
.object({
|
||||
url: z.string().optional(),
|
||||
subscriptionsEndpoint: z.string().optional(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
variables: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Zod schema for {@link AltairUiOptions}. The schema is intentionally
|
||||
* permissive on header/environment values — operators can plug arbitrary
|
||||
* string values into the embedded shell — and strict on the four
|
||||
* structural fields (endpoints + theme).
|
||||
*/
|
||||
export const altairUiOptionsSchema = z
|
||||
.object({
|
||||
uiEndpoint: z
|
||||
.string()
|
||||
.min(1)
|
||||
.startsWith("/", {
|
||||
message: "uiEndpoint must start with '/'",
|
||||
})
|
||||
.default("/graphql/ui"),
|
||||
graphqlEndpoint: z
|
||||
.string()
|
||||
.min(1)
|
||||
.startsWith("/", {
|
||||
message: "graphqlEndpoint must start with '/'",
|
||||
})
|
||||
.default("/graphql"),
|
||||
subscriptionEndpoint: z
|
||||
.string()
|
||||
.min(1)
|
||||
.startsWith("/", {
|
||||
message: "subscriptionEndpoint must start with '/'",
|
||||
})
|
||||
.default("/graphql/ws"),
|
||||
theme: z.enum(["light", "dark", "system"]).default("system"),
|
||||
initialQuery: z.string().default(""),
|
||||
initialHeaders: z.record(z.string(), z.string()).default({}),
|
||||
initialEnvironments: z
|
||||
.record(z.string(), environmentSchema)
|
||||
.default({}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Parsed, defaulted options consumed by `createAltairUi`. */
|
||||
export type AltairUiOptions = z.infer<typeof altairUiOptionsSchema>;
|
||||
|
||||
/** Caller-facing input — every field optional, defaults applied by Zod. */
|
||||
export type AltairUiOptionsInput = z.input<typeof altairUiOptionsSchema>;
|
||||
|
||||
/**
|
||||
* Validate caller-supplied options. Throws on shape violations; returns
|
||||
* a fully-defaulted, immutable `AltairUiOptions` on success.
|
||||
*/
|
||||
export function parseAltairUiOptions(
|
||||
input: AltairUiOptionsInput | undefined
|
||||
): AltairUiOptions {
|
||||
return altairUiOptionsSchema.parse(input ?? {});
|
||||
}
|
||||
Reference in New Issue
Block a user