@shaferllc/keel 0.83.1 → 0.83.2
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/docs/ai-manifest.json +13 -13
- package/docs/billing.md +40 -0
- package/docs/changelog.md +18 -0
- package/docs/cors.md +28 -0
- package/docs/examples/billing.ts +128 -0
- package/docs/examples/cors.ts +29 -0
- package/docs/examples/hono.ts +29 -0
- package/docs/examples/openapi.ts +35 -0
- package/docs/examples/orm.ts +30 -0
- package/docs/examples/packages.ts +26 -0
- package/docs/examples/query-builder.ts +118 -0
- package/docs/examples/security.ts +31 -0
- package/docs/examples/social-auth.ts +76 -0
- package/docs/examples/starter-kits.ts +14 -0
- package/docs/examples/watch.ts +10 -0
- package/docs/orm.md +39 -0
- package/docs/starter-kits.md +14 -0
- package/docs/watch.md +1 -1
- package/llms-full.txt +121 -1
- package/llms.txt +12 -1
- package/package.json +1 -1
package/docs/ai-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaferllc/keel",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.2",
|
|
4
4
|
"description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
|
|
5
5
|
"repo": "https://github.com/shaferllc/keel",
|
|
6
6
|
"generated": "run `npm run build:ai` to regenerate",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"title": "Billing",
|
|
53
53
|
"summary": "Keel Billing is a subscription-billing layer for charging customers, managing subscriptions, and reconciling gateway state through webhooks.",
|
|
54
54
|
"path": "docs/billing.md",
|
|
55
|
-
"example":
|
|
55
|
+
"example": "docs/examples/billing.ts"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"slug": "broadcasting",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"title": "CORS",
|
|
116
116
|
"summary": "Cross-Origin Resource Sharing lets browsers on other origins call your API.",
|
|
117
117
|
"path": "docs/cors.md",
|
|
118
|
-
"example":
|
|
118
|
+
"example": "docs/examples/cors.ts"
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
"slug": "database",
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
"title": "Built on Hono",
|
|
193
193
|
"summary": "Keel's HTTP layer is Hono — an ultrafast, web-standard router that runs on Node, Cloudflare Workers, Deno, Bun, and more.",
|
|
194
194
|
"path": "docs/hono.md",
|
|
195
|
-
"example":
|
|
195
|
+
"example": "docs/examples/hono.ts"
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
"slug": "hooks",
|
|
@@ -269,21 +269,21 @@
|
|
|
269
269
|
"title": "OpenAPI",
|
|
270
270
|
"summary": "Keel OpenAPI generates an OpenAPI 3 spec from your routes and serves Swagger UI to explore it.",
|
|
271
271
|
"path": "docs/openapi.md",
|
|
272
|
-
"example":
|
|
272
|
+
"example": "docs/examples/openapi.ts"
|
|
273
273
|
},
|
|
274
274
|
{
|
|
275
275
|
"slug": "orm",
|
|
276
276
|
"title": "ORM",
|
|
277
277
|
"summary": "Keel's ORM is a compact active record over the query builder: a model is a class pointed at a table, and its rows come back as typed objects with methods.",
|
|
278
278
|
"path": "docs/orm.md",
|
|
279
|
-
"example":
|
|
279
|
+
"example": "docs/examples/orm.ts"
|
|
280
280
|
},
|
|
281
281
|
{
|
|
282
282
|
"slug": "packages",
|
|
283
283
|
"title": "Packages",
|
|
284
284
|
"summary": "A package is a redistributable slice of a Keel app — routes, a UI, config, migrations, console commands — that installs with a single app.register(...).",
|
|
285
285
|
"path": "docs/packages.md",
|
|
286
|
-
"example":
|
|
286
|
+
"example": "docs/examples/packages.ts"
|
|
287
287
|
},
|
|
288
288
|
{
|
|
289
289
|
"slug": "pages",
|
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
"title": "Query Builder",
|
|
305
305
|
"summary": "Keel's driver-agnostic query builder — build and run SQL by chaining methods off db(table).",
|
|
306
306
|
"path": "docs/query-builder.md",
|
|
307
|
-
"example":
|
|
307
|
+
"example": "docs/examples/query-builder.ts"
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"slug": "queues",
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
"title": "Securing SSR apps",
|
|
354
354
|
"summary": "Two middlewares harden server-rendered apps: securityHeaders() sets the defensive HTTP headers browsers act on, and csrf() blocks cross-site form submissions.",
|
|
355
355
|
"path": "docs/security.md",
|
|
356
|
-
"example":
|
|
356
|
+
"example": "docs/examples/security.ts"
|
|
357
357
|
},
|
|
358
358
|
{
|
|
359
359
|
"slug": "sessions",
|
|
@@ -367,14 +367,14 @@
|
|
|
367
367
|
"title": "Social authentication",
|
|
368
368
|
"summary": "\"Sign in with GitHub / Google / Discord\" — OAuth 2.0, without an SDK.",
|
|
369
369
|
"path": "docs/social-auth.md",
|
|
370
|
-
"example":
|
|
370
|
+
"example": "docs/examples/social-auth.ts"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"slug": "starter-kits",
|
|
374
374
|
"title": "Starter kits",
|
|
375
375
|
"summary": "",
|
|
376
376
|
"path": "docs/starter-kits.md",
|
|
377
|
-
"example":
|
|
377
|
+
"example": "docs/examples/starter-kits.ts"
|
|
378
378
|
},
|
|
379
379
|
{
|
|
380
380
|
"slug": "static-files",
|
|
@@ -456,9 +456,9 @@
|
|
|
456
456
|
{
|
|
457
457
|
"slug": "watch",
|
|
458
458
|
"title": "Watch",
|
|
459
|
-
"summary": "Keel Watch is a debug dashboard
|
|
459
|
+
"summary": "Keel Watch is a debug dashboard for Keel apps.",
|
|
460
460
|
"path": "docs/watch.md",
|
|
461
|
-
"example":
|
|
461
|
+
"example": "docs/examples/watch.ts"
|
|
462
462
|
}
|
|
463
463
|
],
|
|
464
464
|
"api": [
|
package/docs/billing.md
CHANGED
|
@@ -205,6 +205,46 @@ resolveBillableUsing(async (customerId) => {
|
|
|
205
205
|
});
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
## A complete flow
|
|
209
|
+
|
|
210
|
+
From "user signs up" to "they're subscribed", with the fake gateway for tests:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { Model } from "@shaferllc/keel/core";
|
|
214
|
+
import {
|
|
215
|
+
Billable,
|
|
216
|
+
BillingManager,
|
|
217
|
+
FakeGateway,
|
|
218
|
+
setBilling,
|
|
219
|
+
} from "@shaferllc/keel/billing";
|
|
220
|
+
|
|
221
|
+
class User extends Billable(Model) {
|
|
222
|
+
static table = "users";
|
|
223
|
+
declare email: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// In a test bootstrap:
|
|
227
|
+
const fake = new FakeGateway();
|
|
228
|
+
const manager = new BillingManager({
|
|
229
|
+
default: "fake",
|
|
230
|
+
currency: "usd",
|
|
231
|
+
billableModel: "User",
|
|
232
|
+
webhook: { path: "billing/webhook" },
|
|
233
|
+
gateways: { stripe: { key: "", webhookSecret: "" }, paddle: { key: "", webhookSecret: "" }, fake: {} },
|
|
234
|
+
});
|
|
235
|
+
manager.register("fake", () => fake);
|
|
236
|
+
setBilling(manager);
|
|
237
|
+
|
|
238
|
+
const user = await User.create({ email: "ada@example.com" });
|
|
239
|
+
await user.newSubscription("default", "price_pro").trialDays(14).create();
|
|
240
|
+
|
|
241
|
+
await user.subscribed(); // true
|
|
242
|
+
fake.calls.some((c) => c.method === "createSubscription"); // true
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
In production you skip the fake manager — `BillingServiceProvider` wires the
|
|
246
|
+
real gateway from `config/billing.ts` and `.env`.
|
|
247
|
+
|
|
208
248
|
## Gateway differences
|
|
209
249
|
|
|
210
250
|
| Concern | Stripe | Paddle |
|
package/docs/changelog.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to Keel are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
5
|
adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.83.2] — 2026-07-12
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **More runnable doc examples.** Billing, ORM, query builder, CORS, security,
|
|
12
|
+
social auth, OpenAPI, Watch, packages, Hono, and starter kits now have
|
|
13
|
+
typechecked harnesses under `docs/examples/` — the same ones
|
|
14
|
+
`npm run typecheck:docs` compiles against the published surface.
|
|
15
|
+
- **Worked examples in the guides** — a complete billing subscribe flow (with
|
|
16
|
+
`FakeGateway`), an ORM CRUD+eager-load walkthrough, a production CORS recipe,
|
|
17
|
+
and a clearer starter-kit picker.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Watch no longer describes itself by another product's name.
|
|
22
|
+
|
|
23
|
+
[0.83.2]: https://github.com/shaferllc/keel/releases/tag/v0.83.2
|
|
24
|
+
|
|
7
25
|
## [0.83.0] — 2026-07-12
|
|
8
26
|
|
|
9
27
|
### Added
|
package/docs/cors.md
CHANGED
|
@@ -21,6 +21,34 @@ router.group(() => { /* … */ }).use(cors({ origin: ["https://app.example.com"]
|
|
|
21
21
|
With no options, `cors()` reflects the caller's origin — convenient in
|
|
22
22
|
development. **Lock it down in production** with an explicit allowlist.
|
|
23
23
|
|
|
24
|
+
## A production API group
|
|
25
|
+
|
|
26
|
+
Typical setup for a JSON API served from `api.example.com` and called from a
|
|
27
|
+
SPA on `app.example.com`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// app/Http/Kernel.ts
|
|
31
|
+
import { cors } from "@shaferllc/keel/core";
|
|
32
|
+
|
|
33
|
+
this.use(
|
|
34
|
+
cors({
|
|
35
|
+
origin: ["https://app.example.com"],
|
|
36
|
+
credentials: true, // cookies / Authorization
|
|
37
|
+
exposeHeaders: ["X-Request-Id"],
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
During local development, allow any localhost port with a predicate:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
cors({
|
|
46
|
+
origin: (origin) =>
|
|
47
|
+
origin.startsWith("http://localhost:") || origin === "https://app.example.com",
|
|
48
|
+
credentials: true,
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
24
52
|
## Options
|
|
25
53
|
|
|
26
54
|
```ts
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Type-check harness for docs/billing.md. Compile-only — never executed.
|
|
2
|
+
import { Model, listen } from "@shaferllc/keel/core";
|
|
3
|
+
import {
|
|
4
|
+
Billable,
|
|
5
|
+
BillingServiceProvider,
|
|
6
|
+
BillingManager,
|
|
7
|
+
FakeGateway,
|
|
8
|
+
setBilling,
|
|
9
|
+
resolveBillableUsing,
|
|
10
|
+
type CheckoutSession,
|
|
11
|
+
type GatewayCharge,
|
|
12
|
+
} from "@shaferllc/keel/billing";
|
|
13
|
+
|
|
14
|
+
export class User extends Billable(Model) {
|
|
15
|
+
static override table = "users";
|
|
16
|
+
declare id: number;
|
|
17
|
+
declare email: string;
|
|
18
|
+
declare name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare const user: User;
|
|
22
|
+
declare const paymentMethodId: string;
|
|
23
|
+
declare const successUrl: string;
|
|
24
|
+
declare const cancelUrl: string;
|
|
25
|
+
|
|
26
|
+
export const providers = [BillingServiceProvider];
|
|
27
|
+
|
|
28
|
+
/* -------------------------------- customers ------------------------------- */
|
|
29
|
+
|
|
30
|
+
export async function customers() {
|
|
31
|
+
await user.createAsCustomer();
|
|
32
|
+
user.hasBillingId();
|
|
33
|
+
await user.getCustomerId();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ------------------------------ subscriptions ----------------------------- */
|
|
37
|
+
|
|
38
|
+
export async function subscribe() {
|
|
39
|
+
await user
|
|
40
|
+
.newSubscription("default", "price_pro")
|
|
41
|
+
.trialDays(14)
|
|
42
|
+
.quantity(3)
|
|
43
|
+
.create(paymentMethodId);
|
|
44
|
+
|
|
45
|
+
const session: CheckoutSession = await user
|
|
46
|
+
.newSubscription("default", "price_pro")
|
|
47
|
+
.checkout({ successUrl, cancelUrl });
|
|
48
|
+
return session;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function status() {
|
|
52
|
+
await user.subscribed();
|
|
53
|
+
await user.subscribedToPrice("price_pro");
|
|
54
|
+
await user.onTrial();
|
|
55
|
+
const sub = await user.subscription();
|
|
56
|
+
if (!sub) return;
|
|
57
|
+
|
|
58
|
+
sub.active();
|
|
59
|
+
sub.onTrial();
|
|
60
|
+
sub.recurring();
|
|
61
|
+
sub.canceled();
|
|
62
|
+
sub.onGracePeriod();
|
|
63
|
+
sub.ended();
|
|
64
|
+
sub.paused();
|
|
65
|
+
sub.valid();
|
|
66
|
+
sub.hasIncompletePayment();
|
|
67
|
+
|
|
68
|
+
await sub.swap("price_enterprise");
|
|
69
|
+
await sub.updateQuantity(10);
|
|
70
|
+
await sub.incrementQuantity(2);
|
|
71
|
+
await sub.decrementQuantity();
|
|
72
|
+
await sub.cancel();
|
|
73
|
+
await sub.resume();
|
|
74
|
+
await sub.cancelNow();
|
|
75
|
+
await sub.endTrial();
|
|
76
|
+
await sub.extendTrial(new Date("2026-01-01"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* -------------------------------- charges --------------------------------- */
|
|
80
|
+
|
|
81
|
+
export async function charges() {
|
|
82
|
+
const charge: GatewayCharge = await user.charge(2000, {
|
|
83
|
+
paymentMethod: "pm_1",
|
|
84
|
+
description: "Credits",
|
|
85
|
+
});
|
|
86
|
+
await user.refund(charge.id);
|
|
87
|
+
await user.refund(charge.id, 500);
|
|
88
|
+
await user.checkout("price_onetime", { successUrl, cancelUrl });
|
|
89
|
+
await user.createSetupIntent();
|
|
90
|
+
await user.paymentMethods();
|
|
91
|
+
await user.invoices();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* -------------------------------- webhooks -------------------------------- */
|
|
95
|
+
|
|
96
|
+
export function webhooks() {
|
|
97
|
+
listen("billing.subscription.updated", (e) => {
|
|
98
|
+
void e.gateway;
|
|
99
|
+
void e.subscriptionId;
|
|
100
|
+
});
|
|
101
|
+
listen("billing.webhook.received", (e) => {
|
|
102
|
+
void e.gateway;
|
|
103
|
+
void e.type;
|
|
104
|
+
});
|
|
105
|
+
resolveBillableUsing(async (customerId, _gateway) => {
|
|
106
|
+
return { id: user.id, type: "User" };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* --------------------------------- testing -------------------------------- */
|
|
111
|
+
|
|
112
|
+
export function fakeGateway() {
|
|
113
|
+
const fake = new FakeGateway();
|
|
114
|
+
const manager = new BillingManager({
|
|
115
|
+
default: "fake",
|
|
116
|
+
currency: "usd",
|
|
117
|
+
billableModel: "User",
|
|
118
|
+
webhook: { path: "billing/webhook" },
|
|
119
|
+
gateways: {
|
|
120
|
+
stripe: { key: "", webhookSecret: "" },
|
|
121
|
+
paddle: { key: "", webhookSecret: "" },
|
|
122
|
+
fake: {},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
manager.register("fake", () => fake);
|
|
126
|
+
setBilling(manager);
|
|
127
|
+
return fake;
|
|
128
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Type-check harness for docs/cors.md. Compile-only — never executed.
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import { cors, type CorsOptions } from "@shaferllc/keel/core";
|
|
4
|
+
|
|
5
|
+
export function kernel(): MiddlewareHandler {
|
|
6
|
+
return cors();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function apiGroup(): MiddlewareHandler {
|
|
10
|
+
return cors({ origin: ["https://app.example.com"] });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function production(): MiddlewareHandler {
|
|
14
|
+
const options: CorsOptions = {
|
|
15
|
+
origin: ["https://app.example.com"],
|
|
16
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
17
|
+
headers: true,
|
|
18
|
+
exposeHeaders: ["X-Request-Id"],
|
|
19
|
+
credentials: true,
|
|
20
|
+
maxAge: 86400,
|
|
21
|
+
};
|
|
22
|
+
return cors(options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function dynamicOrigin(): MiddlewareHandler {
|
|
26
|
+
return cors({
|
|
27
|
+
origin: (origin) => origin.endsWith(".example.com") || origin.startsWith("http://localhost"),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Type-check harness for docs/hono.md. Compile-only — never executed.
|
|
2
|
+
import type { Ctx } from "@shaferllc/keel/core";
|
|
3
|
+
import { Router, json } from "@shaferllc/keel/core";
|
|
4
|
+
|
|
5
|
+
export function withContext(router: Router) {
|
|
6
|
+
router.get("/users/:id", async (c: Ctx) => {
|
|
7
|
+
const id = c.req.param("id");
|
|
8
|
+
const q = c.req.query("q");
|
|
9
|
+
const auth = c.req.header("authorization");
|
|
10
|
+
void q;
|
|
11
|
+
void auth;
|
|
12
|
+
return c.json({ id });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
router.get("/ping", (c) => c.text("pong"));
|
|
16
|
+
router.get("/go", (c) => c.redirect("/"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ambientHelpers() {
|
|
20
|
+
return json({ ok: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function keelContext(c: Ctx) {
|
|
24
|
+
return {
|
|
25
|
+
app: c.get("app"),
|
|
26
|
+
route: c.get("route"),
|
|
27
|
+
subdomains: c.get("subdomains"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Type-check harness for docs/openapi.md. Compile-only — never executed.
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Application, Router, validateRequest, type Ctx } from "@shaferllc/keel/core";
|
|
4
|
+
import { OpenApiServiceProvider, apiDoc, OpenApi } from "@shaferllc/keel/openapi";
|
|
5
|
+
|
|
6
|
+
const NewUser = z.object({ email: z.string().email(), age: z.number().min(18) });
|
|
7
|
+
const UserShape = z.object({ id: z.number(), email: z.string().email() });
|
|
8
|
+
|
|
9
|
+
class Users {
|
|
10
|
+
store(_c: Ctx) {
|
|
11
|
+
return { id: 1, email: "a@b.com" };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function install() {
|
|
16
|
+
const app = new Application();
|
|
17
|
+
app.register(OpenApiServiceProvider);
|
|
18
|
+
OpenApi.auth((c) => c.req.header("x-docs-key") === "secret");
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function document(router: Router) {
|
|
23
|
+
router
|
|
24
|
+
.post("/users", [Users, "store"])
|
|
25
|
+
.name("users.store")
|
|
26
|
+
.config(
|
|
27
|
+
apiDoc({
|
|
28
|
+
summary: "Create a user",
|
|
29
|
+
tags: ["users"],
|
|
30
|
+
request: { body: NewUser },
|
|
31
|
+
responses: { 201: { description: "The created user", schema: UserShape } },
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
.middleware([validateRequest({ body: NewUser })]);
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Type-check harness for docs/orm.md. Compile-only — never executed.
|
|
2
|
+
import { Model } from "@shaferllc/keel/core";
|
|
3
|
+
|
|
4
|
+
class Post extends Model {
|
|
5
|
+
static override table = "posts";
|
|
6
|
+
declare id: number;
|
|
7
|
+
declare title: string;
|
|
8
|
+
declare user_id: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class User extends Model {
|
|
12
|
+
static override table = "users";
|
|
13
|
+
declare id: number;
|
|
14
|
+
declare email: string;
|
|
15
|
+
|
|
16
|
+
posts() {
|
|
17
|
+
return this.hasMany(Post);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function map() {
|
|
22
|
+
const user = await User.find(1);
|
|
23
|
+
if (!user) return;
|
|
24
|
+
await user.posts();
|
|
25
|
+
await User.with("posts").where("email", user.email).first();
|
|
26
|
+
await User.create({ email: "ada@example.com" });
|
|
27
|
+
user.email = "grace@example.com";
|
|
28
|
+
await user.save();
|
|
29
|
+
await user.delete();
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Type-check harness for docs/packages.md. Compile-only — never executed.
|
|
2
|
+
import { PackageProvider, type Router, type Migration, type PackageCommand } from "@shaferllc/keel/core";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
declare const createInvoicesTable: Migration;
|
|
9
|
+
declare const syncInvoicesCommand: PackageCommand;
|
|
10
|
+
declare function registerBillingRoutes(r: Router): void;
|
|
11
|
+
|
|
12
|
+
export class BillingServiceProvider extends PackageProvider {
|
|
13
|
+
readonly name = "billing";
|
|
14
|
+
|
|
15
|
+
register(): void {
|
|
16
|
+
this.mergeConfig("billing", { enabled: true, path: "billing" });
|
|
17
|
+
this.migrations([createInvoicesTable]);
|
|
18
|
+
this.publishes({ [join(here, "config.stub")]: "config/billing.ts" }, "billing-config");
|
|
19
|
+
this.commands([syncInvoicesCommand]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
boot(): void {
|
|
23
|
+
this.assets("billing/assets", join(here, "ui/dist"), { maxAge: 3600 });
|
|
24
|
+
this.routes((r: Router) => registerBillingRoutes(r), { prefix: "billing", as: "billing" });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Type-check harness for docs/query-builder.md. Compile-only — never executed.
|
|
2
|
+
import { db, type Row } from "@shaferllc/keel/core";
|
|
3
|
+
|
|
4
|
+
declare const email: string;
|
|
5
|
+
declare const name: string;
|
|
6
|
+
declare const id: number;
|
|
7
|
+
declare const now: number;
|
|
8
|
+
declare const search: string | undefined;
|
|
9
|
+
declare const includeArchived: boolean;
|
|
10
|
+
declare const key: string;
|
|
11
|
+
declare const value: string;
|
|
12
|
+
|
|
13
|
+
export async function retrieving() {
|
|
14
|
+
await db("users").get();
|
|
15
|
+
await db("users").where("id", 1).first();
|
|
16
|
+
await db("users").where("id", 1).firstOrFail();
|
|
17
|
+
await db("users").find(1);
|
|
18
|
+
await db("users").where("email", email).sole();
|
|
19
|
+
await db("users").where("id", 1).value("email");
|
|
20
|
+
await db("posts").pluck("title");
|
|
21
|
+
await db("tags").orderBy("name").implode("name", ", ");
|
|
22
|
+
|
|
23
|
+
await db("users")
|
|
24
|
+
.orderBy("id")
|
|
25
|
+
.chunk(500, async (rows) => {
|
|
26
|
+
for (const _row of rows) {
|
|
27
|
+
/* process */
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function aggregates() {
|
|
33
|
+
await db("orders").count();
|
|
34
|
+
await db("orders").where("paid", true).sum("total");
|
|
35
|
+
await db("orders").avg("total");
|
|
36
|
+
await db("orders").min("total");
|
|
37
|
+
await db("orders").max("total");
|
|
38
|
+
await db("users").where("email", email).exists();
|
|
39
|
+
await db("users").where("banned", true).doesntExist();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function selectsAndWheres() {
|
|
43
|
+
db("users").select("id", "email");
|
|
44
|
+
db("users").select("id").addSelect("email");
|
|
45
|
+
db("orders").selectRaw("SUM(total) AS revenue");
|
|
46
|
+
db("users").distinct().select("country");
|
|
47
|
+
|
|
48
|
+
db("users").where("votes", 100);
|
|
49
|
+
db("users").where("votes", ">=", 100);
|
|
50
|
+
db("users").where("name", "like", "T%");
|
|
51
|
+
db("users").where("votes", 100).orWhere("name", "John");
|
|
52
|
+
db("users").whereNot("status", "cancelled");
|
|
53
|
+
db("users").whereIn("id", [1, 2, 3]).whereNotIn("id", [4]);
|
|
54
|
+
db("users").whereNull("deleted_at").whereNotNull("email_verified_at");
|
|
55
|
+
db("products").whereBetween("price", [10, 100]).whereNotBetween("stock", [0, 5]);
|
|
56
|
+
db("posts").whereLike("title", "%keel%");
|
|
57
|
+
db("events").whereColumn("updated_at", ">", "created_at");
|
|
58
|
+
db("users").whereRaw("score >= ? AND score <= ?", [10, 90]);
|
|
59
|
+
|
|
60
|
+
await db("users")
|
|
61
|
+
.where("active", true)
|
|
62
|
+
.where((q) => q.where("role", "admin").orWhere("role", "owner"))
|
|
63
|
+
.get();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function orderingJoinsWhen() {
|
|
67
|
+
db("users").orderBy("name").orderByDesc("created_at");
|
|
68
|
+
db("posts").latest();
|
|
69
|
+
db("posts").oldest();
|
|
70
|
+
db("posts").orderByRaw("LENGTH(title) DESC");
|
|
71
|
+
db("users").inRandomOrder();
|
|
72
|
+
db("users").reorder("name");
|
|
73
|
+
db("users").limit(10).offset(20);
|
|
74
|
+
db("users").take(10).skip(20);
|
|
75
|
+
db("users").forPage(3, 15);
|
|
76
|
+
|
|
77
|
+
await db("orders")
|
|
78
|
+
.select("user_id")
|
|
79
|
+
.selectRaw("SUM(total) AS spent")
|
|
80
|
+
.groupBy("user_id")
|
|
81
|
+
.having("spent", ">", 1000)
|
|
82
|
+
.get();
|
|
83
|
+
|
|
84
|
+
await db("posts")
|
|
85
|
+
.join("users", "posts.user_id", "users.id")
|
|
86
|
+
.leftJoin("images", "images.post_id", "posts.id")
|
|
87
|
+
.select("posts.title", "users.name")
|
|
88
|
+
.get();
|
|
89
|
+
|
|
90
|
+
await db("users")
|
|
91
|
+
.when(search, (q, term) => q.whereLike("name", `%${term}%`))
|
|
92
|
+
.unless(includeArchived, (q) => q.whereNull("archived_at"))
|
|
93
|
+
.get();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function writes() {
|
|
97
|
+
await db("users").insert({ email, name });
|
|
98
|
+
await db("users").insertGetId({ email, name });
|
|
99
|
+
await db("logs").insertOrIgnore({ key, value });
|
|
100
|
+
await db("users").upsert([{ id: 1, name: "Ada" }], ["id"], ["name"]);
|
|
101
|
+
|
|
102
|
+
await db("users").where("id", id).update({ name: "Grace" });
|
|
103
|
+
await db("users").updateOrInsert({ email }, { name });
|
|
104
|
+
await db("posts").where("id", id).increment("views");
|
|
105
|
+
await db("posts").where("id", id).decrement("stock", 3, { updated_at: now });
|
|
106
|
+
await db("counters").incrementEach({ hits: 1, misses: 2 });
|
|
107
|
+
|
|
108
|
+
await db("sessions").where("expires_at", "<", now).delete();
|
|
109
|
+
await db("cache").truncate();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function typed() {
|
|
113
|
+
type User = { id: number; email: string };
|
|
114
|
+
const row: User | null = await db<User>("users").where("id", 1).first();
|
|
115
|
+
const rows: User[] = await db<User>("users").get();
|
|
116
|
+
const raw: Row[] = await db("users").get();
|
|
117
|
+
return { row, rows, raw };
|
|
118
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Type-check harness for docs/security.md. Compile-only — never executed.
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import {
|
|
4
|
+
securityHeaders,
|
|
5
|
+
csrf,
|
|
6
|
+
csrfField,
|
|
7
|
+
csrfToken,
|
|
8
|
+
sessionMiddleware,
|
|
9
|
+
} from "@shaferllc/keel/core";
|
|
10
|
+
|
|
11
|
+
export function headers(): MiddlewareHandler[] {
|
|
12
|
+
return [
|
|
13
|
+
securityHeaders(),
|
|
14
|
+
securityHeaders({
|
|
15
|
+
csp: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"] },
|
|
16
|
+
hsts: { maxAge: 15552000, includeSubDomains: true },
|
|
17
|
+
frameGuard: "DENY",
|
|
18
|
+
}),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function csrfStack(): MiddlewareHandler[] {
|
|
23
|
+
return [sessionMiddleware(), csrf(), csrf({ except: ["/billing/webhook/*"] })];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function forms() {
|
|
27
|
+
return {
|
|
28
|
+
field: csrfField(),
|
|
29
|
+
token: csrfToken(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Type-check harness for docs/social-auth.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
social,
|
|
4
|
+
session,
|
|
5
|
+
redirect,
|
|
6
|
+
request,
|
|
7
|
+
auth,
|
|
8
|
+
ForbiddenException,
|
|
9
|
+
Model,
|
|
10
|
+
config,
|
|
11
|
+
} from "@shaferllc/keel/core";
|
|
12
|
+
|
|
13
|
+
class User extends Model {
|
|
14
|
+
static override table = "users";
|
|
15
|
+
declare id: number;
|
|
16
|
+
declare email: string | null;
|
|
17
|
+
declare name: string | null;
|
|
18
|
+
declare github_id: string;
|
|
19
|
+
declare twitter_id: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const github = social.github({
|
|
23
|
+
clientId: config("services.github.id") as string,
|
|
24
|
+
clientSecret: config("services.github.secret") as string,
|
|
25
|
+
redirectUri: "https://app.example.com/auth/github/callback",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const twitter = social.twitter({
|
|
29
|
+
clientId: config("services.twitter.key") as string,
|
|
30
|
+
clientSecret: config("services.twitter.secret") as string,
|
|
31
|
+
redirectUri: "https://app.example.com/auth/twitter/callback",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export function redirectToGithub() {
|
|
35
|
+
const state = social.state();
|
|
36
|
+
session().put("oauth_state", state);
|
|
37
|
+
return redirect(github.redirect({ state }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function githubCallback() {
|
|
41
|
+
const state = String(request.query("state") ?? "");
|
|
42
|
+
if (state !== session().pull("oauth_state")) {
|
|
43
|
+
throw new ForbiddenException("Invalid OAuth state");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const code = String(request.query("code") ?? "");
|
|
47
|
+
const gh = await github.user(code);
|
|
48
|
+
const existing = await User.query().where("github_id", gh.id).first();
|
|
49
|
+
const user =
|
|
50
|
+
existing ??
|
|
51
|
+
(await User.create({ github_id: gh.id, email: gh.email, name: gh.name }));
|
|
52
|
+
|
|
53
|
+
auth().login(user.id as number);
|
|
54
|
+
return redirect("/dashboard");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function twitterFlow() {
|
|
58
|
+
const requestToken = await twitter.requestToken();
|
|
59
|
+
session().put("twitter_secret", requestToken.tokenSecret);
|
|
60
|
+
const toProvider = redirect(twitter.redirect(requestToken));
|
|
61
|
+
|
|
62
|
+
const tw = await twitter.user(
|
|
63
|
+
String(request.query("oauth_token") ?? ""),
|
|
64
|
+
String(request.query("oauth_verifier") ?? ""),
|
|
65
|
+
String(session().pull("twitter_secret") ?? ""),
|
|
66
|
+
);
|
|
67
|
+
const user = await User.firstOrCreate({ twitter_id: tw.id }, { name: tw.name });
|
|
68
|
+
auth().login(user.id as number);
|
|
69
|
+
return { toProvider, user };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function tokenReuse() {
|
|
73
|
+
const code = String(request.query("code") ?? "");
|
|
74
|
+
const token = await github.exchangeCode(code);
|
|
75
|
+
return github.userFromToken(token);
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Type-check harness for docs/starter-kits.md. Compile-only — never executed.
|
|
2
|
+
import { TenantModel } from "@shaferllc/keel/teams";
|
|
3
|
+
|
|
4
|
+
class Project extends TenantModel {
|
|
5
|
+
static override table = "projects";
|
|
6
|
+
declare id: number;
|
|
7
|
+
declare name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function tenantScoped() {
|
|
11
|
+
await Project.all();
|
|
12
|
+
await Project.create({ name: "Hi" });
|
|
13
|
+
await Project.find(1); // null if another team's
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Type-check harness for docs/watch.md. Compile-only — never executed.
|
|
2
|
+
import { Application } from "@shaferllc/keel/core";
|
|
3
|
+
import { WatchServiceProvider, Watch } from "@shaferllc/keel/watch";
|
|
4
|
+
|
|
5
|
+
export function install() {
|
|
6
|
+
const app = new Application();
|
|
7
|
+
app.register(WatchServiceProvider);
|
|
8
|
+
Watch.auth((c) => c.req.header("x-watch-key") === "secret");
|
|
9
|
+
return app;
|
|
10
|
+
}
|
package/docs/orm.md
CHANGED
|
@@ -55,3 +55,42 @@ The ORM is deliberately small — enough for CRUD, relationships, and the common
|
|
|
55
55
|
query shapes without an ORM dependency. For a gnarly one-off report, reach for
|
|
56
56
|
the [query builder](./query-builder.md) or a raw `connection().select(sql)`; the
|
|
57
57
|
model layer never gets in the way.
|
|
58
|
+
|
|
59
|
+
## A worked example
|
|
60
|
+
|
|
61
|
+
A blog with authors and posts — enough to see CRUD, a relation, and eager loading
|
|
62
|
+
together:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Model } from "@shaferllc/keel/core";
|
|
66
|
+
|
|
67
|
+
class Post extends Model {
|
|
68
|
+
static table = "posts";
|
|
69
|
+
static fillable = ["title", "body"];
|
|
70
|
+
declare id: number;
|
|
71
|
+
declare title: string;
|
|
72
|
+
declare user_id: number;
|
|
73
|
+
|
|
74
|
+
author() { return this.belongsTo(User); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class User extends Model {
|
|
78
|
+
static table = "users";
|
|
79
|
+
declare id: number;
|
|
80
|
+
declare email: string;
|
|
81
|
+
|
|
82
|
+
posts() { return this.hasMany(Post); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ada = await User.create({ email: "ada@example.com" });
|
|
86
|
+
await ada.posts().create({ title: "Notes on engines", body: "…" });
|
|
87
|
+
|
|
88
|
+
const withPosts = await User.with("posts").where("id", ada.id).first();
|
|
89
|
+
for (const post of (withPosts as User & { posts: Post[] }).posts) {
|
|
90
|
+
console.log(post.title);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For the full surface — casts, soft deletes, scopes, polymorphic relations —
|
|
95
|
+
see [Models](./models.md).
|
|
96
|
+
|
package/docs/starter-kits.md
CHANGED
|
@@ -14,6 +14,20 @@ to finish.
|
|
|
14
14
|
| `app` *(default)* | Full-stack: views, sessions, register/login, password reset, two-factor. |
|
|
15
15
|
| `saas` | `app` plus teams, roles, invitations, billing, and multi-tenancy. |
|
|
16
16
|
|
|
17
|
+
## Pick a kit
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create keeljs@latest my-app # app (default)
|
|
21
|
+
npm create keeljs@latest my-api -- --preset api
|
|
22
|
+
npm create keeljs@latest my-saas -- --preset saas
|
|
23
|
+
npm create keeljs@latest bare -- --preset minimal
|
|
24
|
+
cd my-app && npm install && npm run dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then open `http://localhost:3000`. The SaaS kit already has a team switcher,
|
|
28
|
+
invites, and a billing stub wired through [teams](./teams.md) and
|
|
29
|
+
[billing](./billing.md) — start by editing `app/Models` and `routes/web.ts`.
|
|
30
|
+
|
|
17
31
|
## Every database, Cloudflare first
|
|
18
32
|
|
|
19
33
|
Each kit ships with all four drivers wired. Switching is `DB_CONNECTION` and nothing
|
package/docs/watch.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Watch
|
|
2
2
|
|
|
3
|
-
Keel Watch is a debug dashboard
|
|
3
|
+
Keel Watch is a debug dashboard for Keel apps. It records the requests,
|
|
4
4
|
queries, exceptions, logs, jobs, mail, notifications, cache lookups, events, and
|
|
5
5
|
scheduled tasks flowing through your app, and shows them in a single-page UI at
|
|
6
6
|
`/watch`, with every request linked to the queries and logs it produced.
|
package/llms-full.txt
CHANGED
|
@@ -5764,6 +5764,46 @@ resolveBillableUsing(async (customerId) => {
|
|
|
5764
5764
|
});
|
|
5765
5765
|
```
|
|
5766
5766
|
|
|
5767
|
+
## A complete flow
|
|
5768
|
+
|
|
5769
|
+
From "user signs up" to "they're subscribed", with the fake gateway for tests:
|
|
5770
|
+
|
|
5771
|
+
```ts
|
|
5772
|
+
import { Model } from "@shaferllc/keel/core";
|
|
5773
|
+
import {
|
|
5774
|
+
Billable,
|
|
5775
|
+
BillingManager,
|
|
5776
|
+
FakeGateway,
|
|
5777
|
+
setBilling,
|
|
5778
|
+
} from "@shaferllc/keel/billing";
|
|
5779
|
+
|
|
5780
|
+
class User extends Billable(Model) {
|
|
5781
|
+
static table = "users";
|
|
5782
|
+
declare email: string;
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
// In a test bootstrap:
|
|
5786
|
+
const fake = new FakeGateway();
|
|
5787
|
+
const manager = new BillingManager({
|
|
5788
|
+
default: "fake",
|
|
5789
|
+
currency: "usd",
|
|
5790
|
+
billableModel: "User",
|
|
5791
|
+
webhook: { path: "billing/webhook" },
|
|
5792
|
+
gateways: { stripe: { key: "", webhookSecret: "" }, paddle: { key: "", webhookSecret: "" }, fake: {} },
|
|
5793
|
+
});
|
|
5794
|
+
manager.register("fake", () => fake);
|
|
5795
|
+
setBilling(manager);
|
|
5796
|
+
|
|
5797
|
+
const user = await User.create({ email: "ada@example.com" });
|
|
5798
|
+
await user.newSubscription("default", "price_pro").trialDays(14).create();
|
|
5799
|
+
|
|
5800
|
+
await user.subscribed(); // true
|
|
5801
|
+
fake.calls.some((c) => c.method === "createSubscription"); // true
|
|
5802
|
+
```
|
|
5803
|
+
|
|
5804
|
+
In production you skip the fake manager — `BillingServiceProvider` wires the
|
|
5805
|
+
real gateway from `config/billing.ts` and `.env`.
|
|
5806
|
+
|
|
5767
5807
|
## Gateway differences
|
|
5768
5808
|
|
|
5769
5809
|
| Concern | Stripe | Paddle |
|
|
@@ -7611,6 +7651,34 @@ router.group(() => { /* … */ }).use(cors({ origin: ["https://app.example.com"]
|
|
|
7611
7651
|
With no options, `cors()` reflects the caller's origin — convenient in
|
|
7612
7652
|
development. **Lock it down in production** with an explicit allowlist.
|
|
7613
7653
|
|
|
7654
|
+
## A production API group
|
|
7655
|
+
|
|
7656
|
+
Typical setup for a JSON API served from `api.example.com` and called from a
|
|
7657
|
+
SPA on `app.example.com`:
|
|
7658
|
+
|
|
7659
|
+
```ts
|
|
7660
|
+
// app/Http/Kernel.ts
|
|
7661
|
+
import { cors } from "@shaferllc/keel/core";
|
|
7662
|
+
|
|
7663
|
+
this.use(
|
|
7664
|
+
cors({
|
|
7665
|
+
origin: ["https://app.example.com"],
|
|
7666
|
+
credentials: true, // cookies / Authorization
|
|
7667
|
+
exposeHeaders: ["X-Request-Id"],
|
|
7668
|
+
}),
|
|
7669
|
+
);
|
|
7670
|
+
```
|
|
7671
|
+
|
|
7672
|
+
During local development, allow any localhost port with a predicate:
|
|
7673
|
+
|
|
7674
|
+
```ts
|
|
7675
|
+
cors({
|
|
7676
|
+
origin: (origin) =>
|
|
7677
|
+
origin.startsWith("http://localhost:") || origin === "https://app.example.com",
|
|
7678
|
+
credentials: true,
|
|
7679
|
+
});
|
|
7680
|
+
```
|
|
7681
|
+
|
|
7614
7682
|
## Options
|
|
7615
7683
|
|
|
7616
7684
|
```ts
|
|
@@ -14934,6 +15002,44 @@ query shapes without an ORM dependency. For a gnarly one-off report, reach for
|
|
|
14934
15002
|
the [query builder](./query-builder.md) or a raw `connection().select(sql)`; the
|
|
14935
15003
|
model layer never gets in the way.
|
|
14936
15004
|
|
|
15005
|
+
## A worked example
|
|
15006
|
+
|
|
15007
|
+
A blog with authors and posts — enough to see CRUD, a relation, and eager loading
|
|
15008
|
+
together:
|
|
15009
|
+
|
|
15010
|
+
```ts
|
|
15011
|
+
import { Model } from "@shaferllc/keel/core";
|
|
15012
|
+
|
|
15013
|
+
class Post extends Model {
|
|
15014
|
+
static table = "posts";
|
|
15015
|
+
static fillable = ["title", "body"];
|
|
15016
|
+
declare id: number;
|
|
15017
|
+
declare title: string;
|
|
15018
|
+
declare user_id: number;
|
|
15019
|
+
|
|
15020
|
+
author() { return this.belongsTo(User); }
|
|
15021
|
+
}
|
|
15022
|
+
|
|
15023
|
+
class User extends Model {
|
|
15024
|
+
static table = "users";
|
|
15025
|
+
declare id: number;
|
|
15026
|
+
declare email: string;
|
|
15027
|
+
|
|
15028
|
+
posts() { return this.hasMany(Post); }
|
|
15029
|
+
}
|
|
15030
|
+
|
|
15031
|
+
const ada = await User.create({ email: "ada@example.com" });
|
|
15032
|
+
await ada.posts().create({ title: "Notes on engines", body: "…" });
|
|
15033
|
+
|
|
15034
|
+
const withPosts = await User.with("posts").where("id", ada.id).first();
|
|
15035
|
+
for (const post of (withPosts as User & { posts: Post[] }).posts) {
|
|
15036
|
+
console.log(post.title);
|
|
15037
|
+
}
|
|
15038
|
+
```
|
|
15039
|
+
|
|
15040
|
+
For the full surface — casts, soft deletes, scopes, polymorphic relations —
|
|
15041
|
+
see [Models](./models.md).
|
|
15042
|
+
|
|
14937
15043
|
|
|
14938
15044
|
|
|
14939
15045
|
---
|
|
@@ -17630,6 +17736,20 @@ to finish.
|
|
|
17630
17736
|
| `app` *(default)* | Full-stack: views, sessions, register/login, password reset, two-factor. |
|
|
17631
17737
|
| `saas` | `app` plus teams, roles, invitations, billing, and multi-tenancy. |
|
|
17632
17738
|
|
|
17739
|
+
## Pick a kit
|
|
17740
|
+
|
|
17741
|
+
```bash
|
|
17742
|
+
npm create keeljs@latest my-app # app (default)
|
|
17743
|
+
npm create keeljs@latest my-api -- --preset api
|
|
17744
|
+
npm create keeljs@latest my-saas -- --preset saas
|
|
17745
|
+
npm create keeljs@latest bare -- --preset minimal
|
|
17746
|
+
cd my-app && npm install && npm run dev
|
|
17747
|
+
```
|
|
17748
|
+
|
|
17749
|
+
Then open `http://localhost:3000`. The SaaS kit already has a team switcher,
|
|
17750
|
+
invites, and a billing stub wired through [teams](./teams.md) and
|
|
17751
|
+
[billing](./billing.md) — start by editing `app/Models` and `routes/web.ts`.
|
|
17752
|
+
|
|
17633
17753
|
## Every database, Cloudflare first
|
|
17634
17754
|
|
|
17635
17755
|
Each kit ships with all four drivers wired. Switching is `DB_CONNECTION` and nothing
|
|
@@ -21244,7 +21364,7 @@ Vite's `manifest.json`, as produced by the build. You rarely touch it directly
|
|
|
21244
21364
|
|
|
21245
21365
|
# Watch
|
|
21246
21366
|
|
|
21247
|
-
Keel Watch is a debug dashboard
|
|
21367
|
+
Keel Watch is a debug dashboard for Keel apps. It records the requests,
|
|
21248
21368
|
queries, exceptions, logs, jobs, mail, notifications, cache lookups, events, and
|
|
21249
21369
|
scheduled tasks flowing through your app, and shows them in a single-page UI at
|
|
21250
21370
|
`/watch`, with every request linked to the queries and logs it produced.
|
package/llms.txt
CHANGED
|
@@ -72,7 +72,7 @@ Userland imports everything from `@shaferllc/keel/core`.
|
|
|
72
72
|
- [Validation](https://github.com/shaferllc/keel/blob/main/docs/validation.md): validate() parses request input against a schema and returns typed data.
|
|
73
73
|
- [Views](https://github.com/shaferllc/keel/blob/main/docs/views.md): Keel renders HTML with Hono JSX — type-safe components that run identically on Node and on Cloudflare Workers (no filesystem templating, so it ports anywhere).
|
|
74
74
|
- [Vite](https://github.com/shaferllc/keel/blob/main/docs/vite.md): Wire a modern frontend build — bundling, hashed filenames, hot module reload — to Keel's server-rendered HTML, the way modern full-stack frameworks do.
|
|
75
|
-
- [Watch](https://github.com/shaferllc/keel/blob/main/docs/watch.md): Keel Watch is a debug dashboard
|
|
75
|
+
- [Watch](https://github.com/shaferllc/keel/blob/main/docs/watch.md): Keel Watch is a debug dashboard for Keel apps.
|
|
76
76
|
|
|
77
77
|
## Examples
|
|
78
78
|
|
|
@@ -82,6 +82,7 @@ Every topic has a runnable, type-checked example:
|
|
|
82
82
|
- [API Resources example](https://github.com/shaferllc/keel/blob/main/docs/examples/api-resources.ts)
|
|
83
83
|
- [Authentication example](https://github.com/shaferllc/keel/blob/main/docs/examples/authentication.ts)
|
|
84
84
|
- [Authorization example](https://github.com/shaferllc/keel/blob/main/docs/examples/authorization.ts)
|
|
85
|
+
- [Billing example](https://github.com/shaferllc/keel/blob/main/docs/examples/billing.ts)
|
|
85
86
|
- [Broadcasting example](https://github.com/shaferllc/keel/blob/main/docs/examples/broadcasting.ts)
|
|
86
87
|
- [Service Broker example](https://github.com/shaferllc/keel/blob/main/docs/examples/broker.ts)
|
|
87
88
|
- [Cache example](https://github.com/shaferllc/keel/blob/main/docs/examples/cache.ts)
|
|
@@ -89,6 +90,7 @@ Every topic has a runnable, type-checked example:
|
|
|
89
90
|
- [The Console example](https://github.com/shaferllc/keel/blob/main/docs/examples/console.ts)
|
|
90
91
|
- [The Service Container example](https://github.com/shaferllc/keel/blob/main/docs/examples/container.ts)
|
|
91
92
|
- [Controllers example](https://github.com/shaferllc/keel/blob/main/docs/examples/controllers.ts)
|
|
93
|
+
- [CORS example](https://github.com/shaferllc/keel/blob/main/docs/examples/cors.ts)
|
|
92
94
|
- [Database example](https://github.com/shaferllc/keel/blob/main/docs/examples/database.ts)
|
|
93
95
|
- [Debugging example](https://github.com/shaferllc/keel/blob/main/docs/examples/debugging.ts)
|
|
94
96
|
- [Request Decorators example](https://github.com/shaferllc/keel/blob/main/docs/examples/decorators.ts)
|
|
@@ -98,6 +100,7 @@ Every topic has a runnable, type-checked example:
|
|
|
98
100
|
- [Hashing & Encryption example](https://github.com/shaferllc/keel/blob/main/docs/examples/hashing.ts)
|
|
99
101
|
- [Health Checks example](https://github.com/shaferllc/keel/blob/main/docs/examples/health.ts)
|
|
100
102
|
- [Helpers example](https://github.com/shaferllc/keel/blob/main/docs/examples/helpers.ts)
|
|
103
|
+
- [Built on Hono example](https://github.com/shaferllc/keel/blob/main/docs/examples/hono.ts)
|
|
101
104
|
- [Lifecycle Hooks example](https://github.com/shaferllc/keel/blob/main/docs/examples/hooks.ts)
|
|
102
105
|
- [Internationalization example](https://github.com/shaferllc/keel/blob/main/docs/examples/i18n.ts)
|
|
103
106
|
- [Inertia example](https://github.com/shaferllc/keel/blob/main/docs/examples/inertia.ts)
|
|
@@ -108,15 +111,22 @@ Every topic has a runnable, type-checked example:
|
|
|
108
111
|
- [Migrations example](https://github.com/shaferllc/keel/blob/main/docs/examples/migrations.ts)
|
|
109
112
|
- [Models example](https://github.com/shaferllc/keel/blob/main/docs/examples/models.ts)
|
|
110
113
|
- [Notifications example](https://github.com/shaferllc/keel/blob/main/docs/examples/notification.ts)
|
|
114
|
+
- [OpenAPI example](https://github.com/shaferllc/keel/blob/main/docs/examples/openapi.ts)
|
|
115
|
+
- [ORM example](https://github.com/shaferllc/keel/blob/main/docs/examples/orm.ts)
|
|
116
|
+
- [Packages example](https://github.com/shaferllc/keel/blob/main/docs/examples/packages.ts)
|
|
111
117
|
- [Pages example](https://github.com/shaferllc/keel/blob/main/docs/examples/pages.ts)
|
|
112
118
|
- [Service Providers example](https://github.com/shaferllc/keel/blob/main/docs/examples/providers.ts)
|
|
119
|
+
- [Query Builder example](https://github.com/shaferllc/keel/blob/main/docs/examples/query-builder.ts)
|
|
113
120
|
- [Queues & Jobs example](https://github.com/shaferllc/keel/blob/main/docs/examples/queues.ts)
|
|
114
121
|
- [Rate Limiting example](https://github.com/shaferllc/keel/blob/main/docs/examples/rate-limiting.ts)
|
|
115
122
|
- [Redis example](https://github.com/shaferllc/keel/blob/main/docs/examples/redis.ts)
|
|
116
123
|
- [Request & Response example](https://github.com/shaferllc/keel/blob/main/docs/examples/request-response.ts)
|
|
117
124
|
- [Routing example](https://github.com/shaferllc/keel/blob/main/docs/examples/routing.ts)
|
|
118
125
|
- [Task Scheduling example](https://github.com/shaferllc/keel/blob/main/docs/examples/scheduling.ts)
|
|
126
|
+
- [Securing SSR apps example](https://github.com/shaferllc/keel/blob/main/docs/examples/security.ts)
|
|
119
127
|
- [Sessions example](https://github.com/shaferllc/keel/blob/main/docs/examples/sessions.ts)
|
|
128
|
+
- [Social authentication example](https://github.com/shaferllc/keel/blob/main/docs/examples/social-auth.ts)
|
|
129
|
+
- [Starter kits example](https://github.com/shaferllc/keel/blob/main/docs/examples/starter-kits.ts)
|
|
120
130
|
- [Static Files example](https://github.com/shaferllc/keel/blob/main/docs/examples/static-files.ts)
|
|
121
131
|
- [Storage example](https://github.com/shaferllc/keel/blob/main/docs/examples/storage.ts)
|
|
122
132
|
- [Teams example](https://github.com/shaferllc/keel/blob/main/docs/examples/teams.ts)
|
|
@@ -128,6 +138,7 @@ Every topic has a runnable, type-checked example:
|
|
|
128
138
|
- [Validation example](https://github.com/shaferllc/keel/blob/main/docs/examples/validation.ts)
|
|
129
139
|
- [Views example](https://github.com/shaferllc/keel/blob/main/docs/examples/views.tsx)
|
|
130
140
|
- [Vite example](https://github.com/shaferllc/keel/blob/main/docs/examples/vite.ts)
|
|
141
|
+
- [Watch example](https://github.com/shaferllc/keel/blob/main/docs/examples/watch.ts)
|
|
131
142
|
|
|
132
143
|
## Optional
|
|
133
144
|
|
package/package.json
CHANGED