@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,81 @@
|
|
|
1
|
+
// Type-check harness for docs/inertia.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
|
+
ServiceProvider,
|
|
6
|
+
singleton,
|
|
7
|
+
Inertia,
|
|
8
|
+
inertia,
|
|
9
|
+
inertiaPageAttr,
|
|
10
|
+
type InertiaPage,
|
|
11
|
+
type InertiaOptions,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const user: { id: number; name: string };
|
|
15
|
+
declare const stats: unknown;
|
|
16
|
+
declare const notifications: unknown;
|
|
17
|
+
declare const activity: unknown;
|
|
18
|
+
declare const page: InertiaPage;
|
|
19
|
+
declare const rootView: (page: InertiaPage) => string;
|
|
20
|
+
|
|
21
|
+
// Configure it
|
|
22
|
+
export class InertiaServiceProvider extends ServiceProvider {
|
|
23
|
+
register(): void {
|
|
24
|
+
singleton(
|
|
25
|
+
Inertia,
|
|
26
|
+
() =>
|
|
27
|
+
new Inertia({
|
|
28
|
+
version: "1",
|
|
29
|
+
rootView: (page) =>
|
|
30
|
+
`<!DOCTYPE html><html><head><meta charset="utf-8"></head>` +
|
|
31
|
+
`<body><div id="app" data-page="${inertiaPageAttr(page)}"></div>` +
|
|
32
|
+
`<script src="/assets/app.js"></script></body></html>`,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Render a page
|
|
39
|
+
export function render() {
|
|
40
|
+
const a: Response | string = inertia("Users/Show", { user });
|
|
41
|
+
const b: Response | string = inertia("Dashboard");
|
|
42
|
+
return { a, b };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Partial reload example
|
|
46
|
+
export function partial() {
|
|
47
|
+
return inertia("Dashboard", { stats, notifications, activity });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Reference: inertiaPageAttr
|
|
51
|
+
export function attr() {
|
|
52
|
+
return `<div id="app" data-page="${inertiaPageAttr(page)}"></div>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reference: Inertia class
|
|
56
|
+
export function classUsage() {
|
|
57
|
+
const i = new Inertia({
|
|
58
|
+
version: "1",
|
|
59
|
+
rootView: (page) => `<div id="app" data-page="${inertiaPageAttr(page)}"></div>`,
|
|
60
|
+
});
|
|
61
|
+
const html: Response | string = i.render("Dashboard", { title: "Welcome" });
|
|
62
|
+
const noProps: Response | string = i.render("Dashboard");
|
|
63
|
+
return { html, noProps };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Interface / type seams
|
|
67
|
+
const options: InertiaOptions = {
|
|
68
|
+
version: "abc123",
|
|
69
|
+
rootView: (page) => `<div id="app" data-page="${inertiaPageAttr(page)}"></div>`,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const pageSeam: InertiaPage = {
|
|
73
|
+
component: "Users/Show",
|
|
74
|
+
props: { user },
|
|
75
|
+
url: "/users/1",
|
|
76
|
+
version: "1",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const _rootView = rootView;
|
|
80
|
+
|
|
81
|
+
export { options, pageSeam, _rootView };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Type-check harness for docs/locks.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
lock,
|
|
4
|
+
restoreLock,
|
|
5
|
+
setLockStore,
|
|
6
|
+
getLockStore,
|
|
7
|
+
Lock,
|
|
8
|
+
MemoryLockStore,
|
|
9
|
+
LockNotHeldError,
|
|
10
|
+
type LockStore,
|
|
11
|
+
type AcquireOptions,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const invoice: { id: number };
|
|
15
|
+
declare function charge(invoice: { id: number }): Promise<void>;
|
|
16
|
+
declare function rebuild(): Promise<void>;
|
|
17
|
+
declare function process(batch: unknown): Promise<void>;
|
|
18
|
+
declare const batches: unknown[];
|
|
19
|
+
|
|
20
|
+
export async function running() {
|
|
21
|
+
const [ran, result] = await lock("invoice:42").run(() => charge(invoice));
|
|
22
|
+
if (!ran) return undefined;
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function waiting() {
|
|
27
|
+
const options: AcquireOptions = { timeout: 5_000, retryDelay: 100 };
|
|
28
|
+
const [ran] = await lock("report").run(() => rebuild(), options);
|
|
29
|
+
|
|
30
|
+
const [immediate] = await lock("report").runImmediately(() => rebuild());
|
|
31
|
+
return { ran, immediate };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ttlAndExtend() {
|
|
35
|
+
const l = lock("import", 60_000);
|
|
36
|
+
await l.run(async () => {
|
|
37
|
+
for (const batch of batches) {
|
|
38
|
+
await process(batch);
|
|
39
|
+
await l.extend();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function manualAcquisition() {
|
|
45
|
+
const l = lock("invoice:42");
|
|
46
|
+
if (!(await l.acquire({ timeout: 2_000 }))) return;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await charge(invoice);
|
|
50
|
+
} finally {
|
|
51
|
+
await l.release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function handoff(): Promise<string> {
|
|
56
|
+
const l = lock("import:99", 60_000);
|
|
57
|
+
await l.acquire();
|
|
58
|
+
return l.serialize();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function resume(serialized: string) {
|
|
62
|
+
const l = restoreLock(serialized);
|
|
63
|
+
await l.extend(60_000);
|
|
64
|
+
await l.release();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function inspecting() {
|
|
68
|
+
const l = lock("k");
|
|
69
|
+
return {
|
|
70
|
+
locked: await l.isLocked(),
|
|
71
|
+
expired: await l.isExpired(),
|
|
72
|
+
remaining: await l.getRemainingTime(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function testing() {
|
|
77
|
+
setLockStore(new MemoryLockStore());
|
|
78
|
+
return getLockStore();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function errors(err: unknown) {
|
|
82
|
+
return err instanceof LockNotHeldError;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// A custom store (the shape of the Redis / database recipes).
|
|
86
|
+
export function customStore(): LockStore {
|
|
87
|
+
const entries = new Map<string, { owner: string; expiresAt: number }>();
|
|
88
|
+
return {
|
|
89
|
+
async acquire(key, owner, ttlMs) {
|
|
90
|
+
const existing = entries.get(key);
|
|
91
|
+
if (existing && existing.expiresAt > Date.now()) return false;
|
|
92
|
+
entries.set(key, { owner, expiresAt: Date.now() + ttlMs });
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
async release(key, owner) {
|
|
96
|
+
const entry = entries.get(key);
|
|
97
|
+
if (!entry || entry.owner !== owner) return false;
|
|
98
|
+
entries.delete(key);
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
async extend(key, owner, ttlMs) {
|
|
102
|
+
const entry = entries.get(key);
|
|
103
|
+
if (!entry || entry.owner !== owner) return false;
|
|
104
|
+
entry.expiresAt = Date.now() + ttlMs;
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
async isLocked(key) {
|
|
108
|
+
const entry = entries.get(key);
|
|
109
|
+
return !!entry && entry.expiresAt > Date.now();
|
|
110
|
+
},
|
|
111
|
+
async remainingTime(key) {
|
|
112
|
+
const entry = entries.get(key);
|
|
113
|
+
return entry ? entry.expiresAt - Date.now() : null;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function direct(): Lock {
|
|
119
|
+
return new Lock("k", 1_000, new MemoryLockStore());
|
|
120
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Type-check harness for docs/logger.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
|
+
logger,
|
|
6
|
+
Logger,
|
|
7
|
+
requestLogger,
|
|
8
|
+
requestLog,
|
|
9
|
+
HttpKernel,
|
|
10
|
+
type LoggerOptions,
|
|
11
|
+
type LogLevel,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
declare const user: { id: number };
|
|
15
|
+
declare const key: string;
|
|
16
|
+
declare const orderId: number;
|
|
17
|
+
declare const err: unknown;
|
|
18
|
+
declare const sql: string;
|
|
19
|
+
declare const ms: number;
|
|
20
|
+
declare const requestId: string;
|
|
21
|
+
declare const jobId: string;
|
|
22
|
+
|
|
23
|
+
export function logging() {
|
|
24
|
+
logger().info("user registered", { userId: user.id });
|
|
25
|
+
logger().warn("cache miss", { key });
|
|
26
|
+
logger().error("payment failed", { orderId, error: String(err) });
|
|
27
|
+
logger().debug("query", { sql, ms });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function levels() {
|
|
31
|
+
const threshold: LogLevel = "warn";
|
|
32
|
+
new Logger({ level: threshold });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function childLoggers() {
|
|
36
|
+
const log = logger().child({ requestId });
|
|
37
|
+
log.info("handling");
|
|
38
|
+
log.info("done");
|
|
39
|
+
|
|
40
|
+
const base = logger().child({ service: "billing" });
|
|
41
|
+
const job = base.child({ jobId });
|
|
42
|
+
job.info("start");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function constructing() {
|
|
46
|
+
const log = new Logger({ level: "debug", pretty: true, bindings: { env: "dev" } });
|
|
47
|
+
log.debug("boot", { pid: 1 });
|
|
48
|
+
|
|
49
|
+
const bare = new Logger();
|
|
50
|
+
bare.info("no options");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function reference() {
|
|
54
|
+
logger().info("ready");
|
|
55
|
+
|
|
56
|
+
const log = new Logger({ level: "warn", pretty: true });
|
|
57
|
+
log.debug("cache lookup", { key });
|
|
58
|
+
log.info("user registered", { userId: 42 });
|
|
59
|
+
log.warn("cache miss", { key });
|
|
60
|
+
log.error("payment failed", { orderId, error: String(err) });
|
|
61
|
+
|
|
62
|
+
const reqLog = logger().child({ requestId });
|
|
63
|
+
reqLog.info("handling");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function redaction() {
|
|
67
|
+
const log = new Logger({ redact: ["password", "req.headers.authorization"] });
|
|
68
|
+
log.info("login", {
|
|
69
|
+
user: "ada",
|
|
70
|
+
password: "s3cret",
|
|
71
|
+
req: { headers: { authorization: "Bearer x" } },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function perRequest(kernel: HttpKernel) {
|
|
76
|
+
kernel.use(requestLogger());
|
|
77
|
+
kernel.use(requestLogger({ idHeader: "x-request-id", logRequests: false }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inRequest() {
|
|
81
|
+
requestLog().info("charging card", { orderId: 1 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Interface / type seams
|
|
85
|
+
const options: LoggerOptions = {
|
|
86
|
+
level: "debug",
|
|
87
|
+
pretty: true,
|
|
88
|
+
bindings: { app: "api" },
|
|
89
|
+
redact: ["password"],
|
|
90
|
+
};
|
|
91
|
+
const level: LogLevel = "warn";
|
|
92
|
+
export { options, level };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Type-check harness for docs/mail.md. Every type-checkable snippet in the doc
|
|
2
|
+
// is exercised here against the real exports, so a renamed method or wrong
|
|
3
|
+
// argument type fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
mail,
|
|
6
|
+
setMailer,
|
|
7
|
+
getMailer,
|
|
8
|
+
fetchTransport,
|
|
9
|
+
Mailer,
|
|
10
|
+
PendingMail,
|
|
11
|
+
ArrayTransport,
|
|
12
|
+
LogTransport,
|
|
13
|
+
type Message,
|
|
14
|
+
type Transport,
|
|
15
|
+
type MailerOptions,
|
|
16
|
+
type FetchTransportOptions,
|
|
17
|
+
} from "@shaferllc/keel/core";
|
|
18
|
+
|
|
19
|
+
declare const env: (key: string) => string;
|
|
20
|
+
declare const apiKey: string;
|
|
21
|
+
declare const message: Message;
|
|
22
|
+
declare const myProviderSdk: { emails: { send(m: Message): Promise<void> } };
|
|
23
|
+
|
|
24
|
+
export async function sending() {
|
|
25
|
+
await mail()
|
|
26
|
+
.to("ada@example.com")
|
|
27
|
+
.subject("Welcome aboard")
|
|
28
|
+
.html("<h1>Hi Ada</h1>")
|
|
29
|
+
.send();
|
|
30
|
+
|
|
31
|
+
await mail()
|
|
32
|
+
.to("a@x.com", "b@x.com")
|
|
33
|
+
.cc("team@x.com")
|
|
34
|
+
.bcc("audit@x.com")
|
|
35
|
+
.replyTo("support@x.com")
|
|
36
|
+
.from("hello@x.com")
|
|
37
|
+
.subject("Report")
|
|
38
|
+
.text("Plain-text body")
|
|
39
|
+
.html("<p>HTML body</p>")
|
|
40
|
+
.header("X-Campaign", "weekly")
|
|
41
|
+
.send();
|
|
42
|
+
|
|
43
|
+
await mail().fill({ to: "a@x.com", subject: "Hi", text: "body" }).send();
|
|
44
|
+
|
|
45
|
+
const sent = await mail().to("ada@example.com").subject("Hi").text("hey").send();
|
|
46
|
+
return { from: sent.from, to: sent.to };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function configuring() {
|
|
50
|
+
setMailer(
|
|
51
|
+
fetchTransport({
|
|
52
|
+
url: "https://api.resend.com/emails",
|
|
53
|
+
headers: { Authorization: `Bearer ${env("RESEND_API_KEY")}` },
|
|
54
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
55
|
+
}),
|
|
56
|
+
{ from: "hello@myapp.com" },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ownTransport() {
|
|
61
|
+
const transport: Transport = {
|
|
62
|
+
async send(message) {
|
|
63
|
+
void message;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
setMailer(transport, { from: "hello@myapp.com" });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function inTests() {
|
|
70
|
+
const transport = new ArrayTransport();
|
|
71
|
+
setMailer(transport, { from: "hi@app.com" });
|
|
72
|
+
|
|
73
|
+
await mail().to("ada@example.com").subject("Welcome").text("hi").send();
|
|
74
|
+
|
|
75
|
+
return { count: transport.sent.length, subject: transport.sent[0]?.subject };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function scopedMailer() {
|
|
79
|
+
const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
|
|
80
|
+
await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ------------------------------- reference ---------------------------------
|
|
84
|
+
|
|
85
|
+
export async function referenceFunctions() {
|
|
86
|
+
await mail().to("ada@example.com").subject("Hi").text("hey").send();
|
|
87
|
+
|
|
88
|
+
const m1: Mailer = setMailer(fetchTransport({ url: "https://x/emails" }), {
|
|
89
|
+
from: "hello@myapp.com",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const mailer: Mailer = getMailer();
|
|
93
|
+
await mailer.message().to("ada@example.com").subject("Hi").text("hey").send();
|
|
94
|
+
|
|
95
|
+
const transport: Transport = fetchTransport({
|
|
96
|
+
url: "https://api.resend.com/emails",
|
|
97
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
98
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { m1, transport };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function referenceMailer() {
|
|
105
|
+
const mailer = new Mailer(new ArrayTransport(), { from: "hi@app.com" });
|
|
106
|
+
const pending: PendingMail = mailer.message();
|
|
107
|
+
const sent: Message = await mailer.send({
|
|
108
|
+
to: ["ada@x.com"],
|
|
109
|
+
subject: "Hi",
|
|
110
|
+
text: "hey",
|
|
111
|
+
});
|
|
112
|
+
return { pending, sent };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function referencePendingMail() {
|
|
116
|
+
mail().to("a@x.com", "b@x.com");
|
|
117
|
+
mail().from("hello@x.com");
|
|
118
|
+
mail().cc("team@x.com").bcc("audit@x.com");
|
|
119
|
+
mail().replyTo("support@x.com");
|
|
120
|
+
mail().subject("Welcome aboard");
|
|
121
|
+
mail().text("Plain body").html("<p>Rich body</p>");
|
|
122
|
+
mail().header("X-Campaign", "weekly");
|
|
123
|
+
mail().fill({ to: ["a@x.com", "b@x.com"], subject: "Hi", text: "body" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function referenceTransports() {
|
|
127
|
+
const transport = new ArrayTransport();
|
|
128
|
+
setMailer(transport);
|
|
129
|
+
transport.sent.length;
|
|
130
|
+
transport.sent[0]?.subject;
|
|
131
|
+
await new ArrayTransport().send(message);
|
|
132
|
+
|
|
133
|
+
setMailer(new LogTransport(), { from: "dev@localhost" });
|
|
134
|
+
await new LogTransport().send(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------- interface / type seams -----------------------
|
|
138
|
+
|
|
139
|
+
const seamMessage: Message = {
|
|
140
|
+
to: ["ada@x.com"],
|
|
141
|
+
from: "hi@app.com",
|
|
142
|
+
subject: "Hi",
|
|
143
|
+
text: "hey",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const seamTransport: Transport = {
|
|
147
|
+
async send(message) {
|
|
148
|
+
await myProviderSdk.emails.send(message);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const seamOptions: MailerOptions = { from: "hello@myapp.com" };
|
|
153
|
+
|
|
154
|
+
const seamFetchOptions: FetchTransportOptions = {
|
|
155
|
+
url: "https://api.resend.com/emails",
|
|
156
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
157
|
+
body: (m) => ({ from: m.from, to: m.to, subject: m.subject, html: m.html }),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export { seamMessage, seamTransport, seamOptions, seamFetchOptions };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Type-check harness for docs/middleware.md. Every type-checkable snippet in the
|
|
2
|
+
// guide is exercised here against the real exports, so a renamed method or wrong
|
|
3
|
+
// argument type fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
HttpKernel,
|
|
6
|
+
Application,
|
|
7
|
+
Router,
|
|
8
|
+
UnauthorizedException,
|
|
9
|
+
type MiddlewareRef,
|
|
10
|
+
type RouteHandler,
|
|
11
|
+
} from "@shaferllc/keel/core";
|
|
12
|
+
import type { MiddlewareHandler } from "hono";
|
|
13
|
+
|
|
14
|
+
// Externals the snippets reference.
|
|
15
|
+
declare const router: Router;
|
|
16
|
+
declare const handler: RouteHandler;
|
|
17
|
+
declare function lookupUser(auth?: string): Promise<{ id: number; name: string }>;
|
|
18
|
+
|
|
19
|
+
// Example middleware — the shape everything else registers.
|
|
20
|
+
export const requestLogger: MiddlewareHandler = async (c, next) => {
|
|
21
|
+
const start = performance.now();
|
|
22
|
+
await next();
|
|
23
|
+
const ms = (performance.now() - start).toFixed(1);
|
|
24
|
+
console.log(` ${c.req.method} ${c.req.path} → ${c.res.status} (${ms}ms)`);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const cors: MiddlewareHandler = async (_c, next) => {
|
|
28
|
+
await next();
|
|
29
|
+
};
|
|
30
|
+
const requestId: MiddlewareHandler = async (_c, next) => {
|
|
31
|
+
await next();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// The HTTP kernel — global middleware + custom error handler.
|
|
35
|
+
export class Kernel extends HttpKernel {
|
|
36
|
+
constructor(app: Application) {
|
|
37
|
+
super(app);
|
|
38
|
+
this.use(requestLogger).use(cors).use(requestId);
|
|
39
|
+
this.onError((err, c) => c.json({ error: String(err) }, 500));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildKernel(app: Application) {
|
|
44
|
+
const kernel = new Kernel(app);
|
|
45
|
+
return kernel.build();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Short-circuiting.
|
|
49
|
+
export const requireApiKey: MiddlewareHandler = async (c, next) => {
|
|
50
|
+
if (c.req.header("x-api-key") !== process.env.API_KEY) {
|
|
51
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
52
|
+
}
|
|
53
|
+
await next();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
class Auth {
|
|
57
|
+
check(): boolean {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const requireAuth: MiddlewareHandler = async (c, next) => {
|
|
62
|
+
if (!c.get("app").make(Auth).check()) throw new UnauthorizedException();
|
|
63
|
+
await next();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Sharing data with handlers.
|
|
67
|
+
export const withUser: MiddlewareHandler = async (c, next) => {
|
|
68
|
+
c.set("user", await lookupUser(c.req.header("authorization")));
|
|
69
|
+
await next();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const noCache: MiddlewareHandler = async (c, next) => {
|
|
73
|
+
await next();
|
|
74
|
+
c.header("Cache-Control", "no-store");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Parameterized middleware via a factory.
|
|
78
|
+
const role = (name: string): MiddlewareHandler => async (_c, next) => {
|
|
79
|
+
void name;
|
|
80
|
+
await next();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Named middleware + applying refs to routes, groups, resources.
|
|
84
|
+
class DashboardController {}
|
|
85
|
+
class PostController {}
|
|
86
|
+
class AdminController {}
|
|
87
|
+
class ReportController {}
|
|
88
|
+
const authMiddleware = requireAuth;
|
|
89
|
+
const adminMiddleware = requireAuth;
|
|
90
|
+
const auditLog = requestLogger;
|
|
91
|
+
|
|
92
|
+
export function named() {
|
|
93
|
+
router.named({ auth: authMiddleware, admin: adminMiddleware });
|
|
94
|
+
router.named({ admin: role("admin"), editor: role("editor") });
|
|
95
|
+
|
|
96
|
+
router.get("/dashboard", [DashboardController, "index"]).use("auth");
|
|
97
|
+
router.group(() => {
|
|
98
|
+
/* … */
|
|
99
|
+
}).use(["auth", "admin"]);
|
|
100
|
+
router.resource("posts", PostController).use(["store", "update"], "auth");
|
|
101
|
+
router.resource("admin", AdminController).use("*", "admin");
|
|
102
|
+
|
|
103
|
+
router.get("/reports", [ReportController, "index"]).use(["auth", auditLog]);
|
|
104
|
+
router.get("/admin", handler).use(role("admin"));
|
|
105
|
+
router.get("/reports2", handler).middleware(["auth", auditLog]);
|
|
106
|
+
|
|
107
|
+
const mw = router.resolveMiddleware("auth");
|
|
108
|
+
return mw;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function groupOrdering() {
|
|
112
|
+
router.group(() => {
|
|
113
|
+
router.get("/posts/:id/edit", handler).use("owns-post");
|
|
114
|
+
}).use("auth");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Type seams.
|
|
118
|
+
const guards: MiddlewareRef[] = ["auth", auditLog];
|
|
119
|
+
export { guards };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Type-check harness for docs/migrations.md. Every type-checkable snippet in the
|
|
2
|
+
// guide is exercised here against the real exports, so a renamed method or wrong
|
|
3
|
+
// argument type fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
Migrator,
|
|
6
|
+
SchemaBuilder,
|
|
7
|
+
TableBuilder,
|
|
8
|
+
Column,
|
|
9
|
+
type Migration,
|
|
10
|
+
type Connection,
|
|
11
|
+
type Dialect,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
// A mock connection. `as Connection` is needed because `select` is declared
|
|
15
|
+
// generic (`select<T = Row>(…): Promise<T[]>`) — see docs/database.md.
|
|
16
|
+
const connection = {
|
|
17
|
+
select: async () => [],
|
|
18
|
+
write: async () => ({ rowsAffected: 1, insertId: 1 }),
|
|
19
|
+
} as Connection;
|
|
20
|
+
|
|
21
|
+
export const migrations: Migration[] = [
|
|
22
|
+
{
|
|
23
|
+
name: "01_create_users",
|
|
24
|
+
up: (schema) =>
|
|
25
|
+
schema.createTable("users", (t) => {
|
|
26
|
+
t.id();
|
|
27
|
+
t.string("email").unique();
|
|
28
|
+
t.string("name");
|
|
29
|
+
t.boolean("active").default(true);
|
|
30
|
+
t.timestamps();
|
|
31
|
+
}),
|
|
32
|
+
down: (schema) => schema.dropTable("users"),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export async function columnTypes() {
|
|
37
|
+
await new SchemaBuilder(connection, "sqlite").createTable("posts", (t) => {
|
|
38
|
+
t.id();
|
|
39
|
+
t.integer("user_id");
|
|
40
|
+
t.string("slug", 120).unique();
|
|
41
|
+
t.text("body").nullable();
|
|
42
|
+
t.json("meta").nullable();
|
|
43
|
+
t.boolean("published").default(false);
|
|
44
|
+
t.timestamps();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function defaults(schema: SchemaBuilder) {
|
|
49
|
+
await schema.createTable("accounts", (t) => {
|
|
50
|
+
t.string("role").default("member");
|
|
51
|
+
t.boolean("active").default(true);
|
|
52
|
+
t.integer("retries").default(0);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function rawExamples(schema: SchemaBuilder) {
|
|
57
|
+
await schema.raw("CREATE INDEX idx_posts_user ON posts (user_id)");
|
|
58
|
+
await schema.raw("UPDATE users SET active = ? WHERE active IS NULL", [true]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runAndRollBack() {
|
|
62
|
+
const migrator = new Migrator(connection, "postgres");
|
|
63
|
+
|
|
64
|
+
const applied = await migrator.up(migrations);
|
|
65
|
+
const rolled = await migrator.down(migrations);
|
|
66
|
+
const names = await migrator.ran();
|
|
67
|
+
|
|
68
|
+
const pending = migrations.filter((m) => !names.includes(m.name));
|
|
69
|
+
return { applied, rolled, names, pending };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function migratorDefaults() {
|
|
73
|
+
const migrator = new Migrator(connection); // dialect defaults to "sqlite"
|
|
74
|
+
return migrator.ran();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SchemaBuilder reference
|
|
78
|
+
export async function schemaBuilderRef(schema: SchemaBuilder) {
|
|
79
|
+
await schema.createTable("users", (t) => {
|
|
80
|
+
t.id();
|
|
81
|
+
t.string("email").unique();
|
|
82
|
+
t.timestamps();
|
|
83
|
+
});
|
|
84
|
+
await schema.dropTable("users");
|
|
85
|
+
await schema.raw("CREATE INDEX idx ON users (email)");
|
|
86
|
+
await schema.raw("UPDATE users SET active = ? WHERE id = ?", [true, 1]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// TableBuilder reference
|
|
90
|
+
export function tableBuilderRef() {
|
|
91
|
+
const t = new TableBuilder();
|
|
92
|
+
t.id();
|
|
93
|
+
t.id("uuid");
|
|
94
|
+
t.string("email");
|
|
95
|
+
t.string("slug", 120);
|
|
96
|
+
t.text("body");
|
|
97
|
+
t.integer("user_id");
|
|
98
|
+
t.bigInteger("view_count");
|
|
99
|
+
t.boolean("active").default(true);
|
|
100
|
+
t.timestamp("published_at").nullable();
|
|
101
|
+
t.json("meta").nullable();
|
|
102
|
+
t.timestamps();
|
|
103
|
+
const cols: Column[] = t.columns;
|
|
104
|
+
const sql: string = t.toCreateSql("users", "postgres");
|
|
105
|
+
return { cols, sql };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Column reference
|
|
109
|
+
export function columnRef(t: TableBuilder) {
|
|
110
|
+
t.text("bio").nullable();
|
|
111
|
+
t.string("email").unique();
|
|
112
|
+
t.boolean("active").default(true);
|
|
113
|
+
t.string("role").default("member");
|
|
114
|
+
const fragment: string = new Column("email", "string").unique().toSql("sqlite");
|
|
115
|
+
return fragment;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Migration type seam
|
|
119
|
+
export const indexMigration: Migration = {
|
|
120
|
+
name: "03_add_index",
|
|
121
|
+
up: (s) => s.raw("CREATE INDEX idx_users_email ON users (email)"),
|
|
122
|
+
down: (s) => s.raw("DROP INDEX idx_users_email"),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const d: Dialect = "mysql";
|
|
126
|
+
export { d };
|