@shaferllc/keel 0.66.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Type-check harness for docs/errors.md. Every type-checkable snippet in the
|
|
2
|
+
// reference is exercised here against the real exports, so a renamed field or a
|
|
3
|
+
// wrong constructor signature fails `npm run typecheck:docs`. Compile-only —
|
|
4
|
+
// never executed.
|
|
5
|
+
import {
|
|
6
|
+
HttpException,
|
|
7
|
+
NotFoundException,
|
|
8
|
+
UnauthorizedException,
|
|
9
|
+
ForbiddenException,
|
|
10
|
+
ValidationException,
|
|
11
|
+
createError,
|
|
12
|
+
STATUS_TEXT,
|
|
13
|
+
HttpKernel,
|
|
14
|
+
Application,
|
|
15
|
+
} from "@shaferllc/keel/core";
|
|
16
|
+
import type { Context } from "hono";
|
|
17
|
+
|
|
18
|
+
// createError --------------------------------------------------------------
|
|
19
|
+
export const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
|
|
20
|
+
export const TenantSuspended = createError("E_TENANT_SUSPENDED", "Tenant %s is suspended.", 403);
|
|
21
|
+
export const RateExceeded = createError("E_RATE", "Slow down.", 429);
|
|
22
|
+
|
|
23
|
+
export function throwCoded(): never {
|
|
24
|
+
const err = new InsufficientFunds("$40");
|
|
25
|
+
const isHttp: boolean = err instanceof HttpException;
|
|
26
|
+
void isHttp;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// HTTP exceptions ----------------------------------------------------------
|
|
31
|
+
export function httpExceptions(): never {
|
|
32
|
+
throw new NotFoundException("Widget not found"); // 404
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function moreThrows() {
|
|
36
|
+
const throwers: Array<() => never> = [
|
|
37
|
+
() => {
|
|
38
|
+
throw new UnauthorizedException();
|
|
39
|
+
},
|
|
40
|
+
() => {
|
|
41
|
+
throw new ForbiddenException();
|
|
42
|
+
},
|
|
43
|
+
() => {
|
|
44
|
+
throw new HttpException(429, "Slow down");
|
|
45
|
+
},
|
|
46
|
+
() => {
|
|
47
|
+
throw new HttpException(503, "Down for maintenance", { "Retry-After": "120" });
|
|
48
|
+
},
|
|
49
|
+
() => {
|
|
50
|
+
throw new HttpException(429, "Slow down", { "Retry-After": "30" });
|
|
51
|
+
},
|
|
52
|
+
() => {
|
|
53
|
+
throw new HttpException(409, "That email is taken");
|
|
54
|
+
},
|
|
55
|
+
() => {
|
|
56
|
+
throw new HttpException(429); // message defaults to "Too Many Requests"
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
return throwers;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validation ---------------------------------------------------------------
|
|
63
|
+
export function validation(): never {
|
|
64
|
+
throw new ValidationException({
|
|
65
|
+
email: ["The email is invalid."],
|
|
66
|
+
password: ["Too short.", "Must contain a number."],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readsErrors() {
|
|
71
|
+
const ex = new ValidationException({ email: ["nope"] });
|
|
72
|
+
const fields: Record<string, string[]> = ex.errors;
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fields the kernel reads --------------------------------------------------
|
|
77
|
+
export function fields() {
|
|
78
|
+
const ex = new HttpException(503, "Down", { "Retry-After": "120" });
|
|
79
|
+
const status: number = ex.status;
|
|
80
|
+
const headers: Record<string, string> | undefined = ex.headers;
|
|
81
|
+
ex.code = "E_DOWN";
|
|
82
|
+
const code: string | undefined = ex.code;
|
|
83
|
+
return { status, headers, code };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Custom exception with self-render / self-report --------------------------
|
|
87
|
+
declare const metrics: { increment(name: string): void };
|
|
88
|
+
|
|
89
|
+
export class PaymentRequiredException extends HttpException {
|
|
90
|
+
code = "E_PAYMENT_REQUIRED";
|
|
91
|
+
|
|
92
|
+
constructor() {
|
|
93
|
+
super(402, "Payment is required to continue.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
handle(c: Context) {
|
|
97
|
+
// hono's `json` wants a status-code literal; the exception's numeric
|
|
98
|
+
// `status` is asserted to it (402 here).
|
|
99
|
+
return c.json({ error: this.message, code: this.code, upgrade: "/billing" }, this.status as 402);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
report() {
|
|
103
|
+
metrics.increment("payment_required");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Custom error handler on the kernel ---------------------------------------
|
|
108
|
+
export class Kernel extends HttpKernel {
|
|
109
|
+
constructor(app: Application) {
|
|
110
|
+
super(app);
|
|
111
|
+
this.onError((_err, c) => c.json({ oops: true }, 500));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// STATUS_TEXT --------------------------------------------------------------
|
|
116
|
+
export function statusText() {
|
|
117
|
+
const notFound: string | undefined = STATUS_TEXT[404]; // "Not Found"
|
|
118
|
+
const expired: string | undefined = STATUS_TEXT[419]; // "Page Expired"
|
|
119
|
+
const fallback: string = STATUS_TEXT[418] ?? "Error"; // not in the map
|
|
120
|
+
return { notFound, expired, fallback };
|
|
121
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Type-check harness for docs/events.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
|
+
emit,
|
|
6
|
+
listen,
|
|
7
|
+
events,
|
|
8
|
+
Events,
|
|
9
|
+
EventBuffer,
|
|
10
|
+
ServiceProvider,
|
|
11
|
+
type Listener,
|
|
12
|
+
type RecordedEvent,
|
|
13
|
+
} from "@shaferllc/keel/core";
|
|
14
|
+
|
|
15
|
+
declare const user: { id: number };
|
|
16
|
+
declare function sendWelcomeEmail(u: unknown): void;
|
|
17
|
+
declare function step1(): Promise<void>;
|
|
18
|
+
declare function step2(): Promise<void>;
|
|
19
|
+
declare function audit(id: number): void;
|
|
20
|
+
declare function fulfill(id: number): Promise<void>;
|
|
21
|
+
declare const handler: Listener;
|
|
22
|
+
declare const listener: Listener;
|
|
23
|
+
declare class Fulfillment {
|
|
24
|
+
ship(order: unknown): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function listenAndEmit() {
|
|
28
|
+
listen("user.registered", (u) => {
|
|
29
|
+
sendWelcomeEmail(u);
|
|
30
|
+
});
|
|
31
|
+
await emit("user.registered", user);
|
|
32
|
+
|
|
33
|
+
const off = listen("tick", () => {});
|
|
34
|
+
off();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function typedPayloads() {
|
|
38
|
+
type OrderPaid = { id: number; total: number };
|
|
39
|
+
|
|
40
|
+
listen<OrderPaid>("order.paid", (order) => {
|
|
41
|
+
order.total; // number
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await emit<OrderPaid>("order.paid", { id: 1, total: 4200 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- The typed event registry ---
|
|
48
|
+
|
|
49
|
+
declare module "@shaferllc/keel/core" {
|
|
50
|
+
interface EventsList {
|
|
51
|
+
"invoice.settled": { id: number; cents: number };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function typedRegistry() {
|
|
56
|
+
// The payload type comes from the registry — no type argument needed.
|
|
57
|
+
listen("invoice.settled", (invoice) => {
|
|
58
|
+
invoice.cents; // number
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await emit("invoice.settled", { id: 1, cents: 4200 });
|
|
62
|
+
|
|
63
|
+
// @ts-expect-error — `cents` must be a number, and the registry now enforces it.
|
|
64
|
+
await emit("invoice.settled", { id: 1, cents: "4200" });
|
|
65
|
+
|
|
66
|
+
// @ts-expect-error — a declared event can't be fired without its payload.
|
|
67
|
+
await emit("invoice.settled");
|
|
68
|
+
|
|
69
|
+
// @ts-expect-error — the listener's payload is checked against the registry too.
|
|
70
|
+
listen("invoice.settled", (invoice: { nope: boolean }) => invoice.nope);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class EventServiceProvider extends ServiceProvider {
|
|
74
|
+
boot(): void {
|
|
75
|
+
listen("order.paid", (order) => this.app.make(Fulfillment).ship(order));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function fullApi() {
|
|
80
|
+
events().once("boot", () => {});
|
|
81
|
+
events().off("tick", listener);
|
|
82
|
+
events().listenerCount("tick");
|
|
83
|
+
events().clear("tick");
|
|
84
|
+
events().clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function orderingAndAwaiting() {
|
|
88
|
+
listen("deploy", async () => { await step1(); });
|
|
89
|
+
listen("deploy", async () => { await step2(); });
|
|
90
|
+
await emit("deploy");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function errorBehavior() {
|
|
94
|
+
try {
|
|
95
|
+
await emit("user.registered", user);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
void err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Events methods ---
|
|
102
|
+
|
|
103
|
+
export function eventsOn() {
|
|
104
|
+
const off = events().on<{ id: number }>("user.deleted", (u) => audit(u.id));
|
|
105
|
+
off();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function eventsOnce() {
|
|
109
|
+
events().once("boot", () => console.log("started"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function eventsOff() {
|
|
113
|
+
events().off("tick", handler);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function eventsEmit() {
|
|
117
|
+
await events().emit("order.paid", { id: 1, total: 4200 });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function eventsListenerCount() {
|
|
121
|
+
if (events().listenerCount("tick") === 0) {
|
|
122
|
+
/* startClock() */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function eventsClear() {
|
|
127
|
+
events().clear("tick");
|
|
128
|
+
events().clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Global helpers ---
|
|
132
|
+
|
|
133
|
+
export function helperEvents() {
|
|
134
|
+
events().once("boot", () => {});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function helperEmit() {
|
|
138
|
+
await emit("user.registered", user);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function helperListen() {
|
|
142
|
+
const off = listen<{ id: number }>("user.deleted", (u) => audit(u.id));
|
|
143
|
+
return off;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Interfaces & types ---
|
|
147
|
+
|
|
148
|
+
const onPaid: Listener<{ id: number }> = async (order) => {
|
|
149
|
+
await fulfill(order.id);
|
|
150
|
+
};
|
|
151
|
+
listen("order.paid", onPaid);
|
|
152
|
+
|
|
153
|
+
// The Events class is resolved via events(); referenced here for completeness.
|
|
154
|
+
export type EventsType = Events;
|
|
155
|
+
|
|
156
|
+
// --- onError / onAny ---
|
|
157
|
+
|
|
158
|
+
declare function log(event: string, payload: unknown): void;
|
|
159
|
+
|
|
160
|
+
export function errorHandling() {
|
|
161
|
+
events().onError((event, error, payload) => {
|
|
162
|
+
void event;
|
|
163
|
+
void error;
|
|
164
|
+
void payload;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function observeEveryEvent() {
|
|
169
|
+
const off = events().onAny((event, payload) => log(event, payload));
|
|
170
|
+
off();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function clearEverything() {
|
|
174
|
+
events().clearAll();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Faking ---
|
|
178
|
+
|
|
179
|
+
export async function faking() {
|
|
180
|
+
const buffer = events().fake();
|
|
181
|
+
|
|
182
|
+
await emit("invoice.settled", { id: 1, cents: 4200 });
|
|
183
|
+
|
|
184
|
+
buffer.assertEmitted("invoice.settled");
|
|
185
|
+
buffer.assertEmitted("invoice.settled", (invoice) => invoice.cents === 4200);
|
|
186
|
+
buffer.assertEmittedCount("invoice.settled", 1);
|
|
187
|
+
buffer.assertNotEmitted("user.deleted");
|
|
188
|
+
|
|
189
|
+
const all: RecordedEvent[] = buffer.all();
|
|
190
|
+
const payloads = buffer.payloadsFor("invoice.settled"); // typed via the registry
|
|
191
|
+
payloads[0]?.cents;
|
|
192
|
+
|
|
193
|
+
events().restore();
|
|
194
|
+
return { all, payloads };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function fakeSomeEvents() {
|
|
198
|
+
events().fake("invoice.settled");
|
|
199
|
+
events().fake(["invoice.settled", "tick"]);
|
|
200
|
+
events().fake().assertNoneEmitted();
|
|
201
|
+
events().restore();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type BufferType = EventBuffer;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Type-check harness for docs/factories.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
factory,
|
|
4
|
+
Faker,
|
|
5
|
+
Seeder,
|
|
6
|
+
seed,
|
|
7
|
+
Model,
|
|
8
|
+
setConnection,
|
|
9
|
+
ModelFactory,
|
|
10
|
+
type Definition,
|
|
11
|
+
type Connection,
|
|
12
|
+
} from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
class User extends Model {
|
|
15
|
+
static table = "users";
|
|
16
|
+
declare id: number;
|
|
17
|
+
}
|
|
18
|
+
class Post extends Model {
|
|
19
|
+
static table = "posts";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare const myConnection: Connection;
|
|
23
|
+
declare const testConnection: Connection;
|
|
24
|
+
|
|
25
|
+
const users = factory(User, (f, i) => ({ name: f.name(), email: `u${i}@x.com` }));
|
|
26
|
+
|
|
27
|
+
export async function building() {
|
|
28
|
+
users.make();
|
|
29
|
+
users.make({ name: "Ada" });
|
|
30
|
+
await users.create();
|
|
31
|
+
await users.create({ role: "admin" });
|
|
32
|
+
const ten = await users.count(10).create();
|
|
33
|
+
const drafts = users.count(3).make();
|
|
34
|
+
users.usingFaker(new Faker(42));
|
|
35
|
+
return { ten, drafts };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function faker() {
|
|
39
|
+
const f = new Faker(42);
|
|
40
|
+
return [
|
|
41
|
+
f.name(),
|
|
42
|
+
f.firstName(),
|
|
43
|
+
f.lastName(),
|
|
44
|
+
f.email(),
|
|
45
|
+
f.word(),
|
|
46
|
+
f.words(3),
|
|
47
|
+
f.sentence(),
|
|
48
|
+
f.paragraph(),
|
|
49
|
+
f.number(1, 100),
|
|
50
|
+
f.boolean(),
|
|
51
|
+
f.pick(["a", "b", "c"]),
|
|
52
|
+
f.slug(),
|
|
53
|
+
f.uuid(),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class UserSeeder extends Seeder {
|
|
58
|
+
async run() {
|
|
59
|
+
await factory(User, (f) => ({ name: f.name() })).count(10).create();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class DatabaseSeeder extends Seeder {
|
|
63
|
+
async run() {
|
|
64
|
+
await this.call([UserSeeder]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function seeding() {
|
|
69
|
+
setConnection(myConnection, "postgres");
|
|
70
|
+
await seed(DatabaseSeeder);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function inTests() {
|
|
74
|
+
setConnection(testConnection, "sqlite");
|
|
75
|
+
const created = await factory(User, (f) => ({ name: f.name() })).count(1).create();
|
|
76
|
+
const author = (created as User[])[0]!;
|
|
77
|
+
await factory(Post, (f) => ({ title: f.sentence() })).create({ user_id: author.id });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// type seams
|
|
81
|
+
const def: Definition<User> = (f, i) => ({ name: f.name(), i });
|
|
82
|
+
const own = factory(User, (f) => ({ name: f.name() }));
|
|
83
|
+
export type UserFactory = ModelFactory<User>;
|
|
84
|
+
export { def, own };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Type-check harness for docs/hashing.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 { hash, encryption } from "@shaferllc/keel/core";
|
|
5
|
+
|
|
6
|
+
declare const password: string;
|
|
7
|
+
declare const plain: string;
|
|
8
|
+
declare const submitted: string;
|
|
9
|
+
declare const untrustedInput: string;
|
|
10
|
+
declare const user: { password: string; save(): Promise<void> };
|
|
11
|
+
declare function unauthorized(): Response;
|
|
12
|
+
|
|
13
|
+
export async function hashingPasswords() {
|
|
14
|
+
const hashed = await hash.make(password); // store this
|
|
15
|
+
const ok = await hash.verify(hashed, password); // → true / false
|
|
16
|
+
return { hashed, ok };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function saltsDiffer() {
|
|
20
|
+
const a = await hash.make("hunter2");
|
|
21
|
+
const b = await hash.make("hunter2");
|
|
22
|
+
const sameString = a === b; // false — different salts
|
|
23
|
+
const va = await hash.verify(a, "hunter2");
|
|
24
|
+
const vb = await hash.verify(b, "hunter2");
|
|
25
|
+
return { sameString, va, vb };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function rotating() {
|
|
29
|
+
if (await hash.verify(user.password, plain)) {
|
|
30
|
+
if (hash.needsRehash(user.password)) {
|
|
31
|
+
user.password = await hash.make(plain);
|
|
32
|
+
await user.save();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function workFactor() {
|
|
38
|
+
const hashed = await hash.make(password, 200_000);
|
|
39
|
+
const atTarget = hash.needsRehash(hashed, 200_000); // false
|
|
40
|
+
const belowNew = hash.needsRehash(hashed, 300_000); // true
|
|
41
|
+
return { hashed, atTarget, belowNew };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function encryptingValues() {
|
|
45
|
+
const token = await encryption.encrypt({ userId: 1, scope: "reset" });
|
|
46
|
+
const data = await encryption.decrypt<{ userId: number }>(token);
|
|
47
|
+
return { token, data };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function decryptGuard() {
|
|
51
|
+
const value = await encryption.decrypt(untrustedInput);
|
|
52
|
+
if (value === null) return unauthorized();
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function reference() {
|
|
57
|
+
const hashed = await hash.make(password);
|
|
58
|
+
const stronger = await hash.make(password, 200_000);
|
|
59
|
+
|
|
60
|
+
const authed = await hash.verify(user.password, submitted);
|
|
61
|
+
|
|
62
|
+
const needs = hash.needsRehash(user.password);
|
|
63
|
+
if (needs) {
|
|
64
|
+
user.password = await hash.make(plain);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const token = await encryption.encrypt({ userId: 1, scope: "reset" });
|
|
68
|
+
const decoded = await encryption.decrypt<{ userId: number }>(token);
|
|
69
|
+
|
|
70
|
+
return { hashed, stronger, authed, needs, token, decoded };
|
|
71
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Type-check harness for docs/health.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
health,
|
|
4
|
+
healthCheck,
|
|
5
|
+
check,
|
|
6
|
+
Result,
|
|
7
|
+
BaseCheck,
|
|
8
|
+
HealthChecks,
|
|
9
|
+
DatabaseCheck,
|
|
10
|
+
RedisCheck,
|
|
11
|
+
CacheCheck,
|
|
12
|
+
HttpKernel,
|
|
13
|
+
ServiceProvider,
|
|
14
|
+
type HealthReport,
|
|
15
|
+
type CheckReport,
|
|
16
|
+
type HealthStatus,
|
|
17
|
+
} from "@shaferllc/keel/core";
|
|
18
|
+
|
|
19
|
+
declare const env: { HEALTH_SECRET: string };
|
|
20
|
+
declare function queue(): { size(): Promise<number> };
|
|
21
|
+
|
|
22
|
+
export class HealthServiceProvider extends ServiceProvider {
|
|
23
|
+
boot(): void {
|
|
24
|
+
health().register([new DatabaseCheck(), new RedisCheck().cacheFor(30)]);
|
|
25
|
+
|
|
26
|
+
this.app.make(HttpKernel).use(healthCheck());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function builtInChecks() {
|
|
31
|
+
health().register([
|
|
32
|
+
new DatabaseCheck(),
|
|
33
|
+
new DatabaseCheck("reporting"),
|
|
34
|
+
new RedisCheck(),
|
|
35
|
+
new CacheCheck(),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function functionChecks() {
|
|
40
|
+
health().register([
|
|
41
|
+
check("stripe", async () => {
|
|
42
|
+
const res = await fetch("https://api.stripe.com/healthcheck");
|
|
43
|
+
return res.ok
|
|
44
|
+
? Result.ok("Stripe is reachable")
|
|
45
|
+
: Result.failed(`Stripe returned ${res.status}`);
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
check("queue-depth", async () => {
|
|
49
|
+
const depth = await queue().size();
|
|
50
|
+
if (depth > 10_000) return Result.failed(`Queue is backed up (${depth})`);
|
|
51
|
+
if (depth > 1_000) return Result.warning(`Queue is deep (${depth})`);
|
|
52
|
+
return Result.ok("Queue is keeping up").withMeta({ depth });
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class QueueDepthCheck extends BaseCheck {
|
|
58
|
+
readonly name = "queue-depth";
|
|
59
|
+
|
|
60
|
+
constructor(private limit = 10_000) {
|
|
61
|
+
super();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async run(): Promise<Result> {
|
|
65
|
+
const depth = await queue().size();
|
|
66
|
+
return depth > this.limit
|
|
67
|
+
? Result.failed(`Queue is backed up (${depth})`)
|
|
68
|
+
: Result.ok("Queue is keeping up").withMeta({ depth });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function protectedEndpoint() {
|
|
73
|
+
return healthCheck({ secret: env.HEALTH_SECRET });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function customBasePathAndRegistry() {
|
|
77
|
+
const checks = new HealthChecks().register([check("a", () => Result.ok("fine"))]);
|
|
78
|
+
return healthCheck({ basePath: "/_internal", checks });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runningByHand(): Promise<{
|
|
82
|
+
report: HealthReport;
|
|
83
|
+
first: CheckReport | undefined;
|
|
84
|
+
status: HealthStatus;
|
|
85
|
+
}> {
|
|
86
|
+
const report = await health().run();
|
|
87
|
+
return { report, first: report.checks[0], status: report.status };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function inspectRegistry() {
|
|
91
|
+
const all: BaseCheck[] = health().all();
|
|
92
|
+
health().clear();
|
|
93
|
+
return all;
|
|
94
|
+
}
|