@shaferllc/keel 0.59.0 → 0.68.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 +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +14 -2
- package/dist/core/auth.d.ts +47 -0
- package/dist/core/auth.js +77 -0
- package/dist/core/authorization.d.ts +9 -1
- package/dist/core/authorization.js +22 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/container.d.ts +20 -0
- package/dist/core/container.js +52 -0
- package/dist/core/cors.d.ts +29 -0
- package/dist/core/cors.js +72 -0
- package/dist/core/crypto.d.ts +40 -4
- package/dist/core/crypto.js +66 -6
- package/dist/core/csrf.d.ts +25 -0
- package/dist/core/csrf.js +78 -0
- package/dist/core/database.d.ts +49 -4
- package/dist/core/database.js +89 -21
- 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 +15 -3
- package/dist/core/helpers.js +23 -3
- package/dist/core/index.d.ts +33 -18
- package/dist/core/index.js +16 -8
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/model.d.ts +2 -0
- package/dist/core/model.js +16 -14
- package/dist/core/provider.d.ts +7 -0
- package/dist/core/provider.js +7 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/rate-limit.js +3 -0
- package/dist/core/relations.js +13 -13
- package/dist/core/request.d.ts +26 -0
- package/dist/core/request.js +77 -0
- package/dist/core/shield.d.ts +39 -0
- package/dist/core/shield.js +60 -0
- package/dist/core/social.d.ts +173 -0
- package/dist/core/social.js +337 -0
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/core/tokens.d.ts +74 -0
- package/dist/core/tokens.js +155 -0
- package/dist/db/d1.d.ts +32 -0
- package/dist/db/d1.js +26 -0
- package/dist/db/libsql.d.ts +29 -0
- package/dist/db/libsql.js +32 -0
- package/dist/db/pg.d.ts +29 -0
- package/dist/db/pg.js +33 -0
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -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 +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -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 +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -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/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -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/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/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -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/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -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/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 +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -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/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +38 -7
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Type-check harness for docs/routing.md. Every type-checkable snippet in the
|
|
2
|
+
// reference is exercised here against the real exports, so a renamed method or
|
|
3
|
+
// wrong argument type fails `npm run typecheck:docs`. Compile-only — never run.
|
|
4
|
+
import {
|
|
5
|
+
Router,
|
|
6
|
+
Route,
|
|
7
|
+
RouteGroup,
|
|
8
|
+
RouteResource,
|
|
9
|
+
matchers,
|
|
10
|
+
json,
|
|
11
|
+
text,
|
|
12
|
+
request,
|
|
13
|
+
type Ctx,
|
|
14
|
+
type RouteHandler,
|
|
15
|
+
type RouteDefinition,
|
|
16
|
+
type Method,
|
|
17
|
+
type Matcher,
|
|
18
|
+
type MiddlewareRef,
|
|
19
|
+
} from "@shaferllc/keel/core";
|
|
20
|
+
|
|
21
|
+
// The framework builds the Router and hands it to your routes file; users never
|
|
22
|
+
// construct it, so we `declare` one here.
|
|
23
|
+
declare const router: Router;
|
|
24
|
+
declare const handler: RouteHandler;
|
|
25
|
+
declare const auth: MiddlewareRef;
|
|
26
|
+
declare const admin: MiddlewareRef;
|
|
27
|
+
declare const logRequests: MiddlewareRef;
|
|
28
|
+
declare const rateLimiterMw: MiddlewareRef;
|
|
29
|
+
declare const user: unknown;
|
|
30
|
+
declare const AboutPage: (props?: unknown) => unknown;
|
|
31
|
+
|
|
32
|
+
// Controllers can be plain classes.
|
|
33
|
+
class HomeController {
|
|
34
|
+
index(_c: Ctx) {
|
|
35
|
+
return "home";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class UserController {
|
|
39
|
+
show(c: Ctx) {
|
|
40
|
+
return c.json({ id: 1 });
|
|
41
|
+
}
|
|
42
|
+
store(_c: Ctx) {
|
|
43
|
+
return "created";
|
|
44
|
+
}
|
|
45
|
+
destroy(_c: Ctx) {
|
|
46
|
+
return "gone";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class PostController {}
|
|
50
|
+
class CommentController {}
|
|
51
|
+
class HookController {
|
|
52
|
+
handle(_c: Ctx) {
|
|
53
|
+
return "ok";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
class MeController {
|
|
57
|
+
show(_c: Ctx) {
|
|
58
|
+
return "me";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function verbs() {
|
|
63
|
+
router.get("/users/:id", [UserController, "show"]);
|
|
64
|
+
router.post("/users", [UserController, "store"]);
|
|
65
|
+
router.put("/users/:id", handler);
|
|
66
|
+
router.patch("/users/:id", handler);
|
|
67
|
+
router.delete("/users/:id", [UserController, "destroy"]);
|
|
68
|
+
router.any("/webhook", [HookController, "handle"]);
|
|
69
|
+
router.route(["GET", "POST"], "/search", handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function handlerShapes() {
|
|
73
|
+
router.get("/a", (c: Ctx) => c.json({ ok: true })); // HandlerFn
|
|
74
|
+
router.get("/hi/:name", (c: Ctx) => c.text("hi")); // HandlerFn -> string
|
|
75
|
+
router.get("/b", [UserController, "show"]); // ControllerAction
|
|
76
|
+
router.get("/single", [HookController]); // [Controller] -> handle
|
|
77
|
+
router.get("/c", json({ up: true })); // Response
|
|
78
|
+
router.get("/robots.txt", text("User-agent: *"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function brisk() {
|
|
82
|
+
router.on("/old").redirect("/new");
|
|
83
|
+
router.on("/ext").redirectToPath("https://example.com", 301);
|
|
84
|
+
router.on("/posts").redirectToRoute("articles.index", {}, { qs: { page: 1 } });
|
|
85
|
+
router.on("/about").render(AboutPage, { title: "About" });
|
|
86
|
+
router.on("/dashboard").renderInertia("Dashboard", { user });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function grouping() {
|
|
90
|
+
const g: RouteGroup = router
|
|
91
|
+
.group(() => {
|
|
92
|
+
router.get("/status", json({ up: true })).name("status");
|
|
93
|
+
router.get("/me", [MeController, "show"]).name("me");
|
|
94
|
+
})
|
|
95
|
+
.prefix("/api")
|
|
96
|
+
.middleware([auth])
|
|
97
|
+
.as("api");
|
|
98
|
+
g.where("id", matchers.uuid()).domain(":tenant.example.com").use([logRequests]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function routeConfig() {
|
|
102
|
+
router.get("/admin", handler).config({ scope: "admin", rateTier: "high" });
|
|
103
|
+
router
|
|
104
|
+
.group(() => {
|
|
105
|
+
router.get("/billing", handler);
|
|
106
|
+
router.get("/billing/export", handler).config({ heavy: true });
|
|
107
|
+
})
|
|
108
|
+
.config({ area: "billing" });
|
|
109
|
+
const cfg = request.route?.config; // { … } | undefined
|
|
110
|
+
void cfg;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resources() {
|
|
114
|
+
const res: RouteResource = router.resource("posts", PostController);
|
|
115
|
+
res.only(["index", "show"]);
|
|
116
|
+
res.except(["destroy"]);
|
|
117
|
+
res.apiOnly();
|
|
118
|
+
res.as("articles");
|
|
119
|
+
res.params({ posts: "post" });
|
|
120
|
+
res.use(["store", "update", "destroy"], "auth");
|
|
121
|
+
res.use("*", logRequests);
|
|
122
|
+
|
|
123
|
+
router.resource("posts.comments", CommentController); // nested
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function constraints() {
|
|
127
|
+
router.get("/users/:id", handler).where("id", /\d+/);
|
|
128
|
+
router.get("/u/:id", handler).where("id", matchers.number());
|
|
129
|
+
router.get("/a/:id", handler).where("id", matchers.uuid());
|
|
130
|
+
router.get("/s/:slug", handler).where("slug", matchers.slug());
|
|
131
|
+
router.get("/x/:id", handler).where("id", { match: /\d+/ });
|
|
132
|
+
router.where("id", matchers.number()); // global
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function routeChain() {
|
|
136
|
+
const r: Route = router.get("/users/:id", [UserController, "show"]);
|
|
137
|
+
r.name("users.show");
|
|
138
|
+
r.as("users.show");
|
|
139
|
+
r.middleware([auth]);
|
|
140
|
+
r.use(["auth", "admin"]);
|
|
141
|
+
r.where("id", /\d+/);
|
|
142
|
+
r.domain("blog.example.com");
|
|
143
|
+
const def: RouteDefinition = r.def;
|
|
144
|
+
return def;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function named() {
|
|
148
|
+
const map: Parameters<typeof router.named>[0] = {};
|
|
149
|
+
router.named(map);
|
|
150
|
+
const mw = router.resolveMiddleware("auth");
|
|
151
|
+
return mw;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function inspect() {
|
|
155
|
+
const routes: RouteDefinition[] = router.all();
|
|
156
|
+
for (const def of routes) {
|
|
157
|
+
console.log(def.methods.join("|"), def.path, def.name ?? "");
|
|
158
|
+
}
|
|
159
|
+
const fn = router.resolve([UserController, "show"]);
|
|
160
|
+
return { routes, fn };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// matchers stand-alone
|
|
164
|
+
export function matcherValues() {
|
|
165
|
+
router.get("/u/:id", handler).where("id", matchers.number());
|
|
166
|
+
return [matchers.number(), matchers.uuid(), matchers.slug(), matchers.alpha()];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Interface / type seams
|
|
170
|
+
const aMatcher: Matcher = /\d+/;
|
|
171
|
+
const bMatcher: Matcher = "\\d+";
|
|
172
|
+
const cMatcher: Matcher = { match: /[a-z]+/ };
|
|
173
|
+
const method: Method = "GET";
|
|
174
|
+
const methods: Method[] = ["GET", "POST"];
|
|
175
|
+
const ref: MiddlewareRef = "auth";
|
|
176
|
+
const refFn: MiddlewareRef = rateLimiterMw;
|
|
177
|
+
const routeDef: RouteDefinition = {
|
|
178
|
+
methods: ["GET"],
|
|
179
|
+
path: "/",
|
|
180
|
+
handler: (c: Ctx) => c.text("ok"),
|
|
181
|
+
middleware: [],
|
|
182
|
+
wheres: {},
|
|
183
|
+
config: {},
|
|
184
|
+
};
|
|
185
|
+
const someHandler: RouteHandler = (c: Ctx) => c.json({ ok: true });
|
|
186
|
+
export { aMatcher, bMatcher, cMatcher, method, methods, ref, refFn, routeDef, someHandler, admin };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Type-check harness for docs/scheduling.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
schedule,
|
|
4
|
+
scheduler,
|
|
5
|
+
setScheduler,
|
|
6
|
+
Scheduler,
|
|
7
|
+
cronMatches,
|
|
8
|
+
Job,
|
|
9
|
+
logger,
|
|
10
|
+
} from "@shaferllc/keel/core";
|
|
11
|
+
|
|
12
|
+
class PruneSessions extends Job {
|
|
13
|
+
async handle() {}
|
|
14
|
+
}
|
|
15
|
+
class SendDigest extends Job {
|
|
16
|
+
async handle() {}
|
|
17
|
+
}
|
|
18
|
+
declare function syncInventory(): Promise<void>;
|
|
19
|
+
declare const job: Job;
|
|
20
|
+
|
|
21
|
+
export function scheduling() {
|
|
22
|
+
schedule(new PruneSessions()).daily();
|
|
23
|
+
schedule(() => syncInventory()).everyFiveMinutes();
|
|
24
|
+
schedule(new SendDigest()).cron("0 9 * * 1");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function cadences() {
|
|
28
|
+
const t = schedule(job);
|
|
29
|
+
t.everyMinute();
|
|
30
|
+
t.everyTenMinutes();
|
|
31
|
+
t.hourly();
|
|
32
|
+
t.hourlyAt(15);
|
|
33
|
+
t.daily();
|
|
34
|
+
t.dailyAt("13:30");
|
|
35
|
+
t.weekly(1);
|
|
36
|
+
t.monthly(15);
|
|
37
|
+
t.cron("*/10 9-17 * * 1-5");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Cloudflare scheduled handler shape
|
|
41
|
+
type ScheduledEvent = { scheduledTime: number };
|
|
42
|
+
type ExecutionContext = { waitUntil(p: Promise<unknown>): void };
|
|
43
|
+
export const worker = {
|
|
44
|
+
async scheduled(event: ScheduledEvent, _env: unknown, ctx: ExecutionContext) {
|
|
45
|
+
ctx.waitUntil(scheduler().runDue(new Date(event.scheduledTime)));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function nodeInterval() {
|
|
50
|
+
setInterval(() => scheduler().runDue(new Date()), 60_000);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function inspecting() {
|
|
54
|
+
schedule(job).named("prune-sessions").daily();
|
|
55
|
+
for (const t of scheduler().due(new Date())) logger().info("due", { task: t.name });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function fresh(): Scheduler {
|
|
59
|
+
const s = setScheduler(new Scheduler());
|
|
60
|
+
const match: boolean = cronMatches("*/5 * * * *", new Date());
|
|
61
|
+
return match ? s : scheduler();
|
|
62
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Type-check harness for docs/sessions.md. Every type-checkable snippet in the
|
|
2
|
+
// reference is exercised here against the real exports, so a renamed method or
|
|
3
|
+
// wrong argument type fails `npm run typecheck:docs`. Compile-only — never run.
|
|
4
|
+
import {
|
|
5
|
+
HttpKernel,
|
|
6
|
+
Application,
|
|
7
|
+
session,
|
|
8
|
+
sessionMiddleware,
|
|
9
|
+
Session,
|
|
10
|
+
redirect,
|
|
11
|
+
type SessionOptions,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const user: { id: number };
|
|
15
|
+
|
|
16
|
+
export class Kernel extends HttpKernel {
|
|
17
|
+
constructor(app: Application) {
|
|
18
|
+
super(app);
|
|
19
|
+
this.use(sessionMiddleware());
|
|
20
|
+
this.use(sessionMiddleware({ cookieName: "sid", cookie: { secure: true } }));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useIt() {
|
|
25
|
+
session().put("userId", user.id);
|
|
26
|
+
const id = session().get("userId");
|
|
27
|
+
const id2 = session().get("userId", null);
|
|
28
|
+
|
|
29
|
+
session().has("userId");
|
|
30
|
+
session().forget("userId");
|
|
31
|
+
session().pull("cart");
|
|
32
|
+
session().increment("visits");
|
|
33
|
+
session().clear();
|
|
34
|
+
session().all();
|
|
35
|
+
|
|
36
|
+
return { id, id2 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sharedData() {
|
|
40
|
+
session().put("step", 1);
|
|
41
|
+
return session().get("step");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function flashMessages() {
|
|
45
|
+
session().flash("status", "Profile saved!");
|
|
46
|
+
const r = redirect("/profile");
|
|
47
|
+
|
|
48
|
+
const status = session().flashed("status");
|
|
49
|
+
return { r, status };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function flashCompartments() {
|
|
53
|
+
session().flash("status", "Saved!");
|
|
54
|
+
const a = session().flashed("status");
|
|
55
|
+
const b = session().get("status");
|
|
56
|
+
return { a, b };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function counters() {
|
|
60
|
+
session().increment("visits");
|
|
61
|
+
session().increment("credits", 10);
|
|
62
|
+
session().decrement("credits", 3);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- API reference ---
|
|
66
|
+
|
|
67
|
+
export function sessionRef() {
|
|
68
|
+
session().put("userId", 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function middlewareRef() {
|
|
72
|
+
return sessionMiddleware({ cookieName: "sid", cookie: { secure: true } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sessionMethods() {
|
|
76
|
+
const s: Session = session();
|
|
77
|
+
|
|
78
|
+
const data = s.all();
|
|
79
|
+
const id = s.get<number>("userId");
|
|
80
|
+
const theme = s.get("theme", "light");
|
|
81
|
+
s.put("userId", 1).put("theme", "dark");
|
|
82
|
+
s.set("locale", "en");
|
|
83
|
+
const logged = s.has("userId");
|
|
84
|
+
s.forget("userId");
|
|
85
|
+
const cart = s.pull("cart", []);
|
|
86
|
+
s.increment("visits");
|
|
87
|
+
s.increment("credits", 25);
|
|
88
|
+
s.decrement("credits", 3);
|
|
89
|
+
s.clear();
|
|
90
|
+
s.flash("status", "Profile saved!");
|
|
91
|
+
const status = s.flashed("status");
|
|
92
|
+
const msg = s.flashed("msg", "");
|
|
93
|
+
|
|
94
|
+
return { data, id, theme, logged, cart, status, msg };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Interface / type seams
|
|
98
|
+
const opts: SessionOptions = {
|
|
99
|
+
cookieName: "sid",
|
|
100
|
+
cookie: { secure: true, maxAge: 60 * 60 * 24 },
|
|
101
|
+
};
|
|
102
|
+
export { opts };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Type-check harness for docs/static-files.md. Every type-checkable snippet in
|
|
2
|
+
// the guide is exercised here against the real exports, so a renamed option or
|
|
3
|
+
// wrong signature fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
HttpKernel,
|
|
6
|
+
serveStatic,
|
|
7
|
+
type Application,
|
|
8
|
+
type StaticOptions,
|
|
9
|
+
} from "@shaferllc/keel/core";
|
|
10
|
+
|
|
11
|
+
export class Kernel extends HttpKernel {
|
|
12
|
+
constructor(app: Application) {
|
|
13
|
+
super(app);
|
|
14
|
+
this.use(serveStatic()); // serves ./public
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function options() {
|
|
19
|
+
serveStatic({
|
|
20
|
+
root: "./public",
|
|
21
|
+
index: "index.html",
|
|
22
|
+
dotFiles: "ignore",
|
|
23
|
+
maxAge: 86400,
|
|
24
|
+
immutable: true,
|
|
25
|
+
headers: (path) =>
|
|
26
|
+
path.endsWith(".html") ? { "X-Frame-Options": "DENY" } : undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function immutableAssets(kernel: HttpKernel) {
|
|
31
|
+
kernel.use(serveStatic({ root: "./dist", maxAge: 31536000, immutable: true }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function perFileHeaders() {
|
|
35
|
+
serveStatic({
|
|
36
|
+
headers: (path): Record<string, string> | undefined => {
|
|
37
|
+
if (path.endsWith(".html")) return { "X-Frame-Options": "DENY" };
|
|
38
|
+
if (path.endsWith(".wasm")) return { "Cross-Origin-Embedder-Policy": "require-corp" };
|
|
39
|
+
return undefined;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reference(kernel: HttpKernel) {
|
|
45
|
+
const assets = serveStatic({ root: "./public", maxAge: 86400 });
|
|
46
|
+
kernel.use(assets);
|
|
47
|
+
|
|
48
|
+
// No-argument form is valid — all options default.
|
|
49
|
+
serveStatic();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Interface / type seam
|
|
53
|
+
const staticOpts: StaticOptions = {
|
|
54
|
+
root: "./dist",
|
|
55
|
+
index: "index.html",
|
|
56
|
+
dotFiles: "deny",
|
|
57
|
+
maxAge: 31536000,
|
|
58
|
+
immutable: true,
|
|
59
|
+
headers: (path) => (path.endsWith(".html") ? { "X-Frame-Options": "DENY" } : undefined),
|
|
60
|
+
};
|
|
61
|
+
serveStatic(staticOpts);
|
|
62
|
+
|
|
63
|
+
export { staticOpts };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Type-check harness for docs/storage.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
storage,
|
|
4
|
+
setDisk,
|
|
5
|
+
fakeDisk,
|
|
6
|
+
restoreDisk,
|
|
7
|
+
serveStorage,
|
|
8
|
+
signStorageUrl,
|
|
9
|
+
verifyStorageUrl,
|
|
10
|
+
contentTypeFor,
|
|
11
|
+
MemoryDisk,
|
|
12
|
+
Storage,
|
|
13
|
+
HttpKernel,
|
|
14
|
+
type Application,
|
|
15
|
+
type Disk,
|
|
16
|
+
type FileMetadata,
|
|
17
|
+
} from "@shaferllc/keel/core";
|
|
18
|
+
|
|
19
|
+
declare const bytes: Uint8Array;
|
|
20
|
+
declare const data: string;
|
|
21
|
+
declare const svg: string;
|
|
22
|
+
|
|
23
|
+
export async function usage() {
|
|
24
|
+
setDisk(new MemoryDisk());
|
|
25
|
+
await storage().put("avatars/1.png", bytes);
|
|
26
|
+
const got = await storage().get("avatars/1.png");
|
|
27
|
+
const text = await storage().getText("notes/todo.md");
|
|
28
|
+
const has = await storage().exists("avatars/1.png");
|
|
29
|
+
await storage().delete("avatars/1.png");
|
|
30
|
+
const files = await storage().list("avatars/");
|
|
31
|
+
const url = storage().url("avatars/1.png");
|
|
32
|
+
return { got, text, has, files, url };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function writing() {
|
|
36
|
+
// The content type is inferred from the extension — this is stored as image/png.
|
|
37
|
+
await storage().put("avatars/1.png", bytes);
|
|
38
|
+
|
|
39
|
+
// ...or set it, along with the rest of the object's metadata.
|
|
40
|
+
await storage().put("exports/report.csv", data, {
|
|
41
|
+
contentType: "text/csv",
|
|
42
|
+
cacheControl: "public, max-age=3600",
|
|
43
|
+
visibility: "private",
|
|
44
|
+
metadata: { uploadedBy: "42" },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return contentTypeFor("avatars/1.png"); // "image/png"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function inspecting() {
|
|
51
|
+
const meta: FileMetadata | null = await storage().metadata("avatars/1.png");
|
|
52
|
+
const size = await storage().size("avatars/1.png");
|
|
53
|
+
|
|
54
|
+
await storage().copy("avatars/1.png", "avatars/1-backup.png");
|
|
55
|
+
await storage().move("tmp/upload.png", "avatars/2.png");
|
|
56
|
+
|
|
57
|
+
return { meta, size };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function signing() {
|
|
61
|
+
// A temporary URL for a private file.
|
|
62
|
+
const url = await storage().signedUrl("invoices/42.pdf", { expiresIn: 300 });
|
|
63
|
+
|
|
64
|
+
// A URL the browser PUTs to directly — the bytes never transit the app.
|
|
65
|
+
const upload = await storage().signedUploadUrl("uploads/clip.mp4", {
|
|
66
|
+
expiresIn: 600,
|
|
67
|
+
contentType: "video/mp4",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Sign and verify any URL yourself.
|
|
71
|
+
const signed = await signStorageUrl("/storage/invoices/42.pdf", 300);
|
|
72
|
+
const valid = await verifyStorageUrl(signed);
|
|
73
|
+
|
|
74
|
+
return { url, upload, valid };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class Kernel extends HttpKernel {
|
|
78
|
+
constructor(app: Application) {
|
|
79
|
+
super(app);
|
|
80
|
+
this.use(serveStorage()); // public files under /storage
|
|
81
|
+
this.use(serveStorage({ disk: "r2", basePath: "/private", signed: true, maxAge: 60 }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function testing() {
|
|
86
|
+
const disk = fakeDisk(); // swap the real disk for an in-memory one
|
|
87
|
+
|
|
88
|
+
await storage().put("avatars/1.png", bytes);
|
|
89
|
+
|
|
90
|
+
await disk.assertExists("avatars/1.png");
|
|
91
|
+
await disk.assertMissing("avatars/2.png");
|
|
92
|
+
await disk.assertContents("notes/todo.md", "buy milk");
|
|
93
|
+
await disk.assertCount(1, "avatars/");
|
|
94
|
+
|
|
95
|
+
restoreDisk();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function named(local: Disk, r2: Disk) {
|
|
99
|
+
setDisk(local, "local");
|
|
100
|
+
setDisk(r2, "r2");
|
|
101
|
+
return Promise.all([
|
|
102
|
+
storage("local").put("cache/x", data),
|
|
103
|
+
storage("r2").put("public/logo.svg", svg),
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A minimal custom disk (the shape of the local/R2 examples)
|
|
108
|
+
function makeDisk(): Disk {
|
|
109
|
+
const store = new Map<string, Uint8Array>();
|
|
110
|
+
return {
|
|
111
|
+
async put(path, b) {
|
|
112
|
+
store.set(path, b);
|
|
113
|
+
},
|
|
114
|
+
async get(path) {
|
|
115
|
+
return store.get(path) ?? null;
|
|
116
|
+
},
|
|
117
|
+
async exists(path) {
|
|
118
|
+
return store.has(path);
|
|
119
|
+
},
|
|
120
|
+
async delete(path) {
|
|
121
|
+
store.delete(path);
|
|
122
|
+
},
|
|
123
|
+
async list(prefix = "") {
|
|
124
|
+
return [...store.keys()].filter((k) => k.startsWith(prefix));
|
|
125
|
+
},
|
|
126
|
+
url: (path) => `https://cdn.example/${path}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function wrap(): Storage {
|
|
131
|
+
return new Storage(makeDisk());
|
|
132
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Type-check harness for docs/templates.md. Every type-checkable snippet is
|
|
2
|
+
// exercised here against the real exports, so a renamed method or wrong argument
|
|
3
|
+
// type fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
templates,
|
|
6
|
+
render,
|
|
7
|
+
setTemplateEngine,
|
|
8
|
+
escapeHtml,
|
|
9
|
+
TemplateEngine,
|
|
10
|
+
html,
|
|
11
|
+
type Filter,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const user: { name: string; admin: boolean };
|
|
15
|
+
declare function readFile(path: string, enc: string): Promise<string>;
|
|
16
|
+
|
|
17
|
+
export async function rendering() {
|
|
18
|
+
templates().register("greeting", "Hello, {{ name }}!");
|
|
19
|
+
const out = await render("greeting", { name: "Ada" });
|
|
20
|
+
return html(out);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function registerAll() {
|
|
24
|
+
templates().registerAll({
|
|
25
|
+
layout: await readFile("views/layout.html", "utf8"),
|
|
26
|
+
home: await readFile("views/home.html", "utf8"),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function customFilter() {
|
|
31
|
+
templates().filter("currency", (v, code) =>
|
|
32
|
+
new Intl.NumberFormat("en-US", { style: "currency", currency: String(code) }).format(Number(v)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function globals() {
|
|
37
|
+
templates()
|
|
38
|
+
.global("appName", "Keel")
|
|
39
|
+
.global("asset", (path: string) => `/static/${path}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ownEngine() {
|
|
43
|
+
const engine = new TemplateEngine();
|
|
44
|
+
engine.register("home", "…");
|
|
45
|
+
engine.registerAll({ a: "A", b: "B" });
|
|
46
|
+
engine.has("home");
|
|
47
|
+
engine.global("appName", "Keel");
|
|
48
|
+
engine.filter("upper", (v) => String(v).toUpperCase());
|
|
49
|
+
setTemplateEngine(engine);
|
|
50
|
+
return engine.render("home", { user });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Interfaces & types
|
|
54
|
+
const filter: Filter = (value, ...args) => `${String(value)}${args.length}`;
|
|
55
|
+
|
|
56
|
+
export function escaping() {
|
|
57
|
+
return [escapeHtml("<b>&\"'</b>"), escapeHtml(null), escapeHtml(42), filter("x", 1)];
|
|
58
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Type-check harness for docs/testing.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
Application,
|
|
4
|
+
Router,
|
|
5
|
+
HttpKernel,
|
|
6
|
+
json,
|
|
7
|
+
testClient,
|
|
8
|
+
sessionMiddleware,
|
|
9
|
+
requestLogger,
|
|
10
|
+
type TestClient,
|
|
11
|
+
type TestResponse,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const body: unknown;
|
|
15
|
+
|
|
16
|
+
type User = { id: number; email: string };
|
|
17
|
+
|
|
18
|
+
async function makeApp() {
|
|
19
|
+
const app = new Application();
|
|
20
|
+
await app.boot([], { discoverConfig: false, config: { app: {} } });
|
|
21
|
+
app.make(Router).get("/health", () => json({ ok: true }));
|
|
22
|
+
return app;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function basic() {
|
|
26
|
+
const client: TestClient = testClient(await makeApp());
|
|
27
|
+
const res: TestResponse = await client.get("/health");
|
|
28
|
+
res.assertStatus(200).assertJson({ ok: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function requests(client: TestClient) {
|
|
32
|
+
await client.get("/users");
|
|
33
|
+
await client.get("/users?active=true");
|
|
34
|
+
await client.post("/users", { email: "a@b.com", name: "Ada" });
|
|
35
|
+
await client.put("/users/1", { name: "Grace" });
|
|
36
|
+
await client.delete("/users/1");
|
|
37
|
+
await client.request("/users", { method: "POST", headers: { authorization: "Bearer x" }, body: JSON.stringify(body) });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function responseShape(client: TestClient) {
|
|
41
|
+
const res = await client.get("/user");
|
|
42
|
+
const s: number = res.status;
|
|
43
|
+
const ct = res.header("content-type");
|
|
44
|
+
const t: string = res.text();
|
|
45
|
+
const u = res.json<User>();
|
|
46
|
+
return { s, ct, t, u, raw: res.raw };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function assertions(client: TestClient) {
|
|
50
|
+
const res = await client.post("/users", body);
|
|
51
|
+
res
|
|
52
|
+
.assertStatus(201)
|
|
53
|
+
.assertOk()
|
|
54
|
+
.assertJson({ id: 2 })
|
|
55
|
+
.assertText("")
|
|
56
|
+
.assertHeader("content-type", "application/json")
|
|
57
|
+
.assertRedirect("/login");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function withMiddleware() {
|
|
61
|
+
const app = await makeApp();
|
|
62
|
+
const kernel = new HttpKernel(app);
|
|
63
|
+
kernel.use(sessionMiddleware());
|
|
64
|
+
kernel.use(requestLogger());
|
|
65
|
+
return testClient(kernel);
|
|
66
|
+
}
|