@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/pages.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Pages
|
|
2
|
+
|
|
3
|
+
Page-based routing — **a file is a route**.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
resources/pages/index.tsx → /
|
|
7
|
+
resources/pages/about.tsx → /about
|
|
8
|
+
resources/pages/users/index.tsx → /users
|
|
9
|
+
resources/pages/users/[id].tsx → /users/:id
|
|
10
|
+
resources/pages/docs/[...slug].tsx → /docs/* (catch-all)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// resources/pages/users/[id].tsx
|
|
15
|
+
import { db, type Ctx, type PageProps } from "@shaferllc/keel/core";
|
|
16
|
+
|
|
17
|
+
export const loader = (c: Ctx) => db("users").where("id", c.req.param("id")).first();
|
|
18
|
+
|
|
19
|
+
export default function UserPage({ params, data }: PageProps<{ id: string }, User>) {
|
|
20
|
+
return (
|
|
21
|
+
<Layout>
|
|
22
|
+
<h1>{data.name}</h1>
|
|
23
|
+
<p>User #{params.id}</p>
|
|
24
|
+
</Layout>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's the whole page. No route file to keep in sync, no controller, no wiring.
|
|
30
|
+
|
|
31
|
+
## It doesn't replace the router — it drives it
|
|
32
|
+
|
|
33
|
+
Every page becomes an **ordinary named route**. `url()` finds it, route middleware
|
|
34
|
+
applies to it, and `keel routes` lists it. You can mix pages and hand-written
|
|
35
|
+
routes freely, and reach for a controller the moment a page outgrows a file.
|
|
36
|
+
|
|
37
|
+
That matters, because file-based routing is a lovely default and a bad prison.
|
|
38
|
+
Here it's a *convenience over* the router, not a replacement for it.
|
|
39
|
+
|
|
40
|
+
## Registering them
|
|
41
|
+
|
|
42
|
+
In a service provider's `boot()`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { pages } from "@shaferllc/keel/core";
|
|
46
|
+
|
|
47
|
+
export class PageServiceProvider extends ServiceProvider {
|
|
48
|
+
async boot(): Promise<void> {
|
|
49
|
+
await pages(); // scans resources/pages
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`pages()` reads the filesystem, so it's **Node-only**. On the edge, hand
|
|
55
|
+
`definePages()` a build-time manifest instead — Vite's `import.meta.glob` produces
|
|
56
|
+
exactly the map it wants:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
definePages(import.meta.glob("./pages/**/*.tsx", { eager: true }));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Same behavior, no filesystem.
|
|
63
|
+
|
|
64
|
+
## The file conventions
|
|
65
|
+
|
|
66
|
+
| File | URL |
|
|
67
|
+
|------|-----|
|
|
68
|
+
| `index.tsx` | `/` |
|
|
69
|
+
| `about.tsx` | `/about` |
|
|
70
|
+
| `users/index.tsx` | `/users` |
|
|
71
|
+
| `users/[id].tsx` | `/users/:id` |
|
|
72
|
+
| `users/[id]/edit.tsx` | `/users/:id/edit` |
|
|
73
|
+
| `teams/[team]/users/[id].tsx` | `/teams/:team/users/:id` |
|
|
74
|
+
| `docs/[...slug].tsx` | `/docs/*` — a catch-all; `params.slug` is the whole rest |
|
|
75
|
+
| `_layout.tsx` | **not a route** — a leading `_` keeps a file private |
|
|
76
|
+
|
|
77
|
+
A trailing `index` names its directory rather than a child of it. A leading
|
|
78
|
+
underscore is how layouts, partials, and helpers live *beside* your pages without
|
|
79
|
+
becoming URLs.
|
|
80
|
+
|
|
81
|
+
## Specificity is decided for you
|
|
82
|
+
|
|
83
|
+
This is the part file-based routing usually gets wrong:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
users/[id].tsx → /users/:id
|
|
87
|
+
users/new.tsx → /users/new
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Register `:id` first and `/users/new` is **unreachable forever** — `:id` happily
|
|
91
|
+
matches `"new"`. Whether your app works would come down to the order the
|
|
92
|
+
filesystem happened to hand back.
|
|
93
|
+
|
|
94
|
+
So pages are **sorted before they're registered**: literal segments beat
|
|
95
|
+
parameters, parameters beat catch-alls, and a catch-all is always the last resort.
|
|
96
|
+
`/users/new` wins, and the file layout stops being a trap.
|
|
97
|
+
|
|
98
|
+
## Loading data
|
|
99
|
+
|
|
100
|
+
`loader` runs before the page renders; whatever it returns arrives as `data`.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
export const loader = async (c: Ctx) => {
|
|
104
|
+
const post = await db("posts").where("slug", c.req.param("slug")).first();
|
|
105
|
+
if (!post) throw new NotFoundException();
|
|
106
|
+
return post;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default function PostPage({ data }: PageProps<{ slug: string }, Post>) {
|
|
110
|
+
return <article>{data.body}</article>;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It takes the request context, so it can read params, query, headers, or the
|
|
115
|
+
session. A page with no `loader` simply gets `data: undefined`.
|
|
116
|
+
|
|
117
|
+
## Middleware
|
|
118
|
+
|
|
119
|
+
Per page:
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
export const middleware = [authGuard()];
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or for every page at once:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
await pages({ middleware: [authGuard()] });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Both run before the `loader`, so a page that's refused never loads its data.
|
|
132
|
+
|
|
133
|
+
## Names and URLs
|
|
134
|
+
|
|
135
|
+
Each page gets a route name derived from its path — `users/[id].tsx` becomes
|
|
136
|
+
`users.id` — so URL generation works without you naming anything:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
router.url("users.id", { id: 5 }); // "/users/5"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Override it when the derived name is ugly:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
export const name = "users.show";
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Escape hatches
|
|
149
|
+
|
|
150
|
+
Move a page's URL without moving the file:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
export const path = "/pricing"; // even though the file is at marketing/plans.tsx
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Mount every page under a prefix:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await pages({ prefix: "/app" }); // index.tsx is now /app
|
|
160
|
+
await pages({ dir: "app/pages" }); // ...or keep them somewhere else
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## API reference
|
|
166
|
+
|
|
167
|
+
### `pages(options?)`
|
|
168
|
+
|
|
169
|
+
`pages(options?: PagesOptions): Promise<RegisteredPage[]>`
|
|
170
|
+
|
|
171
|
+
Scan a directory and register every page in it. **Node only** — it reads the
|
|
172
|
+
filesystem (`node:fs` is imported dynamically, so the core still loads on the
|
|
173
|
+
edge).
|
|
174
|
+
|
|
175
|
+
### `definePages(modules, options?)`
|
|
176
|
+
|
|
177
|
+
`definePages(modules: Record<string, PageModule>, options?: PagesOptions): RegisteredPage[]`
|
|
178
|
+
|
|
179
|
+
Register pages from a `file path → module` map. The edge-safe half — pair it with
|
|
180
|
+
`import.meta.glob`.
|
|
181
|
+
|
|
182
|
+
### `PagesOptions`
|
|
183
|
+
|
|
184
|
+
| Option | Meaning |
|
|
185
|
+
|--------|---------|
|
|
186
|
+
| `dir` | Where the pages live. Default `"resources/pages"` |
|
|
187
|
+
| `prefix` | Prefix every page's URL |
|
|
188
|
+
| `middleware` | Middleware applied to every page |
|
|
189
|
+
| `router` | The router to register on. Defaults to the application's |
|
|
190
|
+
|
|
191
|
+
### `PageModule`
|
|
192
|
+
|
|
193
|
+
What a page file exports.
|
|
194
|
+
|
|
195
|
+
| Export | Meaning |
|
|
196
|
+
|--------|---------|
|
|
197
|
+
| `default` | **Required.** The component. May be async |
|
|
198
|
+
| `loader` | `(ctx) => data` — runs before the page renders |
|
|
199
|
+
| `middleware` | Middleware for this page alone |
|
|
200
|
+
| `name` | The route name. Defaults to one derived from the path |
|
|
201
|
+
| `path` | Override the URL entirely |
|
|
202
|
+
|
|
203
|
+
### `PageProps<P, D>`
|
|
204
|
+
|
|
205
|
+
What the component receives: `params` (typed by `P`), `data` (whatever `loader`
|
|
206
|
+
returned, typed by `D`), and `ctx`.
|
|
207
|
+
|
|
208
|
+
### `RegisteredPage`
|
|
209
|
+
|
|
210
|
+
`{ file, pattern, name }` — what `pages()` / `definePages()` return, so you can
|
|
211
|
+
see exactly what got mounted.
|
|
212
|
+
|
|
213
|
+
### `routePattern(file)` / `routeName(file)`
|
|
214
|
+
|
|
215
|
+
The two pure functions behind the conventions, exported so you can test or reuse
|
|
216
|
+
them. `routePattern("users/[id].tsx")` → `"/users/:id"`;
|
|
217
|
+
`routeName("users/[id].tsx")` → `"users.id"`.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# Service Providers
|
|
2
|
+
|
|
3
|
+
Service providers are the central place to configure your application. Nearly
|
|
4
|
+
everything Keel boots — config, routing, your own services — is wired up in a
|
|
5
|
+
provider.
|
|
6
|
+
|
|
7
|
+
## The lifecycle
|
|
8
|
+
|
|
9
|
+
A provider has four hooks, run across the application's lifecycle. Only
|
|
10
|
+
`register()` and `boot()` are needed day-to-day; `ready()` and `shutdown()` are
|
|
11
|
+
there for work that must happen once the whole app is live, or as it stops:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { ServiceProvider } from "@shaferllc/keel/core";
|
|
15
|
+
|
|
16
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
17
|
+
register(): void {
|
|
18
|
+
// Phase 1. Bind things into the container.
|
|
19
|
+
// Do NOT resolve other services here — nothing is guaranteed
|
|
20
|
+
// to be registered yet.
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
boot(): void {
|
|
24
|
+
// Phase 2. Runs after EVERY provider has registered.
|
|
25
|
+
// Safe to resolve services and wire them together.
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ready(): void {
|
|
29
|
+
// Phase 3. Runs after every provider has booted and the app is fully up.
|
|
30
|
+
// For work that needs a live app — warm a cache, attach to the server.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
shutdown(): void {
|
|
34
|
+
// Phase 4. Runs on graceful termination, in reverse registration order.
|
|
35
|
+
// Close connections, flush queues, cancel timers.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `Application` runs **all** `register()` methods first, then **all** `boot()`
|
|
41
|
+
methods, then **all** `ready()` methods — that ordering is what lets providers
|
|
42
|
+
depend on each other without worrying about load order. On `app.terminate()`
|
|
43
|
+
(wired to SIGINT/SIGTERM by `keel serve`), every `shutdown()` runs in **reverse**
|
|
44
|
+
registration order (LIFO), so a provider tears down before the ones it depended
|
|
45
|
+
on. All four hooks may be `async` — the application awaits them. `ready()` and
|
|
46
|
+
`shutdown()` are optional and map onto the app's `onReady()` / `onShutdown()`
|
|
47
|
+
hooks, so a provider's `shutdown()` runs alongside any it registered by hand.
|
|
48
|
+
|
|
49
|
+
## The `app` reference
|
|
50
|
+
|
|
51
|
+
Every provider is handed the `Application` at construction and holds it as
|
|
52
|
+
`this.app` (a `protected` field). The `Application` **is** the service container,
|
|
53
|
+
so `this.app` gives you `bind`, `singleton`, `instance`, `make`, and `bound`
|
|
54
|
+
directly, plus the framework accessors `config()`, `router()`, and `view()`:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { ServiceProvider } from "@shaferllc/keel/core";
|
|
58
|
+
import { SearchIndex } from "../Services/SearchIndex.js";
|
|
59
|
+
|
|
60
|
+
export class SearchServiceProvider extends ServiceProvider {
|
|
61
|
+
register(): void {
|
|
62
|
+
this.app.singleton("search", () => new SearchIndex());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
boot(): void {
|
|
66
|
+
const debug = this.app.config().get("app.debug", false);
|
|
67
|
+
this.app.router().get("/health", () => "ok");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You never construct a provider yourself — the `Application` does it for you when
|
|
73
|
+
you register the class. It calls `new Provider(app)`, so `this.app` is always the
|
|
74
|
+
live application instance.
|
|
75
|
+
|
|
76
|
+
## Registering a provider
|
|
77
|
+
|
|
78
|
+
Add your provider class to `bootstrap/providers.ts`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type { ProviderClass } from "@shaferllc/keel/core";
|
|
82
|
+
import { AppServiceProvider } from "../app/Providers/AppServiceProvider.js";
|
|
83
|
+
import { BillingServiceProvider } from "../app/Providers/BillingServiceProvider.js";
|
|
84
|
+
|
|
85
|
+
export const providers: ProviderClass[] = [
|
|
86
|
+
AppServiceProvider,
|
|
87
|
+
BillingServiceProvider,
|
|
88
|
+
];
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Providers boot in array order. Under the hood, `app.boot(providers)` calls
|
|
92
|
+
`app.register(Provider)` for each class — which does `new Provider(app)` and
|
|
93
|
+
stashes the instance — then runs the two phases across the whole set.
|
|
94
|
+
|
|
95
|
+
## Providers are Keel's plugin system
|
|
96
|
+
|
|
97
|
+
A service provider is Keel's answer to a **plugin**: a self-contained slice of
|
|
98
|
+
functionality you register into the app. To make one **reusable**, register it
|
|
99
|
+
with **options** — they arrive as `this.options`, typed via the generic:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
class RateLimitProvider extends ServiceProvider<{ max: number }> {
|
|
103
|
+
boot() {
|
|
104
|
+
this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max }));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
app.register(RateLimitProvider, { max: 100 }); // parameterized, like a plugin
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The same provider class can be registered more than once with different options.
|
|
112
|
+
Without options, `this.options` is an empty object.
|
|
113
|
+
|
|
114
|
+
> Unlike Fastify plugins, Keel providers are **not encapsulated** — bindings,
|
|
115
|
+
> decorators, and routes are registered into the one global container. That's a
|
|
116
|
+
> deliberate simplification: there's a single, predictable scope, and no
|
|
117
|
+
> plugin-boundary rules to reason about. For per-request behavior in the HTTP
|
|
118
|
+
> pipeline (auth, logging, etc.), reach for [middleware](./middleware.md), which
|
|
119
|
+
> *is* scoped to the routes you attach it to.
|
|
120
|
+
|
|
121
|
+
## Generating a provider
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm run keel make:provider Billing
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Writes `app/Providers/BillingServiceProvider.ts`. Remember to add it to
|
|
128
|
+
`bootstrap/providers.ts`.
|
|
129
|
+
|
|
130
|
+
## A realistic example
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { ServiceProvider, Config } from "@shaferllc/keel/core";
|
|
134
|
+
import { StripeClient } from "../Services/StripeClient.js";
|
|
135
|
+
|
|
136
|
+
export class BillingServiceProvider extends ServiceProvider {
|
|
137
|
+
register(): void {
|
|
138
|
+
this.app.singleton(StripeClient, (app) => {
|
|
139
|
+
const key = app.make(Config).get<string>("services.stripe.key");
|
|
140
|
+
return new StripeClient(key);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
boot(): void {
|
|
145
|
+
// e.g. register webhooks, warm a cache, etc.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Now any controller or service can `this.app.make(StripeClient)` and get the same
|
|
151
|
+
configured instance.
|
|
152
|
+
|
|
153
|
+
## Async providers
|
|
154
|
+
|
|
155
|
+
Both phases may return a promise, and the application `await`s each one before
|
|
156
|
+
moving on. Reach for this when a binding needs to open a connection or fetch a
|
|
157
|
+
remote manifest:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { ServiceProvider } from "@shaferllc/keel/core";
|
|
161
|
+
import { SearchClient } from "../Services/SearchClient.js";
|
|
162
|
+
|
|
163
|
+
export class SearchServiceProvider extends ServiceProvider {
|
|
164
|
+
async register(): Promise<void> {
|
|
165
|
+
const client = await SearchClient.connect(process.env.SEARCH_URL!);
|
|
166
|
+
this.app.instance(SearchClient, client);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async boot(): Promise<void> {
|
|
170
|
+
await this.app.make(SearchClient).warm();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Because every `register()` is awaited before any `boot()` runs, an async
|
|
176
|
+
`register()` in one provider still completes before another provider's `boot()`
|
|
177
|
+
tries to resolve what it bound.
|
|
178
|
+
|
|
179
|
+
## Error behavior
|
|
180
|
+
|
|
181
|
+
`register()` and `boot()` run inside `app.boot()`, which awaits each call in
|
|
182
|
+
sequence. If any provider throws (or rejects), `app.boot()` rejects and the
|
|
183
|
+
remaining providers never run — so a bad binding fails the whole boot loudly
|
|
184
|
+
rather than leaving a half-wired container. Booting is also idempotent: once
|
|
185
|
+
`app.boot()` has completed, calling it again returns immediately without
|
|
186
|
+
re-running any provider.
|
|
187
|
+
|
|
188
|
+
## Rules of thumb
|
|
189
|
+
|
|
190
|
+
- **`register()` binds. `boot()` uses.** Resolving a service in `register()` is
|
|
191
|
+
the most common mistake — the thing you need may not be bound yet.
|
|
192
|
+
- **Keep providers focused.** One provider per concern (billing, auth, search)
|
|
193
|
+
reads better than one giant `AppServiceProvider`.
|
|
194
|
+
- **Order matters only for `boot()` side effects**, since all registration
|
|
195
|
+
happens before any booting.
|
|
196
|
+
|
|
197
|
+
## Related
|
|
198
|
+
|
|
199
|
+
Providers wire services into the [container](./container.md); the
|
|
200
|
+
[Application](./architecture.md) kernel constructs and boots them. See
|
|
201
|
+
[configuration](./configuration.md) for what `this.app.config()` reads.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## API reference
|
|
206
|
+
|
|
207
|
+
### `ServiceProvider`
|
|
208
|
+
|
|
209
|
+
The abstract base class every provider extends. You never instantiate it
|
|
210
|
+
directly — you subclass it and register the subclass (via `bootstrap/providers.ts`
|
|
211
|
+
or `app.register()`), and the `Application` constructs it for you. Override
|
|
212
|
+
`register()` and/or `boot()`; both are no-ops by default.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { ServiceProvider } from "@shaferllc/keel/core";
|
|
216
|
+
|
|
217
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
218
|
+
register(): void {
|
|
219
|
+
this.app.bind("clock", () => new Date().toISOString());
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Notes:** `abstract`, so it can't be `new`ed on its own. A subclass needn't
|
|
225
|
+
override both methods — leave one off to inherit the empty default.
|
|
226
|
+
|
|
227
|
+
#### `constructor(app, options?)`
|
|
228
|
+
|
|
229
|
+
`constructor(app: Application, options?: O)` (on `ServiceProvider<O>`)
|
|
230
|
+
|
|
231
|
+
Receives the live `Application` (stored as `protected this.app`) and the options
|
|
232
|
+
passed to `register` (stored as `protected this.options`, `{}` if none). The
|
|
233
|
+
`Application` invokes this for you; you don't call it.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
class RateLimitProvider extends ServiceProvider<{ max: number }> {
|
|
237
|
+
boot() {
|
|
238
|
+
this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
app.register(RateLimitProvider, { max: 100 });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Notes:** `app` and `options` are `protected` — reachable from subclass methods,
|
|
245
|
+
not from outside. Type the options via the class generic `ServiceProvider<O>`.
|
|
246
|
+
|
|
247
|
+
#### `app.register(Provider, options?)`
|
|
248
|
+
|
|
249
|
+
`register(Provider: ProviderClass, options?: unknown): this`
|
|
250
|
+
|
|
251
|
+
Registers a provider, optionally with options handed to its constructor. Chainable.
|
|
252
|
+
`app.boot([Providers])` registers each without options.
|
|
253
|
+
|
|
254
|
+
#### `register()`
|
|
255
|
+
|
|
256
|
+
`register(): void | Promise<void>`
|
|
257
|
+
|
|
258
|
+
Phase-one hook: bind services into the container. Called for every provider
|
|
259
|
+
before any `boot()` runs. Default implementation is empty.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
register(): void {
|
|
263
|
+
this.app.singleton(StripeClient, (app) =>
|
|
264
|
+
new StripeClient(app.make(Config).get<string>("services.stripe.key")),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Notes:** do **not** resolve other services here — another provider may not have
|
|
270
|
+
bound them yet. May be `async`; the application awaits it. Throwing rejects
|
|
271
|
+
`app.boot()`.
|
|
272
|
+
|
|
273
|
+
#### `boot()`
|
|
274
|
+
|
|
275
|
+
`boot(): void | Promise<void>`
|
|
276
|
+
|
|
277
|
+
Phase-two hook: runs after **every** provider has registered, so it's safe to
|
|
278
|
+
resolve services and wire them together. Default implementation is empty.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
async boot(): Promise<void> {
|
|
282
|
+
await this.app.make(SearchClient).warm();
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Notes:** providers boot in registration (array) order — the only place order
|
|
287
|
+
matters, since all `register()` calls finish first. May be `async`; the
|
|
288
|
+
application awaits it. Throwing rejects `app.boot()`.
|
|
289
|
+
|
|
290
|
+
#### `ready()`
|
|
291
|
+
|
|
292
|
+
`ready(): void | Promise<void>`
|
|
293
|
+
|
|
294
|
+
Phase-three hook: runs after **every** provider has booted and the app is fully
|
|
295
|
+
up (after the app's own `onReady` hooks). For work that needs a live app —
|
|
296
|
+
warming a cache, attaching to the running server. Default implementation is
|
|
297
|
+
empty.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
async ready(): Promise<void> {
|
|
301
|
+
await this.app.make(Cache).warm(["home", "pricing"]);
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Notes:** runs in registration order, once, at the end of `app.boot()`. Optional
|
|
306
|
+
— omit it and nothing runs. May be `async`; the application awaits it.
|
|
307
|
+
|
|
308
|
+
#### `shutdown()`
|
|
309
|
+
|
|
310
|
+
`shutdown(): void | Promise<void>`
|
|
311
|
+
|
|
312
|
+
Cleanup hook: runs on `app.terminate()` (which `keel serve` wires to SIGINT and
|
|
313
|
+
SIGTERM) in **reverse** registration order. Close database/Redis connections,
|
|
314
|
+
flush logs, cancel timers. Default implementation is empty.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
async shutdown(): Promise<void> {
|
|
318
|
+
await this.app.make(Redis).quit();
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Notes:** LIFO — the last provider registered shuts down first, so a provider
|
|
323
|
+
tears down before the ones it depends on. It joins the app's `onShutdown` hooks,
|
|
324
|
+
so a hook a provider registered by hand and its `shutdown()` both run. A throw
|
|
325
|
+
doesn't stop the others; the first error is re-thrown after all have run.
|
|
326
|
+
|
|
327
|
+
#### `app` (protected property)
|
|
328
|
+
|
|
329
|
+
`protected app: Application`
|
|
330
|
+
|
|
331
|
+
The application/container this provider configures. Use it in `register()` to
|
|
332
|
+
bind and in `boot()` to resolve.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
boot(): void {
|
|
336
|
+
const level = this.app.config().get("logger.level", "info");
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Notes:** it's the same `Application` instance across every provider, exposing
|
|
341
|
+
the container methods (`bind`, `singleton`, `instance`, `make`, `bound`) plus
|
|
342
|
+
`config()`, `router()`, and `view()`. Being `protected`, it's only visible to
|
|
343
|
+
subclass code.
|
|
344
|
+
|
|
345
|
+
### Interfaces & types
|
|
346
|
+
|
|
347
|
+
#### `ProviderClass`
|
|
348
|
+
|
|
349
|
+
`type ProviderClass = new (app: Application) => ServiceProvider`
|
|
350
|
+
|
|
351
|
+
The constructor type of a provider — a class (not an instance) that takes an
|
|
352
|
+
`Application` and yields a `ServiceProvider`. Use it to type the array you export
|
|
353
|
+
from `bootstrap/providers.ts` and anywhere you pass provider classes around.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import type { ProviderClass } from "@shaferllc/keel/core";
|
|
357
|
+
import { AppServiceProvider } from "../app/Providers/AppServiceProvider.js";
|
|
358
|
+
|
|
359
|
+
export const providers: ProviderClass[] = [AppServiceProvider];
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Notes:** it references the class itself, so entries are the class name with no
|
|
363
|
+
`new` and no parentheses. `app.register()` and `app.boot()` both accept these.
|