@shaferllc/keel 0.66.0 → 0.74.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 +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- 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 +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -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/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- 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 +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -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.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- 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.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- 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 +170 -1
- package/dist/core/testing.js +504 -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/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- 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 +20 -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 +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
package/docs/ai.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Building Keel apps with AI
|
|
2
|
+
|
|
3
|
+
Keel is built to be **written with an AI agent**. This page is the map of the
|
|
4
|
+
AI-facing surface: an MCP server, machine-readable docs (`llms.txt` /
|
|
5
|
+
`llms-full.txt`), an agent playbook (`AGENTS.md`), and code generators an agent
|
|
6
|
+
can drive directly.
|
|
7
|
+
|
|
8
|
+
If you only read one thing: point your agent at the [MCP server](#the-mcp-server)
|
|
9
|
+
and have it call `keel_overview` first.
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
An agent working in a Keel app needs three things: to know the **conventions**
|
|
14
|
+
(where things live, what imports to use), to **look up APIs and guides** without
|
|
15
|
+
hallucinating, and to **generate correct boilerplate**. Keel provides each as a
|
|
16
|
+
first-class, always-current surface — generated from the same source as the
|
|
17
|
+
human docs, so they never drift.
|
|
18
|
+
|
|
19
|
+
## The MCP server
|
|
20
|
+
|
|
21
|
+
Keel ships an [MCP](https://modelcontextprotocol.io) server that exposes its
|
|
22
|
+
documentation, the full public API surface (380+ exports), the generators, and
|
|
23
|
+
its conventions to any MCP-capable client.
|
|
24
|
+
|
|
25
|
+
### Connect it
|
|
26
|
+
|
|
27
|
+
**Claude Code**, in an app that depends on `@shaferllc/keel`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
claude mcp add keel -- npx -y keel-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Hacking on the framework repo itself:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
claude mcp add keel -- npm --prefix /path/to/keel run mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**`.mcp.json`, Cursor, Windsurf, or any client** that reads the standard config:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"keel": { "command": "npx", "args": ["-y", "keel-mcp"] }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The server speaks over stdio and prints its banner to **stderr** (stdout is the
|
|
50
|
+
protocol channel). It resolves docs from the installed `@shaferllc/keel`
|
|
51
|
+
package, so it always matches your installed version.
|
|
52
|
+
|
|
53
|
+
### Tools
|
|
54
|
+
|
|
55
|
+
| Tool | What it does |
|
|
56
|
+
|------|--------------|
|
|
57
|
+
| `keel_overview` | Version, conventions, folder layout, every doc topic, and the generators. **Call this first.** |
|
|
58
|
+
| `keel_search_docs` | Full-text search across all guides; returns snippets + slugs. |
|
|
59
|
+
| `keel_read_doc` | A full guide by slug, optionally with its runnable example appended. |
|
|
60
|
+
| `keel_search_api` | Search the public export surface; returns each symbol's module and its guide. |
|
|
61
|
+
| `keel_list_generators` | The `keel make:*` generators, what they produce, and their flags. |
|
|
62
|
+
| `keel_scaffold` | Generate a controller/provider/middleware/factory/seeder/job/notification/transformer stub. Returns code + target path — it does **not** write to disk. |
|
|
63
|
+
|
|
64
|
+
### Resources
|
|
65
|
+
|
|
66
|
+
- `keel://overview` — the same orientation text as `keel_overview`
|
|
67
|
+
- `keel://llms-full` — every guide concatenated (drop into a fresh context)
|
|
68
|
+
- `keel://docs/<slug>` — one resource per guide (e.g. `keel://docs/routing`)
|
|
69
|
+
|
|
70
|
+
### A typical agent loop
|
|
71
|
+
|
|
72
|
+
1. `keel_overview` → learn the conventions and topic list.
|
|
73
|
+
2. `keel_search_docs { query: "belongsToMany pivot" }` → find the right guide.
|
|
74
|
+
3. `keel_read_doc { slug: "models", include_example: true }` → read it in full.
|
|
75
|
+
4. `keel_scaffold { kind: "controller", name: "Post", resource: true }` → get the stub.
|
|
76
|
+
5. Write the file, add the route, run `npm run typecheck`.
|
|
77
|
+
|
|
78
|
+
## `llms.txt` and `llms-full.txt`
|
|
79
|
+
|
|
80
|
+
At the package root:
|
|
81
|
+
|
|
82
|
+
- **[`llms.txt`](../llms.txt)** — the [llms.txt-spec](https://llmstxt.org) index:
|
|
83
|
+
a titled, summarized, linked list of every guide and example. Good for AI
|
|
84
|
+
crawlers and "add this URL as context" flows.
|
|
85
|
+
- **[`llms-full.txt`](../llms-full.txt)** — every guide concatenated into one
|
|
86
|
+
file (~17k lines), ordered from getting-started outward. Paste it into a fresh
|
|
87
|
+
context window when you want the agent to have all of Keel at once.
|
|
88
|
+
|
|
89
|
+
Both are generated by `npm run build:ai` from `docs/` and shipped in the npm
|
|
90
|
+
package, so they're available at `node_modules/@shaferllc/keel/llms-full.txt`.
|
|
91
|
+
|
|
92
|
+
## `AGENTS.md`
|
|
93
|
+
|
|
94
|
+
[`AGENTS.md`](../AGENTS.md) at the repo root is the agent playbook: the one
|
|
95
|
+
import rule, the folder map, the container/provider mental model, a "how to add
|
|
96
|
+
X" table, the commands, and the guardrails (typecheck before finishing, rerun
|
|
97
|
+
`build:ai` after doc changes). It follows the emerging cross-tool `AGENTS.md`
|
|
98
|
+
convention and is shipped in the package. `CLAUDE.md` points to it.
|
|
99
|
+
|
|
100
|
+
## Generators
|
|
101
|
+
|
|
102
|
+
Everything scaffoldable is available three ways, all emitting the same stubs:
|
|
103
|
+
|
|
104
|
+
- **MCP:** `keel_scaffold` (returns code; you write it)
|
|
105
|
+
- **Console:** `keel make:controller Post -r` (writes the file, won't overwrite)
|
|
106
|
+
- **By hand:** copy from `keel_read_doc` examples
|
|
107
|
+
|
|
108
|
+
See [the console guide](./console.md) for the full command list.
|
|
109
|
+
|
|
110
|
+
## Keeping it current
|
|
111
|
+
|
|
112
|
+
The AI surface is generated, not hand-maintained:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run build:ai # regenerates llms.txt, llms-full.txt, docs/ai-manifest.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`npm run build` runs it automatically before compiling. After you add or edit a
|
|
119
|
+
doc, or change the public exports in `src/core/index.ts`, run `build:ai` so the
|
|
120
|
+
manifest the MCP server reads stays in sync. `docs/ai-manifest.json` is the
|
|
121
|
+
single machine-readable index (docs + API + generators); treat it as generated
|
|
122
|
+
output.
|
|
123
|
+
|
|
124
|
+
## See also
|
|
125
|
+
|
|
126
|
+
- [Getting Started](./getting-started.md) — the human first-hour guide
|
|
127
|
+
- [Architecture](./architecture.md) — how a request flows through Keel
|
|
128
|
+
- [The Console](./console.md) — the `keel` command and generators
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# API Resources
|
|
2
|
+
|
|
3
|
+
`apiResource(router, Model, options)` generates a full CRUD REST API from a Keel
|
|
4
|
+
[model](./models.md) — the [Remult](https://remult.dev) idea done the Keel way:
|
|
5
|
+
explicit, server-side, and composed from pieces you already have. It's imported
|
|
6
|
+
from `@shaferllc/keel/api`.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { apiResource } from "@shaferllc/keel/api";
|
|
10
|
+
import { Post } from "../app/Models/Post.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
export default function routes(router) {
|
|
14
|
+
apiResource(router, Post, {
|
|
15
|
+
filter: ["status", "authorId"],
|
|
16
|
+
sort: ["createdAt", "title"],
|
|
17
|
+
body: z.object({ title: z.string(), body: z.string(), status: z.string() }),
|
|
18
|
+
access: { read: true, write: (c) => isEditor(c) },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That registers five routes:
|
|
24
|
+
|
|
25
|
+
| Method | Path | Action |
|
|
26
|
+
|--------|------|--------|
|
|
27
|
+
| `GET` | `/posts` | list (filtered, sorted, paginated) |
|
|
28
|
+
| `GET` | `/posts/:id` | read one |
|
|
29
|
+
| `POST` | `/posts` | create |
|
|
30
|
+
| `PUT` / `PATCH` | `/posts/:id` | update |
|
|
31
|
+
| `DELETE` | `/posts/:id` | delete |
|
|
32
|
+
|
|
33
|
+
They're real routes, so [`@shaferllc/keel/openapi`](./openapi.md) documents them
|
|
34
|
+
automatically, and writes go through the model's mass-assignment guard and your
|
|
35
|
+
Zod schema.
|
|
36
|
+
|
|
37
|
+
## Access is deny-by-default
|
|
38
|
+
|
|
39
|
+
An auto-generated API that's open by default is a footgun. Every action whose
|
|
40
|
+
access you don't declare returns **403**. You opt routes open — never shut.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
access: {
|
|
44
|
+
read: true, // list + read: anyone
|
|
45
|
+
write: (c) => auth().check(), // create + update + delete: signed-in only
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Rules resolve per action: the action's own key (`list`, `get`, `create`,
|
|
50
|
+
`update`, `delete`), then the `read` / `write` shorthand, then `all`, then denied.
|
|
51
|
+
Each rule is a boolean or a `(c) => boolean | Promise<boolean>` predicate.
|
|
52
|
+
|
|
53
|
+
## Filtering, sorting, pagination — allow-listed
|
|
54
|
+
|
|
55
|
+
The list endpoint reads the query string, but **only** columns you allow-list:
|
|
56
|
+
|
|
57
|
+
- `filter: ["status"]` → `GET /posts?status=published` filters; `?secret=x` is
|
|
58
|
+
ignored. Nothing reaches SQL unless it's on the list.
|
|
59
|
+
- `sort: ["title", "createdAt"]` → `GET /posts?sort=title,-createdAt` (a `-`
|
|
60
|
+
prefix is descending); unknown columns are dropped.
|
|
61
|
+
- `GET /posts?page=2&perPage=20` paginates. `perPage` is clamped to `maxPerPage`
|
|
62
|
+
(default 100) — the guard against "give me everything".
|
|
63
|
+
|
|
64
|
+
The response is a paginated envelope:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "data": [ … ], "meta": { "total": 42, "perPage": 20, "currentPage": 2, "lastPage": 3 } }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Row-level security with `scope`
|
|
71
|
+
|
|
72
|
+
`scope` constrains the base query for **every** row operation — list, read,
|
|
73
|
+
update, delete. A row outside the scope reads as 404, so it can't be fetched,
|
|
74
|
+
changed, or removed:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
apiResource(router, Post, {
|
|
78
|
+
access: { read: true, write: true },
|
|
79
|
+
scope: (q, c) => q.where("authorId", currentUserId(c)), // only your own posts
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Shaping input and output
|
|
84
|
+
|
|
85
|
+
- **`body` / `createBody` / `updateBody`** — a Zod schema validating writes
|
|
86
|
+
(a failure is a 422). It also becomes the request-body schema in the OpenAPI
|
|
87
|
+
docs.
|
|
88
|
+
- **`beforeWrite(data, c, action)`** — mutate the write payload (stamp an owner
|
|
89
|
+
id, a timestamp) before it's saved.
|
|
90
|
+
- **`transform`** — shape the output: a `(model, c) => …` function, or a Keel
|
|
91
|
+
[Transformer](./transformers.md) (its `item`/`collection` are used).
|
|
92
|
+
|
|
93
|
+
## Options reference
|
|
94
|
+
|
|
95
|
+
| Option | Purpose |
|
|
96
|
+
|--------|---------|
|
|
97
|
+
| `path` | Base path (default: the model's table). |
|
|
98
|
+
| `name` | Route-name prefix (default: the path). |
|
|
99
|
+
| `only` / `except` | Restrict which of the five actions are generated. |
|
|
100
|
+
| `filter` / `sort` | Allow-listed columns for `?filter` and `?sort`. |
|
|
101
|
+
| `perPage` / `maxPerPage` | Page-size default and ceiling. |
|
|
102
|
+
| `body` / `createBody` / `updateBody` | Write validation schemas. |
|
|
103
|
+
| `access` | Per-action access rules (deny by default). |
|
|
104
|
+
| `scope` | Row-level query constraint for every operation. |
|
|
105
|
+
| `transform` | Output shaping. |
|
|
106
|
+
| `beforeWrite` | Mutate the payload before save. |
|
|
107
|
+
| `tags` | OpenAPI tags for the routes. |
|
|
108
|
+
|
|
109
|
+
Global pagination defaults live in `config/api.ts` (register the optional
|
|
110
|
+
`ApiServiceProvider`, then `keel vendor:publish --tag api-config`).
|
|
111
|
+
|
|
112
|
+
## What this isn't
|
|
113
|
+
|
|
114
|
+
Remult also ships an isomorphic, type-safe **frontend** client that shares the
|
|
115
|
+
model with the server. Keel deliberately stops at the server boundary: this
|
|
116
|
+
generates a plain REST API you call however you like (fetch, your Inertia pages,
|
|
117
|
+
a mobile app). That keeps the model server-only and the wire contract explicit —
|
|
118
|
+
and it's exactly the contract the OpenAPI docs describe.
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Keel is small on purpose. This page maps the pieces and traces a request from
|
|
4
|
+
socket to response. Nothing here is magic — every layer is a short, readable file
|
|
5
|
+
in `src/core/`, and this guide is mostly a reading order for it.
|
|
6
|
+
|
|
7
|
+
## The layers
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────────────────────┐
|
|
11
|
+
│ bin/keel.ts console entry (serve, make:*, …) │
|
|
12
|
+
├─────────────────────────────────────────────────────────┤
|
|
13
|
+
│ bootstrap/app.ts createApplication() │
|
|
14
|
+
│ └─ boots providers, binds HTTP kernel, loads routes │
|
|
15
|
+
├─────────────────────────────────────────────────────────┤
|
|
16
|
+
│ Application (extends Container) │
|
|
17
|
+
│ ├─ loads .env + config/*.ts │
|
|
18
|
+
│ └─ provider register() → boot() lifecycle │
|
|
19
|
+
├─────────────────────────────────────────────────────────┤
|
|
20
|
+
│ Container bind / singleton / instance / make │
|
|
21
|
+
├─────────────────────────────────────────────────────────┤
|
|
22
|
+
│ HttpKernel global middleware → compiles routes → Hono │
|
|
23
|
+
├─────────────────────────────────────────────────────────┤
|
|
24
|
+
│ @hono/node-server the actual HTTP server │
|
|
25
|
+
└─────────────────────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Read it top to bottom as the flow of control at boot, and bottom to top as the
|
|
29
|
+
flow of a request. The console and the server both enter through
|
|
30
|
+
`createApplication()` — the difference is only what they do with the app once
|
|
31
|
+
it's booted (serve it, or run a command against it).
|
|
32
|
+
|
|
33
|
+
## Core building blocks
|
|
34
|
+
|
|
35
|
+
- **`Container`** ([container.ts](../src/core/container.ts)) — the dependency
|
|
36
|
+
registry. Two maps (bindings, cached instances) and a `make()` resolver.
|
|
37
|
+
- **`Application`** ([application.ts](../src/core/application.ts)) — a
|
|
38
|
+
`Container` with a lifecycle: load env, auto-load config, register and boot
|
|
39
|
+
providers.
|
|
40
|
+
- **`Config`** ([config.ts](../src/core/config.ts)) — a dot-notation repository
|
|
41
|
+
plus the `env()` coercion helper.
|
|
42
|
+
- **`ServiceProvider`** ([provider.ts](../src/core/provider.ts)) — the
|
|
43
|
+
`register()` / `boot()` contract.
|
|
44
|
+
- **`Router`** ([http/router.ts](../src/core/http/router.ts)) — collects route
|
|
45
|
+
definitions; resolves controller tuples out of the container.
|
|
46
|
+
- **`HttpKernel`** ([http/kernel.ts](../src/core/http/kernel.ts)) — holds global
|
|
47
|
+
middleware and compiles the router onto a Hono instance.
|
|
48
|
+
|
|
49
|
+
## The container is the center
|
|
50
|
+
|
|
51
|
+
Everything else hangs off the container. `Application` **is** a `Container` — it
|
|
52
|
+
extends it — so the same `bind` / `singleton` / `instance` / `make` surface that
|
|
53
|
+
registers a service also holds `Config`, `Router`, `View`, the `Logger`, and
|
|
54
|
+
your own controllers.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
app.singleton(Router, (a) => new Router(a)); // registered at construction
|
|
58
|
+
const router = app.make(Router); // resolved anywhere later
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Two properties make this the spine of the framework:
|
|
62
|
+
|
|
63
|
+
- **Everything resolves through one place.** A controller doesn't `new` its
|
|
64
|
+
dependencies — it receives the container in its constructor and pulls what it
|
|
65
|
+
needs, so tests can swap any binding for a fake without touching the code under
|
|
66
|
+
test.
|
|
67
|
+
- **Classes auto-resolve.** `make(SomeClass)` builds `SomeClass` even with no
|
|
68
|
+
explicit binding, handing its constructor the container. You only register a
|
|
69
|
+
binding when construction needs configuration or should be shared.
|
|
70
|
+
|
|
71
|
+
`bind` gives a fresh value each resolve; `singleton` caches after the first;
|
|
72
|
+
`instance` stores an already-built value. The global helpers (`make()`,
|
|
73
|
+
`bind()`, `config()`, `app()`) are thin wrappers that resolve against the active
|
|
74
|
+
application, so you rarely thread the container by hand. See
|
|
75
|
+
[The Service Container](./container.md) for the full API and resolution rules.
|
|
76
|
+
|
|
77
|
+
## Service providers wire it up
|
|
78
|
+
|
|
79
|
+
Providers are the seams where your services enter the container. Each has two
|
|
80
|
+
phases, and the split matters:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
84
|
+
register(): void {
|
|
85
|
+
// Bind only. Nothing else is guaranteed registered yet.
|
|
86
|
+
bind("clock", () => new Date().toISOString());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
boot(): void {
|
|
90
|
+
// Every provider has registered — safe to resolve and wire things.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`Application.boot()` runs **all** providers' `register()` before **any**
|
|
96
|
+
provider's `boot()`. That ordering is the whole point: `register()` may only
|
|
97
|
+
add bindings, so `boot()` can safely depend on anything any provider bound,
|
|
98
|
+
regardless of order. Reaching for another service inside `register()` is the
|
|
99
|
+
classic bug — the binding may not exist yet. [Service Providers](./providers.md)
|
|
100
|
+
goes deeper.
|
|
101
|
+
|
|
102
|
+
## Boot sequence
|
|
103
|
+
|
|
104
|
+
When `keel serve` runs:
|
|
105
|
+
|
|
106
|
+
1. **`createApplication()`** constructs the `Application` with the project root.
|
|
107
|
+
2. The constructor registers the active application (for global helpers) and
|
|
108
|
+
binds core services (`Config`, `Router`, `View`, `Events`, `Cache`,
|
|
109
|
+
`Logger`).
|
|
110
|
+
3. **`app.boot(providers)`**:
|
|
111
|
+
- loads `.env`, then every `config/*.ts` file into the `Config` repository,
|
|
112
|
+
- runs each provider's **`register()`** (bind-only phase),
|
|
113
|
+
- runs each provider's **`boot()`** (wire-up phase).
|
|
114
|
+
4. The HTTP kernel (`app/Http/Kernel.ts`) is bound as a singleton.
|
|
115
|
+
5. `routes/web.ts` registers routes on the `Router`.
|
|
116
|
+
6. `HttpKernel.build()` returns a Hono app; `@hono/node-server` serves it.
|
|
117
|
+
|
|
118
|
+
Steps 1–3 are identical whether you're serving or running a console command —
|
|
119
|
+
`createApplication()` is the single door in. Only steps 4–6 are HTTP-specific.
|
|
120
|
+
|
|
121
|
+
## The application object
|
|
122
|
+
|
|
123
|
+
`Application` is Keel's central object — the [container](./container.md) plus a
|
|
124
|
+
lifecycle. Beyond `register()`/`boot()`, it carries a small ergonomic surface
|
|
125
|
+
modelled on the classic service-app pattern (Feathers' `app`): a settings store,
|
|
126
|
+
an inline plugin hook, and app-level events.
|
|
127
|
+
|
|
128
|
+
**Configure with a plain function.** `register(Provider)` gives you the
|
|
129
|
+
two-phase register/boot lifecycle; `configure(fn)` is the one-shot alternative
|
|
130
|
+
for inline setup — call a function with the app, chain the next:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
app
|
|
134
|
+
.configure((a) => a.set("mail.from", "hi@keel.dev"))
|
|
135
|
+
.configure(installBilling); // (app) => { … }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Store app-wide values.** `set`/`get` are a thin façade over the `Config`
|
|
139
|
+
repository, so `app.set("db.url", …)` and `config().get("db.url")` read the same
|
|
140
|
+
store — no second bag to keep in sync:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
app.set("db.url", process.env.DATABASE_URL);
|
|
144
|
+
const url = app.get<string>("db.url");
|
|
145
|
+
const port = app.get("port", 3000); // typed fallback
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Emit and listen at the app level.** `on`/`once`/`off`/`emit` delegate to the
|
|
149
|
+
[`Events`](./events.md) singleton, so `app.on(...)` and the global `listen()`
|
|
150
|
+
helper share one emitter. Listeners may be async; `emit` awaits them in order:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const off = app.on("user.registered", (user) => sendWelcome(user));
|
|
154
|
+
await app.emit("user.registered", user);
|
|
155
|
+
off(); // unsubscribe
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### API reference
|
|
159
|
+
|
|
160
|
+
#### `app.configure(fn)`
|
|
161
|
+
|
|
162
|
+
Run a `Configurator` — `(app) => unknown` — against the app and return the app
|
|
163
|
+
for chaining. The lightweight alternative to a `ServiceProvider` when you don't
|
|
164
|
+
need the register/boot split.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
app.configure((a) => a.router().get("/health", () => "ok"));
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Notes: runs immediately and synchronously in call order. Its return value is
|
|
171
|
+
ignored (return-for-chaining is the app, not the fn's result). For anything that
|
|
172
|
+
must bind before another service boots, use a provider instead.
|
|
173
|
+
|
|
174
|
+
#### `app.set(key, value)` / `app.get(key, fallback?)`
|
|
175
|
+
|
|
176
|
+
Write and read an app-wide value. Both use dot-notation and are backed by
|
|
177
|
+
`Config`, so values set here are visible to `config()` and vice-versa. `set`
|
|
178
|
+
returns the app (chainable); `get` takes an optional typed fallback.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
app.set("app.name", "Keel");
|
|
182
|
+
config().get("app.name"); // "Keel"
|
|
183
|
+
app.get("app.name"); // "Keel"
|
|
184
|
+
app.get("app.locale", "en"); // fallback when unset
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Notes: because the store is shared with `Config`, prefer namespaced keys
|
|
188
|
+
(`"mail.from"`, not `"from"`) to avoid collisions with `config/*.ts` files.
|
|
189
|
+
|
|
190
|
+
#### `app.on(event, listener)` / `app.once(event, listener)`
|
|
191
|
+
|
|
192
|
+
Subscribe to an app event; `once` auto-unsubscribes after the first emission.
|
|
193
|
+
Both return an **unsubscribe function**. Delegates to the `Events` singleton.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const off = app.on<Order>("order.paid", (o) => fulfil(o));
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Notes: the listener signature is `(payload) => void | Promise<void>`. Identical
|
|
200
|
+
to `app.make(Events).on(...)` — the method is sugar so you rarely resolve
|
|
201
|
+
`Events` by hand.
|
|
202
|
+
|
|
203
|
+
#### `app.off(event, listener)`
|
|
204
|
+
|
|
205
|
+
Remove a listener registered with `on`/`once`. Returns the app (chainable). Pass
|
|
206
|
+
the *same* function reference used to subscribe.
|
|
207
|
+
|
|
208
|
+
#### `app.emit(event, payload?)`
|
|
209
|
+
|
|
210
|
+
Emit an app event, awaiting every listener in registration order. Returns a
|
|
211
|
+
`Promise<void>`. An async listener that rejects propagates out of `emit`.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
await app.emit("cache.cleared", { at: Date.now() });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Request lifecycle
|
|
218
|
+
|
|
219
|
+
For each incoming request:
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
request
|
|
223
|
+
→ Hono matches the route
|
|
224
|
+
→ contextStorage() stashes the context for the request helpers
|
|
225
|
+
→ context middleware sets c.get("app") = the container
|
|
226
|
+
→ global middleware stack (e.g. requestLogger) runs, in order
|
|
227
|
+
→ the route handler runs:
|
|
228
|
+
• a closure → called with (c)
|
|
229
|
+
• a [Controller, m] → controller resolved from the container,
|
|
230
|
+
then method(c) is called (DI in the ctor)
|
|
231
|
+
→ the handler's return value becomes the response
|
|
232
|
+
(a string is wrapped as HTML; a Response passes through)
|
|
233
|
+
→ middleware unwinds on the way back out
|
|
234
|
+
response
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
A few details worth knowing:
|
|
238
|
+
|
|
239
|
+
- **The context is stashed per request.** Before your middleware runs, the
|
|
240
|
+
kernel enables Hono's `contextStorage()`. That's what lets the [request
|
|
241
|
+
helpers](./request-response.md) (`request`, `param()`, `json()`) reach the
|
|
242
|
+
current request without you passing `c` around.
|
|
243
|
+
- **Controllers are resolved lazily, per request.** The router turns a
|
|
244
|
+
`[Controller, method]` tuple into a function that resolves the controller from
|
|
245
|
+
the container when the route fires — so constructor DI runs against the live
|
|
246
|
+
app. Tuples may also be `() => import(...)` loaders for code-splitting.
|
|
247
|
+
- **Errors funnel through the kernel.** A thrown `HttpException` renders at its
|
|
248
|
+
status; anything else is a 500. The kernel content-negotiates — HTML for
|
|
249
|
+
browsers, JSON otherwise — and hides internals unless `app.debug` is on.
|
|
250
|
+
Unmatched routes go through the same path as a `NotFoundException`.
|
|
251
|
+
|
|
252
|
+
[Middleware](./middleware.md) covers the stack in detail, and [Routing](./routing.md)
|
|
253
|
+
covers how handlers are declared and matched.
|
|
254
|
+
|
|
255
|
+
## Edge-safe by design
|
|
256
|
+
|
|
257
|
+
Keel's core imports **no** Node built-ins at module load. `fs`, `path`, `url`,
|
|
258
|
+
and `dotenv` are pulled in **dynamically**, and only when filesystem
|
|
259
|
+
discovery is enabled:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
// application.ts — dynamic, guarded, optional
|
|
263
|
+
const { readdir } = await import("node:fs/promises");
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The payoff is that the same `Application`, `Router`, `View`, and query builder
|
|
267
|
+
run unchanged on Cloudflare Workers, Deno, and Bun — anywhere with web-standard
|
|
268
|
+
`fetch`, `Request`, `Response`, and Web Crypto. On Workers, where there's no
|
|
269
|
+
filesystem to scan, you skip discovery and pass config inline:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
await app.boot(providers, { discoverConfig: false, config: { app: { name: "Keel" } } });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The second argument is `BootOptions`: `discoverConfig` (skip filesystem config
|
|
276
|
+
discovery — the default on the edge) and `config` (an inline config object merged
|
|
277
|
+
in). Together they let the app boot with no filesystem at all.
|
|
278
|
+
|
|
279
|
+
Everything that would normally reach for a platform API is designed around this
|
|
280
|
+
seam: the [database](./database.md) layer talks to a `Connection` you provide
|
|
281
|
+
rather than importing a driver; signed URLs use Web Crypto's `crypto.subtle`;
|
|
282
|
+
views render to strings with no filesystem. The rule is simple — the core owns
|
|
283
|
+
logic, the platform owns I/O, and the two meet at an interface you supply.
|
|
284
|
+
|
|
285
|
+
## Two repos: library and starter
|
|
286
|
+
|
|
287
|
+
Keel is distributed like most frameworks — a **library** you install plus an
|
|
288
|
+
**app** that depends on it:
|
|
289
|
+
|
|
290
|
+
| Repo | Role |
|
|
291
|
+
|------|------|
|
|
292
|
+
| `shaferllc/keel` | The framework. Published as `@shaferllc/keel`; userland imports `@shaferllc/keel/core`. |
|
|
293
|
+
| `shaferllc/keel-app` | The starter app — clone it to build something. Picks up core updates via `npm update`. |
|
|
294
|
+
|
|
295
|
+
The split mirrors the classic application-vs-library separation. Your code lives
|
|
296
|
+
in `app/` (controllers, providers, middleware); the framework lives behind the
|
|
297
|
+
package boundary. Because the two are versioned separately, a framework upgrade
|
|
298
|
+
is an ordinary dependency bump — your `app/` doesn't move. [Getting
|
|
299
|
+
Started](./getting-started.md#install) walks through both install paths.
|
|
300
|
+
|
|
301
|
+
## Design principles
|
|
302
|
+
|
|
303
|
+
- **One container, resolved everywhere.** Testability and composition follow
|
|
304
|
+
from routing every dependency through it.
|
|
305
|
+
- **Convention over configuration.** Fixed folders (`app/`, `config/`,
|
|
306
|
+
`routes/`, `bootstrap/`) mean no manual wiring for the common case.
|
|
307
|
+
- **Thin over clever.** The framework is a few hundred readable lines. When in
|
|
308
|
+
doubt, open the source — there is no hidden magic.
|
|
309
|
+
- **Wrap the best, own the surface.** Hono does HTTP; Keel owns the developer-
|
|
310
|
+
facing API so the underlying library can change without breaking your app.
|
|
311
|
+
[Built on Hono](./hono.md) draws that line precisely.
|
|
312
|
+
- **Edge-safe, driver-agnostic.** The core imports no database driver, no mail
|
|
313
|
+
SDK, no socket. Every backend — [database](./database.md), [mail](./mail.md),
|
|
314
|
+
[redis](./redis.md), [queues](./queues.md), [storage](./storage.md),
|
|
315
|
+
[broadcasting](./broadcasting.md) — plugs in behind a small interface, so the
|
|
316
|
+
same app runs on Node and on the edge. You bring the driver; Keel brings the
|
|
317
|
+
ergonomics.
|
|
318
|
+
- **Explicit over implicit.** No hidden runtime magic: body parsing is a method
|
|
319
|
+
you call, [templates](./templates.md) interpret rather than `eval`, and
|
|
320
|
+
providers register into one predictable global scope. Boring and predictable
|
|
321
|
+
beats clever and surprising.
|
|
322
|
+
|
|
323
|
+
## Extending Keel
|
|
324
|
+
|
|
325
|
+
The MVP core is deliberately small. Natural extension points:
|
|
326
|
+
|
|
327
|
+
- **New services** → a service provider that binds them into the container.
|
|
328
|
+
- **New console commands** → add to [cli/index.ts](../src/core/cli/index.ts).
|
|
329
|
+
- **New subsystems** (ORM, queues, mail) → a provider that registers the
|
|
330
|
+
subsystem plus a `config/*.ts` file for its settings. This is exactly how the
|
|
331
|
+
roadmap items will land.
|