@shaferllc/keel 0.68.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/README.md +15 -5
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +93 -0
- package/dist/api/resource.js +185 -0
- package/dist/core/application.js +6 -0
- package/dist/core/cache.d.ts +1 -2
- package/dist/core/cache.js +9 -2
- package/dist/core/cli/index.d.ts +28 -0
- package/dist/core/cli/index.js +427 -0
- package/dist/core/cli/stubs.d.ts +14 -0
- package/dist/core/cli/stubs.js +105 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/health.d.ts +2 -2
- package/dist/core/health.js +2 -2
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +25 -4
- package/dist/core/index.js +13 -3
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/logger.d.ts +7 -0
- package/dist/core/logger.js +28 -1
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +117 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.js +26 -5
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.js +15 -3
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +171 -1
- package/dist/core/testing.js +505 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +22 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +737 -3
- package/docs/api-resources.md +119 -0
- package/docs/configuration.md +74 -0
- package/docs/console.md +217 -13
- package/docs/database.md +101 -0
- package/docs/examples/api-resources.ts +86 -0
- package/docs/examples/configuration.ts +40 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/database.ts +90 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/logger.ts +74 -0
- package/docs/examples/mail.ts +103 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/testing.ts +150 -0
- package/docs/health.md +4 -4
- package/docs/i18n.md +302 -0
- package/docs/logger.md +156 -10
- package/docs/mail.md +73 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/storage.md +5 -3
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +3 -4
- package/docs/testing.md +252 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +2389 -241
- package/llms.txt +13 -1
- package/package.json +32 -6
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route documentation metadata. Attach it to a route with `.config(apiDoc(...))`
|
|
3
|
+
* and the generator turns it into a rich OpenAPI operation — request/response
|
|
4
|
+
* schemas, summary, tags. Request schemas reuse Keel's `RequestSchemas` shape, so
|
|
5
|
+
* the very object you hand `validateRequest({ body })` also documents the route.
|
|
6
|
+
*
|
|
7
|
+
* router
|
|
8
|
+
* .post("/users", [Users, "store"])
|
|
9
|
+
* .config(apiDoc({ summary: "Create a user", request: { body: NewUser }, tags: ["users"] }))
|
|
10
|
+
* .middleware([validateRequest({ body: NewUser })]);
|
|
11
|
+
*/
|
|
12
|
+
import type { RequestSchemas } from "../core/validation.js";
|
|
13
|
+
/** The key under which docs metadata lives in a route's `config`. */
|
|
14
|
+
export declare const OPENAPI_KEY = "openapi";
|
|
15
|
+
/** One response's documentation: a description and an optional body schema. */
|
|
16
|
+
export interface ResponseDoc {
|
|
17
|
+
description?: string;
|
|
18
|
+
/** A Zod schema or a plain JSON Schema object for the response body. */
|
|
19
|
+
schema?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/** Everything a route can say about itself for the spec. */
|
|
22
|
+
export interface OperationDoc {
|
|
23
|
+
summary?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
/** Overrides the generated operationId (default: the route name). */
|
|
27
|
+
operationId?: string;
|
|
28
|
+
deprecated?: boolean;
|
|
29
|
+
/** Request body/query/params schemas — Zod or plain JSON Schema. */
|
|
30
|
+
request?: RequestSchemas;
|
|
31
|
+
/** Responses keyed by status code, e.g. `{ 200: { schema: User }, 404: {} }`. */
|
|
32
|
+
responses?: Record<string | number, ResponseDoc>;
|
|
33
|
+
/** Leave this route out of the spec entirely. */
|
|
34
|
+
hidden?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wrap operation docs for a route's `.config()`. Returns `{ openapi: doc }`, which
|
|
38
|
+
* the generator reads off `RouteDefinition.config`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function apiDoc(doc: OperationDoc): Record<string, unknown>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route documentation metadata. Attach it to a route with `.config(apiDoc(...))`
|
|
3
|
+
* and the generator turns it into a rich OpenAPI operation — request/response
|
|
4
|
+
* schemas, summary, tags. Request schemas reuse Keel's `RequestSchemas` shape, so
|
|
5
|
+
* the very object you hand `validateRequest({ body })` also documents the route.
|
|
6
|
+
*
|
|
7
|
+
* router
|
|
8
|
+
* .post("/users", [Users, "store"])
|
|
9
|
+
* .config(apiDoc({ summary: "Create a user", request: { body: NewUser }, tags: ["users"] }))
|
|
10
|
+
* .middleware([validateRequest({ body: NewUser })]);
|
|
11
|
+
*/
|
|
12
|
+
/** The key under which docs metadata lives in a route's `config`. */
|
|
13
|
+
export const OPENAPI_KEY = "openapi";
|
|
14
|
+
/**
|
|
15
|
+
* Wrap operation docs for a route's `.config()`. Returns `{ openapi: doc }`, which
|
|
16
|
+
* the generator reads off `RouteDefinition.config`.
|
|
17
|
+
*/
|
|
18
|
+
export function apiDoc(doc) {
|
|
19
|
+
return { [OPENAPI_KEY]: doc };
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `keel openapi:export [--out openapi.json]` — write the spec to a file, for CI,
|
|
3
|
+
* client generation, or committing it. Contributed as a package command.
|
|
4
|
+
*/
|
|
5
|
+
import type { PackageCommand } from "../core/package.js";
|
|
6
|
+
import type { Router } from "../core/http/router.js";
|
|
7
|
+
import type { OpenApiConfig } from "./config.js";
|
|
8
|
+
export declare function exportCommand(getRouter: () => Router, config: OpenApiConfig, base: string): PackageCommand;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `keel openapi:export [--out openapi.json]` — write the spec to a file, for CI,
|
|
3
|
+
* client generation, or committing it. Contributed as a package command.
|
|
4
|
+
*/
|
|
5
|
+
import { defineCommand, flag } from "../core/console.js";
|
|
6
|
+
import { buildSpec } from "./spec.js";
|
|
7
|
+
export function exportCommand(getRouter, config, base) {
|
|
8
|
+
return defineCommand({
|
|
9
|
+
name: "openapi:export",
|
|
10
|
+
description: "Write the OpenAPI spec to a file",
|
|
11
|
+
flags: { out: flag.string({ description: "output path", default: "openapi.json" }) },
|
|
12
|
+
async run({ flags, ui }) {
|
|
13
|
+
const spec = buildSpec(getRouter().all(), config, base);
|
|
14
|
+
const { writeFile } = await import("node:fs/promises");
|
|
15
|
+
await writeFile(flags.out, JSON.stringify(spec, null, 2));
|
|
16
|
+
ui.success(`Wrote ${flags.out} (${Object.keys(spec.paths).length} paths)`);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may see the docs. A spec advertises your whole API surface, so it's gated
|
|
3
|
+
* shut in production by default — open only when `app.debug` is on, outside
|
|
4
|
+
* production, or when you set `public: true`. Override with `OpenApi.auth()`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx } from "../core/http/router.js";
|
|
7
|
+
import type { OpenApiConfig } from "./config.js";
|
|
8
|
+
export type OpenApiGate = (c: Ctx) => boolean | Promise<boolean>;
|
|
9
|
+
export declare const OpenApi: {
|
|
10
|
+
/** Restrict docs access. Return true to allow the request. */
|
|
11
|
+
auth(fn: OpenApiGate): void;
|
|
12
|
+
/** Remove a custom gate, reverting to the default. */
|
|
13
|
+
clearAuth(): void;
|
|
14
|
+
};
|
|
15
|
+
export declare function passesGate(c: Ctx, cfg: OpenApiConfig): Promise<boolean>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may see the docs. A spec advertises your whole API surface, so it's gated
|
|
3
|
+
* shut in production by default — open only when `app.debug` is on, outside
|
|
4
|
+
* production, or when you set `public: true`. Override with `OpenApi.auth()`.
|
|
5
|
+
*/
|
|
6
|
+
import { config } from "../core/helpers.js";
|
|
7
|
+
let gate;
|
|
8
|
+
export const OpenApi = {
|
|
9
|
+
/** Restrict docs access. Return true to allow the request. */
|
|
10
|
+
auth(fn) {
|
|
11
|
+
gate = fn;
|
|
12
|
+
},
|
|
13
|
+
/** Remove a custom gate, reverting to the default. */
|
|
14
|
+
clearAuth() {
|
|
15
|
+
gate = undefined;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
function defaultGate(cfg) {
|
|
19
|
+
if (cfg.public)
|
|
20
|
+
return true;
|
|
21
|
+
if (config("app.debug", false))
|
|
22
|
+
return true;
|
|
23
|
+
return config("app.env", "production") !== "production";
|
|
24
|
+
}
|
|
25
|
+
export async function passesGate(c, cfg) {
|
|
26
|
+
return gate ? gate(c) : defaultGate(cfg);
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel OpenAPI — spec generation + Swagger UI for Keel apps, imported from
|
|
3
|
+
* `@shaferllc/keel/openapi`.
|
|
4
|
+
*
|
|
5
|
+
* import { OpenApiServiceProvider } from "@shaferllc/keel/openapi";
|
|
6
|
+
* app.register(OpenApiServiceProvider); // serves /docs and /docs/openapi.json
|
|
7
|
+
*
|
|
8
|
+
* Document a route with `apiDoc()`; lock the docs down with `OpenApi.auth()`.
|
|
9
|
+
*/
|
|
10
|
+
export { OpenApiServiceProvider } from "./provider.js";
|
|
11
|
+
export { OpenApi } from "./gate.js";
|
|
12
|
+
export type { OpenApiGate } from "./gate.js";
|
|
13
|
+
export { apiDoc, OPENAPI_KEY } from "./doc.js";
|
|
14
|
+
export type { OperationDoc, ResponseDoc } from "./doc.js";
|
|
15
|
+
export { buildSpec } from "./spec.js";
|
|
16
|
+
export type { OpenApiDocument } from "./spec.js";
|
|
17
|
+
export { toJsonSchema } from "./zod.js";
|
|
18
|
+
export { resolveConfig, defaultConfig } from "./config.js";
|
|
19
|
+
export type { OpenApiConfig } from "./config.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel OpenAPI — spec generation + Swagger UI for Keel apps, imported from
|
|
3
|
+
* `@shaferllc/keel/openapi`.
|
|
4
|
+
*
|
|
5
|
+
* import { OpenApiServiceProvider } from "@shaferllc/keel/openapi";
|
|
6
|
+
* app.register(OpenApiServiceProvider); // serves /docs and /docs/openapi.json
|
|
7
|
+
*
|
|
8
|
+
* Document a route with `apiDoc()`; lock the docs down with `OpenApi.auth()`.
|
|
9
|
+
*/
|
|
10
|
+
export { OpenApiServiceProvider } from "./provider.js";
|
|
11
|
+
export { OpenApi } from "./gate.js";
|
|
12
|
+
export { apiDoc, OPENAPI_KEY } from "./doc.js";
|
|
13
|
+
export { buildSpec } from "./spec.js";
|
|
14
|
+
export { toJsonSchema } from "./zod.js";
|
|
15
|
+
export { resolveConfig, defaultConfig } from "./config.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { env } from "@shaferllc/keel/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keel OpenAPI — spec + Swagger UI configuration.
|
|
5
|
+
* Published with `keel vendor:publish --tag openapi-config`.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
enabled: env("OPENAPI_ENABLED", true),
|
|
9
|
+
|
|
10
|
+
// Docs UI at http://localhost:3000/docs, spec at /docs/openapi.json
|
|
11
|
+
path: "docs",
|
|
12
|
+
|
|
13
|
+
// Shown in the spec's `info`. Title defaults to config("app.name").
|
|
14
|
+
title: "",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
description: undefined,
|
|
17
|
+
|
|
18
|
+
// Where the API is served (OpenAPI `servers`), e.g. ["https://api.example.com"].
|
|
19
|
+
servers: [],
|
|
20
|
+
|
|
21
|
+
// Serve the docs in production too. Off by default — a spec advertises your API.
|
|
22
|
+
public: env("OPENAPI_PUBLIC", false),
|
|
23
|
+
|
|
24
|
+
// Swagger UI assets. Pin the version, or point at a copy you host.
|
|
25
|
+
cdn: "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.17.14",
|
|
26
|
+
|
|
27
|
+
// Route path prefixes to leave out of the spec.
|
|
28
|
+
ignorePaths: ["/watch"],
|
|
29
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel OpenAPI — generate an OpenAPI 3 spec from your routes and serve Swagger UI,
|
|
3
|
+
* shipped as a Keel package. One line turns it on:
|
|
4
|
+
*
|
|
5
|
+
* app.register(OpenApiServiceProvider);
|
|
6
|
+
*
|
|
7
|
+
* The spec is built from Keel's own route table plus whatever each route attaches
|
|
8
|
+
* with `.config(apiDoc(...))`. Nothing is scraped or guessed: paths and methods
|
|
9
|
+
* are always right, and schemas are as rich as the metadata a route provides.
|
|
10
|
+
*/
|
|
11
|
+
import { PackageProvider } from "../core/package.js";
|
|
12
|
+
export declare class OpenApiServiceProvider extends PackageProvider {
|
|
13
|
+
readonly name = "openapi";
|
|
14
|
+
private config;
|
|
15
|
+
private base;
|
|
16
|
+
register(): void;
|
|
17
|
+
boot(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keel OpenAPI — generate an OpenAPI 3 spec from your routes and serve Swagger UI,
|
|
3
|
+
* shipped as a Keel package. One line turns it on:
|
|
4
|
+
*
|
|
5
|
+
* app.register(OpenApiServiceProvider);
|
|
6
|
+
*
|
|
7
|
+
* The spec is built from Keel's own route table plus whatever each route attaches
|
|
8
|
+
* with `.config(apiDoc(...))`. Nothing is scraped or guessed: paths and methods
|
|
9
|
+
* are always right, and schemas are as rich as the metadata a route provides.
|
|
10
|
+
*/
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { PackageProvider } from "../core/package.js";
|
|
14
|
+
import { resolveConfig, defaultConfig } from "./config.js";
|
|
15
|
+
import { registerOpenApiRoutes } from "./routes.js";
|
|
16
|
+
import { exportCommand } from "./export.js";
|
|
17
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
export class OpenApiServiceProvider extends PackageProvider {
|
|
19
|
+
name = "openapi";
|
|
20
|
+
config;
|
|
21
|
+
base() {
|
|
22
|
+
return "/" + this.config.path.replace(/^\/|\/$/g, "");
|
|
23
|
+
}
|
|
24
|
+
register() {
|
|
25
|
+
this.mergeConfig("openapi", defaultConfig);
|
|
26
|
+
this.config = resolveConfig();
|
|
27
|
+
this.publishes({ [join(here, "openapi.config.stub")]: "config/openapi.ts" }, "openapi-config");
|
|
28
|
+
this.commands([exportCommand(() => this.app.router(), this.config, this.base())]);
|
|
29
|
+
}
|
|
30
|
+
boot() {
|
|
31
|
+
if (!this.config.enabled)
|
|
32
|
+
return;
|
|
33
|
+
this.routes((r) => registerOpenApiRoutes(r, this.config, this.app.router(), this.base()), { prefix: this.config.path, as: "openapi" });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs HTTP surface: the generated spec at `/openapi.json` and the Swagger UI
|
|
3
|
+
* at the base path. The spec is built once, lazily, from the full route table
|
|
4
|
+
* (which is complete only after the app has loaded its routes) and cached.
|
|
5
|
+
* Both routes are behind the gate.
|
|
6
|
+
*/
|
|
7
|
+
import type { Router } from "../core/http/router.js";
|
|
8
|
+
import type { OpenApiConfig } from "./config.js";
|
|
9
|
+
export declare function registerOpenApiRoutes(r: Router, config: OpenApiConfig, router: Router, base: string): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs HTTP surface: the generated spec at `/openapi.json` and the Swagger UI
|
|
3
|
+
* at the base path. The spec is built once, lazily, from the full route table
|
|
4
|
+
* (which is complete only after the app has loaded its routes) and cached.
|
|
5
|
+
* Both routes are behind the gate.
|
|
6
|
+
*/
|
|
7
|
+
import { buildSpec } from "./spec.js";
|
|
8
|
+
import { swaggerHtml } from "./ui.js";
|
|
9
|
+
import { passesGate } from "./gate.js";
|
|
10
|
+
export function registerOpenApiRoutes(r, config, router, base) {
|
|
11
|
+
let cached;
|
|
12
|
+
const spec = () => (cached ??= buildSpec(router.all(), config, base));
|
|
13
|
+
r.get("/openapi.json", async (c) => {
|
|
14
|
+
if (!(await passesGate(c, config)))
|
|
15
|
+
return c.json({ error: "Forbidden" }, 403);
|
|
16
|
+
return c.json(spec());
|
|
17
|
+
}).name("spec");
|
|
18
|
+
r.get("/", async (c) => {
|
|
19
|
+
if (!(await passesGate(c, config)))
|
|
20
|
+
return c.text("Forbidden", 403);
|
|
21
|
+
return c.html(swaggerHtml(`${base}/openapi.json`, config));
|
|
22
|
+
}).name("ui");
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build an OpenAPI 3.0 document from Keel's own route table. Every registered
|
|
3
|
+
* route already carries its methods, path, name, param constraints, and any
|
|
4
|
+
* `.config(apiDoc(...))` metadata — that's the whole input. Paths and methods
|
|
5
|
+
* come for free; request/response schemas come from the docs metadata a route
|
|
6
|
+
* chose to attach. A route with no metadata still appears, just bare.
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteDefinition } from "../core/http/router.js";
|
|
9
|
+
import type { OpenApiConfig } from "./config.js";
|
|
10
|
+
export interface OpenApiDocument {
|
|
11
|
+
openapi: string;
|
|
12
|
+
info: {
|
|
13
|
+
title: string;
|
|
14
|
+
version: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
};
|
|
17
|
+
servers?: {
|
|
18
|
+
url: string;
|
|
19
|
+
}[];
|
|
20
|
+
paths: Record<string, Record<string, unknown>>;
|
|
21
|
+
}
|
|
22
|
+
/** Assemble the whole document from the route table. */
|
|
23
|
+
export declare function buildSpec(routes: RouteDefinition[], config: OpenApiConfig, base: string): OpenApiDocument;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build an OpenAPI 3.0 document from Keel's own route table. Every registered
|
|
3
|
+
* route already carries its methods, path, name, param constraints, and any
|
|
4
|
+
* `.config(apiDoc(...))` metadata — that's the whole input. Paths and methods
|
|
5
|
+
* come for free; request/response schemas come from the docs metadata a route
|
|
6
|
+
* chose to attach. A route with no metadata still appears, just bare.
|
|
7
|
+
*/
|
|
8
|
+
import { OPENAPI_KEY } from "./doc.js";
|
|
9
|
+
import { toJsonSchema } from "./zod.js";
|
|
10
|
+
/** `/users/:id` (and `:id?`) → `/users/{id}` — OpenAPI's path-template syntax. */
|
|
11
|
+
function toTemplate(path) {
|
|
12
|
+
return path.replace(/:([A-Za-z0-9_]+)\??/g, "{$1}");
|
|
13
|
+
}
|
|
14
|
+
/** The `:param` names in a path, in order. */
|
|
15
|
+
function paramNames(path) {
|
|
16
|
+
return [...path.matchAll(/:([A-Za-z0-9_]+)\??/g)].map((m) => m[1]);
|
|
17
|
+
}
|
|
18
|
+
/** An operationId that's stable and unique: the route name, else method + path. */
|
|
19
|
+
function operationId(route, method) {
|
|
20
|
+
if (route.name)
|
|
21
|
+
return route.name;
|
|
22
|
+
const slug = route.path.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
23
|
+
return `${method.toLowerCase()}_${slug || "root"}`;
|
|
24
|
+
}
|
|
25
|
+
/** Path parameters — from the URL template, enriched by any `params` schema/`where`. */
|
|
26
|
+
function pathParameters(route, doc) {
|
|
27
|
+
const paramsSchema = doc?.request?.params ? toJsonSchema(doc.request.params) : undefined;
|
|
28
|
+
const props = (paramsSchema?.properties ?? {});
|
|
29
|
+
return paramNames(route.path).map((name) => {
|
|
30
|
+
const schema = props[name] ?? { type: "string" };
|
|
31
|
+
// A `.where("id", /\d+/)` constraint becomes a pattern hint.
|
|
32
|
+
if (route.wheres[name] && !("pattern" in schema)) {
|
|
33
|
+
schema.pattern = `^${route.wheres[name]}$`;
|
|
34
|
+
}
|
|
35
|
+
return { name, in: "path", required: true, schema };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Query parameters — expanded from a `query` schema's top-level properties. */
|
|
39
|
+
function queryParameters(doc) {
|
|
40
|
+
const schema = doc?.request?.query ? toJsonSchema(doc.request.query) : undefined;
|
|
41
|
+
if (!schema?.properties)
|
|
42
|
+
return [];
|
|
43
|
+
const required = new Set(schema.required ?? []);
|
|
44
|
+
return Object.entries(schema.properties).map(([name, propSchema]) => ({
|
|
45
|
+
name,
|
|
46
|
+
in: "query",
|
|
47
|
+
required: required.has(name),
|
|
48
|
+
schema: propSchema,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
const HAS_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
52
|
+
/** Build one operation object for a (route, method) pair. */
|
|
53
|
+
function buildOperation(route, method, doc) {
|
|
54
|
+
const op = {
|
|
55
|
+
operationId: doc?.operationId ?? operationId(route, method),
|
|
56
|
+
tags: doc?.tags ?? [defaultTag(route.path)],
|
|
57
|
+
parameters: [...pathParameters(route, doc), ...queryParameters(doc)],
|
|
58
|
+
};
|
|
59
|
+
if (doc?.summary)
|
|
60
|
+
op.summary = doc.summary;
|
|
61
|
+
if (doc?.description)
|
|
62
|
+
op.description = doc.description;
|
|
63
|
+
if (doc?.deprecated)
|
|
64
|
+
op.deprecated = true;
|
|
65
|
+
const body = doc?.request?.body ? toJsonSchema(doc.request.body) : undefined;
|
|
66
|
+
if (body && HAS_BODY.has(method)) {
|
|
67
|
+
op.requestBody = { required: true, content: { "application/json": { schema: body } } };
|
|
68
|
+
}
|
|
69
|
+
op.responses = buildResponses(doc);
|
|
70
|
+
return op;
|
|
71
|
+
}
|
|
72
|
+
/** Responses from the docs, or a sensible default, plus a 422 when input is validated. */
|
|
73
|
+
function buildResponses(doc) {
|
|
74
|
+
const responses = {};
|
|
75
|
+
if (doc?.responses) {
|
|
76
|
+
for (const [status, res] of Object.entries(doc.responses)) {
|
|
77
|
+
const schema = res.schema ? toJsonSchema(res.schema) : undefined;
|
|
78
|
+
responses[status] = {
|
|
79
|
+
description: res.description ?? "",
|
|
80
|
+
...(schema ? { content: { "application/json": { schema } } } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!Object.keys(responses).length)
|
|
85
|
+
responses["200"] = { description: "OK" };
|
|
86
|
+
// A documented request means validation can reject it.
|
|
87
|
+
if (doc?.request && !responses["422"]) {
|
|
88
|
+
responses["422"] = { description: "Validation failed" };
|
|
89
|
+
}
|
|
90
|
+
return responses;
|
|
91
|
+
}
|
|
92
|
+
/** The tag for an undocumented route: its first path segment. */
|
|
93
|
+
function defaultTag(path) {
|
|
94
|
+
const seg = path.split("/").filter(Boolean)[0] ?? "default";
|
|
95
|
+
return seg.startsWith(":") ? "default" : seg;
|
|
96
|
+
}
|
|
97
|
+
/** Whether a route is left out of the spec. */
|
|
98
|
+
function skip(route, config, base) {
|
|
99
|
+
const doc = route.config?.[OPENAPI_KEY];
|
|
100
|
+
if (doc?.hidden)
|
|
101
|
+
return true;
|
|
102
|
+
if (route.path.includes("*"))
|
|
103
|
+
return true; // wildcard/asset routes aren't API endpoints
|
|
104
|
+
if (route.path === base || route.path.startsWith(`${base}/`))
|
|
105
|
+
return true; // the docs' own routes
|
|
106
|
+
return config.ignorePaths.some((p) => route.path.startsWith(p));
|
|
107
|
+
}
|
|
108
|
+
/** Assemble the whole document from the route table. */
|
|
109
|
+
export function buildSpec(routes, config, base) {
|
|
110
|
+
const paths = {};
|
|
111
|
+
for (const route of routes) {
|
|
112
|
+
if (skip(route, config, base))
|
|
113
|
+
continue;
|
|
114
|
+
const doc = route.config?.[OPENAPI_KEY];
|
|
115
|
+
const template = toTemplate(route.path);
|
|
116
|
+
for (const method of route.methods) {
|
|
117
|
+
if (method === "HEAD" || method === "OPTIONS")
|
|
118
|
+
continue;
|
|
119
|
+
(paths[template] ??= {})[method.toLowerCase()] = buildOperation(route, method, doc);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
openapi: "3.0.3",
|
|
124
|
+
info: {
|
|
125
|
+
title: config.title,
|
|
126
|
+
version: config.version,
|
|
127
|
+
...(config.description ? { description: config.description } : {}),
|
|
128
|
+
},
|
|
129
|
+
...(config.servers.length ? { servers: config.servers.map((url) => ({ url })) } : {}),
|
|
130
|
+
paths,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs UI: a Swagger UI shell pointed at the generated spec. Swagger UI's
|
|
3
|
+
* assets load from the configured `cdn` (pin it, or point it at a copy you host)
|
|
4
|
+
* — the one external dependency in this package, kept to the UI. The spec itself
|
|
5
|
+
* (`/openapi.json`) is generated with zero dependencies and works anywhere.
|
|
6
|
+
*/
|
|
7
|
+
import type { OpenApiConfig } from "./config.js";
|
|
8
|
+
export declare function swaggerHtml(specUrl: string, cfg: OpenApiConfig): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs UI: a Swagger UI shell pointed at the generated spec. Swagger UI's
|
|
3
|
+
* assets load from the configured `cdn` (pin it, or point it at a copy you host)
|
|
4
|
+
* — the one external dependency in this package, kept to the UI. The spec itself
|
|
5
|
+
* (`/openapi.json`) is generated with zero dependencies and works anywhere.
|
|
6
|
+
*/
|
|
7
|
+
import { escapeHtml } from "../core/template.js";
|
|
8
|
+
export function swaggerHtml(specUrl, cfg) {
|
|
9
|
+
const cdn = cfg.cdn.replace(/\/+$/, "");
|
|
10
|
+
return `<!doctype html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8" />
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
15
|
+
<title>${escapeHtml(cfg.title)} — API docs</title>
|
|
16
|
+
<link rel="stylesheet" href="${cdn}/swagger-ui.css" />
|
|
17
|
+
<style>body { margin: 0; }</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="swagger-ui"></div>
|
|
21
|
+
<script src="${cdn}/swagger-ui-bundle.js" crossorigin></script>
|
|
22
|
+
<script>
|
|
23
|
+
window.ui = SwaggerUIBundle({
|
|
24
|
+
url: ${JSON.stringify(specUrl)},
|
|
25
|
+
dom_id: "#swagger-ui",
|
|
26
|
+
deepLinking: true,
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema conversion. A route documents itself with the same Zod schemas it
|
|
3
|
+
* validates with; the spec needs JSON Schema. Zod 4 ships that conversion
|
|
4
|
+
* (`z.toJSONSchema`), so this is a thin, defensive wrapper: Zod schemas are
|
|
5
|
+
* converted (targeting OpenAPI 3.0's dialect), plain JSON Schema objects pass
|
|
6
|
+
* through untouched, and anything unrecognized yields `undefined` rather than
|
|
7
|
+
* throwing — a missing schema shouldn't sink the whole document.
|
|
8
|
+
*/
|
|
9
|
+
type JsonSchema = Record<string, unknown>;
|
|
10
|
+
/** Convert a Zod schema or JSON Schema to an OpenAPI-flavoured JSON Schema. */
|
|
11
|
+
export declare function toJsonSchema(schema: unknown): JsonSchema | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema conversion. A route documents itself with the same Zod schemas it
|
|
3
|
+
* validates with; the spec needs JSON Schema. Zod 4 ships that conversion
|
|
4
|
+
* (`z.toJSONSchema`), so this is a thin, defensive wrapper: Zod schemas are
|
|
5
|
+
* converted (targeting OpenAPI 3.0's dialect), plain JSON Schema objects pass
|
|
6
|
+
* through untouched, and anything unrecognized yields `undefined` rather than
|
|
7
|
+
* throwing — a missing schema shouldn't sink the whole document.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
/** Duck-type a Zod (or Zod-like) schema. */
|
|
11
|
+
function isZod(value) {
|
|
12
|
+
return (!!value &&
|
|
13
|
+
typeof value.parse === "function" &&
|
|
14
|
+
typeof value.safeParse === "function");
|
|
15
|
+
}
|
|
16
|
+
/** A value that already looks like a JSON Schema (or a `$ref`). */
|
|
17
|
+
function isJsonSchema(value) {
|
|
18
|
+
if (!value || typeof value !== "object" || isZod(value))
|
|
19
|
+
return false;
|
|
20
|
+
const keys = ["type", "$ref", "properties", "items", "oneOf", "anyOf", "allOf", "enum"];
|
|
21
|
+
return keys.some((k) => k in value);
|
|
22
|
+
}
|
|
23
|
+
/** Convert a Zod schema or JSON Schema to an OpenAPI-flavoured JSON Schema. */
|
|
24
|
+
export function toJsonSchema(schema) {
|
|
25
|
+
if (!schema)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (isJsonSchema(schema))
|
|
28
|
+
return schema;
|
|
29
|
+
if (!isZod(schema))
|
|
30
|
+
return undefined;
|
|
31
|
+
const convert = z.toJSONSchema;
|
|
32
|
+
if (typeof convert !== "function")
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
// OpenAPI 3.0 uses `nullable` and a few other divergences from raw JSON Schema.
|
|
36
|
+
return convert(schema, { target: "openapi-3.0", io: "input" });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
try {
|
|
40
|
+
return convert(schema);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch configuration. Defaults live here and are merged under `config("watch")`
|
|
3
|
+
* by the provider; an app overrides any of them in `config/watch.ts` (publish it
|
|
4
|
+
* with `keel vendor:publish --tag watch-config`).
|
|
5
|
+
*/
|
|
6
|
+
import type { EntryType } from "./entry.js";
|
|
7
|
+
export interface WatchConfig {
|
|
8
|
+
/** Master switch. Off → no watchers install and the dashboard 404s. */
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
/** URL prefix the dashboard and API mount under. Default: "watch". */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Where entries live. "database" persists them; "memory" is a per-process ring. */
|
|
13
|
+
storage: "database" | "memory";
|
|
14
|
+
/** The database connection to use (when storage is "database"). */
|
|
15
|
+
connection?: string;
|
|
16
|
+
/** The table entries are stored in. */
|
|
17
|
+
table: string;
|
|
18
|
+
/** Ring size for the memory store, and the default page size for the API. */
|
|
19
|
+
limit: number;
|
|
20
|
+
/** Record only this fraction of eligible entries (0–1). 1 records everything. */
|
|
21
|
+
sampling: number;
|
|
22
|
+
/** Tag queries slower than this (ms) with `slow`. */
|
|
23
|
+
slowQueryMs: number;
|
|
24
|
+
/** Request path prefixes to never record (the dashboard's own are always skipped). */
|
|
25
|
+
ignorePaths: string[];
|
|
26
|
+
/** Delete entries older than this many hours when `watch:prune` runs. */
|
|
27
|
+
retentionHours: number;
|
|
28
|
+
/** Per-type on/off switches. Noisy watchers (cache, event) default off. */
|
|
29
|
+
watchers: Record<EntryType, boolean>;
|
|
30
|
+
}
|
|
31
|
+
export declare const defaultConfig: WatchConfig;
|
|
32
|
+
/** Read the effective Watch config off the application, filling any gaps. */
|
|
33
|
+
export declare function resolveConfig(): WatchConfig;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch configuration. Defaults live here and are merged under `config("watch")`
|
|
3
|
+
* by the provider; an app overrides any of them in `config/watch.ts` (publish it
|
|
4
|
+
* with `keel vendor:publish --tag watch-config`).
|
|
5
|
+
*/
|
|
6
|
+
import { config } from "../core/helpers.js";
|
|
7
|
+
export const defaultConfig = {
|
|
8
|
+
enabled: true,
|
|
9
|
+
path: "watch",
|
|
10
|
+
storage: "database",
|
|
11
|
+
table: "watch_entries",
|
|
12
|
+
limit: 100,
|
|
13
|
+
sampling: 1,
|
|
14
|
+
slowQueryMs: 100,
|
|
15
|
+
ignorePaths: [],
|
|
16
|
+
retentionHours: 24,
|
|
17
|
+
watchers: {
|
|
18
|
+
request: true,
|
|
19
|
+
query: true,
|
|
20
|
+
exception: true,
|
|
21
|
+
log: true,
|
|
22
|
+
mail: true,
|
|
23
|
+
job: true,
|
|
24
|
+
notification: true,
|
|
25
|
+
cache: false,
|
|
26
|
+
event: false,
|
|
27
|
+
schedule: true,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
/** Read the effective Watch config off the application, filling any gaps. */
|
|
31
|
+
export function resolveConfig() {
|
|
32
|
+
const raw = config("watch", {});
|
|
33
|
+
return {
|
|
34
|
+
...defaultConfig,
|
|
35
|
+
...raw,
|
|
36
|
+
watchers: { ...defaultConfig.watchers, ...(raw.watchers ?? {}) },
|
|
37
|
+
};
|
|
38
|
+
}
|