@shaferllc/keel 0.66.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 +2 -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/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/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- 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/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- 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 +26 -7
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Type-check harness for docs/helpers.md. Every type-checkable snippet in the
|
|
2
|
+
// guide is exercised here against the real exports, so a renamed helper or wrong
|
|
3
|
+
// argument type fails `npm run typecheck:docs`. Compile-only — never executed.
|
|
4
|
+
import {
|
|
5
|
+
app,
|
|
6
|
+
config,
|
|
7
|
+
view,
|
|
8
|
+
bind,
|
|
9
|
+
singleton,
|
|
10
|
+
instance,
|
|
11
|
+
make,
|
|
12
|
+
bound,
|
|
13
|
+
events,
|
|
14
|
+
emit,
|
|
15
|
+
listen,
|
|
16
|
+
cache,
|
|
17
|
+
logger,
|
|
18
|
+
Config,
|
|
19
|
+
type Listener,
|
|
20
|
+
type Token,
|
|
21
|
+
type Factory,
|
|
22
|
+
type Renderable,
|
|
23
|
+
} from "@shaferllc/keel/core";
|
|
24
|
+
|
|
25
|
+
// Stand-ins for app-defined values referenced in the guide.
|
|
26
|
+
type User = { id: number };
|
|
27
|
+
class Mailer {
|
|
28
|
+
constructor(_dep?: unknown) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare const user: User;
|
|
32
|
+
declare const order: { id: number };
|
|
33
|
+
declare const code: string;
|
|
34
|
+
declare const orderId: number;
|
|
35
|
+
declare function computeStats(): Promise<{ total: number }>;
|
|
36
|
+
declare function sendWelcome(u: User): void;
|
|
37
|
+
declare function fulfil(id: number): void;
|
|
38
|
+
declare function Welcome(props: { appName: string }): Renderable;
|
|
39
|
+
declare function HomePage(): Renderable;
|
|
40
|
+
|
|
41
|
+
// --- Intro ---
|
|
42
|
+
export function intro() {
|
|
43
|
+
const name = config<string>("app.name", "Keel");
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function introUsage() {
|
|
48
|
+
const stats = await cache().remember("stats", 60, () => computeStats());
|
|
49
|
+
await emit("user.registered", user);
|
|
50
|
+
logger().info("welcome sent", { userId: user.id });
|
|
51
|
+
return stats;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Container helpers, up close ---
|
|
55
|
+
export function containerHelpers() {
|
|
56
|
+
singleton(Mailer, (a) => new Mailer(a.make(Config)));
|
|
57
|
+
const mailer = make(Mailer);
|
|
58
|
+
if (bound("clock")) {
|
|
59
|
+
/* someone registered it */
|
|
60
|
+
}
|
|
61
|
+
return mailer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Events, cache, logger ---
|
|
65
|
+
export async function servicesUpClose() {
|
|
66
|
+
listen("order.paid", (o: { id: number }) => fulfil(o.id));
|
|
67
|
+
await emit("order.paid", order);
|
|
68
|
+
events().listenerCount("order.paid");
|
|
69
|
+
|
|
70
|
+
await cache().put("otp", code, 300);
|
|
71
|
+
logger().warn("retrying", { attempt: 2 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Rendering a view ---
|
|
75
|
+
function WelcomeInline({ appName }: { appName: string }): Renderable {
|
|
76
|
+
return `<h1>Welcome to ${appName}</h1>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function rendering() {
|
|
80
|
+
const a = await view(WelcomeInline, { appName: "Keel" });
|
|
81
|
+
const b = await view(HomePage);
|
|
82
|
+
return { a, b };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- API reference: app() ---
|
|
86
|
+
export function appHelper() {
|
|
87
|
+
const port = app().config().get<number>("app.port", 3000);
|
|
88
|
+
return port;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- config() ---
|
|
92
|
+
export function configHelper() {
|
|
93
|
+
const a = config<string>("app.name");
|
|
94
|
+
const b = config("app.port", 3000);
|
|
95
|
+
return { a, b };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- view() ---
|
|
99
|
+
export async function viewHelper() {
|
|
100
|
+
const a = await view(Welcome, { appName: "Keel" });
|
|
101
|
+
const b = await view(HomePage);
|
|
102
|
+
return { a, b };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- bind() ---
|
|
106
|
+
export function bindHelper() {
|
|
107
|
+
bind("clock", () => new Date());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- singleton() ---
|
|
111
|
+
export function singletonHelper() {
|
|
112
|
+
singleton(Mailer, (a) => new Mailer(a.make(Config)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- instance() ---
|
|
116
|
+
export function instanceHelper() {
|
|
117
|
+
const version = instance("app.version", "0.30.0");
|
|
118
|
+
return version;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- make() ---
|
|
122
|
+
export function makeHelper() {
|
|
123
|
+
const mailer = make(Mailer);
|
|
124
|
+
const version = make<string>("app.version");
|
|
125
|
+
return { mailer, version };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- bound() ---
|
|
129
|
+
export function boundHelper() {
|
|
130
|
+
if (bound("clock")) return make<Date>("clock");
|
|
131
|
+
return new Date();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- events() ---
|
|
135
|
+
export function eventsHelper() {
|
|
136
|
+
events().listenerCount("order.paid");
|
|
137
|
+
events().clear("order.paid");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- emit() ---
|
|
141
|
+
export async function emitHelper() {
|
|
142
|
+
await emit("user.registered", user);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- listen() ---
|
|
146
|
+
export function listenHelper() {
|
|
147
|
+
const off = listen("user.registered", (u: User) => sendWelcome(u));
|
|
148
|
+
off();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- cache() ---
|
|
152
|
+
export async function cacheHelper() {
|
|
153
|
+
const stats = await cache().remember("stats", 60, () => computeStats());
|
|
154
|
+
await cache().put("otp", code, 300);
|
|
155
|
+
return stats;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- logger() ---
|
|
159
|
+
export function loggerHelper() {
|
|
160
|
+
logger().info("user registered", { userId: user.id });
|
|
161
|
+
logger().error("payment failed", { orderId });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Interfaces & types ---
|
|
165
|
+
const onOrder: Listener<{ id: number }> = async (o) => fulfil(o.id);
|
|
166
|
+
const strToken: Token<string> = "app.version";
|
|
167
|
+
const svcToken: Token<Mailer> = Mailer;
|
|
168
|
+
const mailerFactory: Factory<Mailer> = (a) => new Mailer(a.make(Config));
|
|
169
|
+
const renderable: Renderable = "<p>hi</p>";
|
|
170
|
+
|
|
171
|
+
export { onOrder, strToken, svcToken, mailerFactory, renderable };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Type-check harness for docs/hooks.md. Compile-only — never executed.
|
|
2
|
+
import {
|
|
3
|
+
Application,
|
|
4
|
+
Router,
|
|
5
|
+
onReady,
|
|
6
|
+
onShutdown,
|
|
7
|
+
terminate,
|
|
8
|
+
logger,
|
|
9
|
+
redis,
|
|
10
|
+
type LifecycleHook,
|
|
11
|
+
} from "@shaferllc/keel/core";
|
|
12
|
+
|
|
13
|
+
declare function warmCaches(): Promise<void>;
|
|
14
|
+
|
|
15
|
+
export function globalHooks() {
|
|
16
|
+
onReady(async () => {
|
|
17
|
+
await warmCaches();
|
|
18
|
+
logger().info("app ready");
|
|
19
|
+
});
|
|
20
|
+
onShutdown(async () => {
|
|
21
|
+
await redis().flushAll();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function signals() {
|
|
26
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
27
|
+
process.on(signal, async () => {
|
|
28
|
+
await terminate();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function methodForm() {
|
|
35
|
+
const app = new Application();
|
|
36
|
+
app
|
|
37
|
+
.onReady((a) => void a)
|
|
38
|
+
.onShutdown(async () => {});
|
|
39
|
+
await app.boot([], { discoverConfig: false, config: { app: {} } });
|
|
40
|
+
const done: boolean = app.isTerminated;
|
|
41
|
+
await app.terminate();
|
|
42
|
+
return done;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function routeHook(app: Application) {
|
|
46
|
+
const router = app.make(Router);
|
|
47
|
+
router.onRoute((def) => {
|
|
48
|
+
logger().debug("route", { methods: def.methods, path: def.path, name: def.name });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The hook type
|
|
53
|
+
const ready: LifecycleHook = (app) => void app;
|
|
54
|
+
export { ready };
|