@shaferllc/keel 0.68.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.
Files changed (140) hide show
  1. package/AGENTS.md +2 -0
  2. package/README.md +14 -5
  3. package/dist/api/api.config.stub +9 -0
  4. package/dist/api/config.d.ts +13 -0
  5. package/dist/api/config.js +14 -0
  6. package/dist/api/index.d.ts +16 -0
  7. package/dist/api/index.js +13 -0
  8. package/dist/api/provider.d.ts +10 -0
  9. package/dist/api/provider.js +17 -0
  10. package/dist/api/query.d.ts +35 -0
  11. package/dist/api/query.js +42 -0
  12. package/dist/api/resource.d.ts +91 -0
  13. package/dist/api/resource.js +188 -0
  14. package/dist/core/application.js +6 -0
  15. package/dist/core/cache.d.ts +1 -2
  16. package/dist/core/cache.js +9 -2
  17. package/dist/core/cli/stubs.d.ts +14 -0
  18. package/dist/core/cli/stubs.js +105 -0
  19. package/dist/core/console-prompt.d.ts +79 -0
  20. package/dist/core/console-prompt.js +239 -0
  21. package/dist/core/console-ui.d.ts +96 -0
  22. package/dist/core/console-ui.js +187 -0
  23. package/dist/core/console.d.ts +188 -0
  24. package/dist/core/console.js +395 -0
  25. package/dist/core/database.d.ts +70 -1
  26. package/dist/core/database.js +174 -15
  27. package/dist/core/env.d.ts +96 -0
  28. package/dist/core/env.js +140 -0
  29. package/dist/core/health.d.ts +2 -2
  30. package/dist/core/health.js +2 -2
  31. package/dist/core/http/kernel.d.ts +2 -0
  32. package/dist/core/http/kernel.js +48 -0
  33. package/dist/core/http/router.d.ts +5 -5
  34. package/dist/core/http/router.js +5 -5
  35. package/dist/core/i18n.d.ts +162 -0
  36. package/dist/core/i18n.js +472 -0
  37. package/dist/core/index.d.ts +25 -4
  38. package/dist/core/index.js +13 -3
  39. package/dist/core/instrumentation.d.ts +113 -0
  40. package/dist/core/instrumentation.js +52 -0
  41. package/dist/core/logger.d.ts +7 -0
  42. package/dist/core/logger.js +28 -1
  43. package/dist/core/notification.js +10 -1
  44. package/dist/core/package.d.ts +120 -0
  45. package/dist/core/package.js +169 -0
  46. package/dist/core/pages.d.ts +108 -0
  47. package/dist/core/pages.js +199 -0
  48. package/dist/core/queue.js +26 -5
  49. package/dist/core/repl.d.ts +33 -0
  50. package/dist/core/repl.js +88 -0
  51. package/dist/core/scheduler.js +6 -0
  52. package/dist/core/social.d.ts +4 -4
  53. package/dist/core/social.js +4 -4
  54. package/dist/core/storage.js +15 -3
  55. package/dist/core/telemetry.d.ts +208 -0
  56. package/dist/core/telemetry.js +383 -0
  57. package/dist/core/template.d.ts +2 -3
  58. package/dist/core/template.js +2 -3
  59. package/dist/core/testing.d.ts +170 -1
  60. package/dist/core/testing.js +504 -2
  61. package/dist/db/d1.js +13 -0
  62. package/dist/db/pg.d.ts +13 -0
  63. package/dist/db/pg.js +46 -4
  64. package/dist/openapi/config.d.ts +28 -0
  65. package/dist/openapi/config.js +25 -0
  66. package/dist/openapi/doc.d.ts +40 -0
  67. package/dist/openapi/doc.js +20 -0
  68. package/dist/openapi/export.d.ts +8 -0
  69. package/dist/openapi/export.js +19 -0
  70. package/dist/openapi/gate.d.ts +15 -0
  71. package/dist/openapi/gate.js +27 -0
  72. package/dist/openapi/index.d.ts +19 -0
  73. package/dist/openapi/index.js +15 -0
  74. package/dist/openapi/openapi.config.stub +29 -0
  75. package/dist/openapi/provider.d.ts +18 -0
  76. package/dist/openapi/provider.js +35 -0
  77. package/dist/openapi/routes.d.ts +9 -0
  78. package/dist/openapi/routes.js +23 -0
  79. package/dist/openapi/spec.d.ts +23 -0
  80. package/dist/openapi/spec.js +132 -0
  81. package/dist/openapi/ui.d.ts +8 -0
  82. package/dist/openapi/ui.js +31 -0
  83. package/dist/openapi/zod.d.ts +12 -0
  84. package/dist/openapi/zod.js +46 -0
  85. package/dist/watch/config.d.ts +33 -0
  86. package/dist/watch/config.js +38 -0
  87. package/dist/watch/entry.d.ts +53 -0
  88. package/dist/watch/entry.js +105 -0
  89. package/dist/watch/gate.d.ts +20 -0
  90. package/dist/watch/gate.js +32 -0
  91. package/dist/watch/index.d.ts +21 -0
  92. package/dist/watch/index.js +17 -0
  93. package/dist/watch/migration.d.ts +7 -0
  94. package/dist/watch/migration.js +20 -0
  95. package/dist/watch/provider.d.ts +22 -0
  96. package/dist/watch/provider.js +58 -0
  97. package/dist/watch/prune.d.ts +11 -0
  98. package/dist/watch/prune.js +20 -0
  99. package/dist/watch/recorder.d.ts +24 -0
  100. package/dist/watch/recorder.js +39 -0
  101. package/dist/watch/routes.d.ts +13 -0
  102. package/dist/watch/routes.js +55 -0
  103. package/dist/watch/store.d.ts +54 -0
  104. package/dist/watch/store.js +158 -0
  105. package/dist/watch/ui/dist/watch.css +1 -0
  106. package/dist/watch/ui/dist/watch.js +555 -0
  107. package/dist/watch/ui-shell.d.ts +12 -0
  108. package/dist/watch/ui-shell.js +24 -0
  109. package/dist/watch/watch.config.stub +47 -0
  110. package/dist/watch/watchers.d.ts +12 -0
  111. package/dist/watch/watchers.js +156 -0
  112. package/docs/ai-manifest.json +737 -3
  113. package/docs/api-resources.md +118 -0
  114. package/docs/configuration.md +74 -0
  115. package/docs/console.md +193 -13
  116. package/docs/database.md +101 -0
  117. package/docs/examples/configuration.ts +40 -0
  118. package/docs/examples/console.ts +134 -0
  119. package/docs/examples/database.ts +90 -0
  120. package/docs/examples/i18n.ts +117 -0
  121. package/docs/examples/logger.ts +74 -0
  122. package/docs/examples/mail.ts +103 -0
  123. package/docs/examples/pages.ts +82 -0
  124. package/docs/examples/telemetry.ts +127 -0
  125. package/docs/examples/testing.ts +149 -0
  126. package/docs/health.md +4 -4
  127. package/docs/i18n.md +302 -0
  128. package/docs/logger.md +156 -10
  129. package/docs/mail.md +73 -0
  130. package/docs/openapi.md +111 -0
  131. package/docs/packages.md +118 -0
  132. package/docs/pages.md +217 -0
  133. package/docs/storage.md +5 -3
  134. package/docs/telemetry.md +263 -0
  135. package/docs/templates.md +3 -4
  136. package/docs/testing.md +251 -0
  137. package/docs/watch.md +118 -0
  138. package/llms-full.txt +2370 -248
  139. package/llms.txt +12 -1
  140. package/package.json +18 -2
@@ -0,0 +1,134 @@
1
+ // Type-check harness for docs/console.md. Compile-only — never executed.
2
+ import {
3
+ defineCommand,
4
+ arg,
5
+ flag,
6
+ ConsoleKernel,
7
+ createUi,
8
+ createPrompt,
9
+ startRepl,
10
+ stripAnsi,
11
+ type Application,
12
+ type Ui,
13
+ type Prompt,
14
+ type CommandDefinition,
15
+ type ReplHelper,
16
+ } from "@shaferllc/keel/core";
17
+
18
+ export const greet = defineCommand({
19
+ name: "greet",
20
+ description: "Greet someone",
21
+
22
+ args: { name: arg.string({ description: "who to greet" }) },
23
+ flags: { loud: flag.boolean({ alias: "l", description: "SHOUT IT" }) },
24
+
25
+ async run({ args, flags, ui }) {
26
+ // The inference is the point: these annotations must hold.
27
+ const name: string = args.name;
28
+ const loud: boolean = flags.loud;
29
+
30
+ const message = `Hello, ${name}!`;
31
+ ui.success(loud ? message.toUpperCase() : message);
32
+ },
33
+ });
34
+
35
+ export const everySpec = defineCommand({
36
+ name: "specs",
37
+ args: {
38
+ required: arg.string(),
39
+ optional: arg.string({ required: false }),
40
+ defaulted: arg.string({ default: "x" }),
41
+ count: arg.number(),
42
+ rest: arg.spread({ required: false }),
43
+ },
44
+ flags: {
45
+ on: flag.boolean(),
46
+ name: flag.string({ alias: "n" }),
47
+ needed: flag.string({ required: true }),
48
+ times: flag.number({ default: 1 }),
49
+ tags: flag.array(),
50
+ },
51
+ allowUnknownFlags: true,
52
+
53
+ run({ args, flags, rest }) {
54
+ const a: string = args.required;
55
+ const b: string | undefined = args.optional;
56
+ const c: string = args.defaulted;
57
+ const d: number = args.count;
58
+ const e: string[] = args.rest;
59
+
60
+ const f: boolean = flags.on;
61
+ const g: string | undefined = flags.name;
62
+ const h: string = flags.needed;
63
+ const i: number = flags.times;
64
+ const j: string[] = flags.tags;
65
+
66
+ void [a, b, c, d, e, f, g, h, i, j, rest];
67
+ return 0; // the exit code
68
+ },
69
+ });
70
+
71
+ export function terminalUi(ui: Ui) {
72
+ ui.info("Checking…");
73
+ ui.success("Migrated 3 tables");
74
+ ui.warning("Nothing to do");
75
+ ui.error("Failed");
76
+ ui.debug("verbose detail");
77
+
78
+ ui.action("create", "app/Models/User.ts");
79
+ ui.action("skip", "app/Models/Post.ts", "skipped");
80
+
81
+ ui.table(["Name", "Rows"]).row(["users", "42"]).row(["orgs", "7"]).render();
82
+
83
+ ui.sticker(["http://localhost:3000"], "Server running");
84
+ ui.instructions(["cd my-app", "npm install", "keel serve"], "Next steps");
85
+
86
+ return ui.colors("green", "done");
87
+ }
88
+
89
+ export async function tasks(ui: Ui) {
90
+ return ui
91
+ .tasks()
92
+ .add("Install dependencies", async (task) => {
93
+ task.update("resolving…");
94
+ return "42 packages";
95
+ })
96
+ .add("Run migrations", async () => "3 tables")
97
+ .run();
98
+ }
99
+
100
+ export async function prompts(prompt: Prompt) {
101
+ const name = await prompt.ask("Project name?", { default: "my-app" });
102
+ const secret = await prompt.secure("API key?");
103
+ const ok = await prompt.confirm("Delete everything?");
104
+ const driver = await prompt.choice("Database?", ["sqlite", "postgres"]);
105
+ const features = await prompt.multiple("Features?", ["auth", "queue", "mail"]);
106
+
107
+ return { name, secret, ok, driver, features };
108
+ }
109
+
110
+ export async function testingACommand(setup: CommandDefinition) {
111
+ const ui = createUi({ raw: true });
112
+ const prompt = createPrompt({ trap: true });
113
+ const kernel = new ConsoleKernel({ ui, prompt }).register(setup);
114
+
115
+ prompt.trap("Project name?").replyWith("keel-app");
116
+ prompt.trap("Database?").chooseOption(1);
117
+ prompt.trap("Write the config?").accept();
118
+
119
+ prompt
120
+ .trap("Email?")
121
+ .assertFails("", "Email is required")
122
+ .assertPasses("ada@example.com")
123
+ .replyWith("ada@example.com");
124
+
125
+ const code = await kernel.run(["setup"]);
126
+
127
+ prompt.assertAllTrapsUsed();
128
+
129
+ return { code, logs: ui.logs, errors: ui.errors, plain: stripAnsi(ui.logs.join("\n")) };
130
+ }
131
+
132
+ export async function repl(app: Application, helpers: ReplHelper[]) {
133
+ await startRepl(app, { helpers, prompt: "keel > " });
134
+ }
@@ -116,3 +116,93 @@ const dialect: Dialect = "postgres";
116
116
  const op: Operator = "like";
117
117
  const row: Row = { id: 1 };
118
118
  export { mock, dialect, op, row };
119
+
120
+ /* --- Transactions --- */
121
+
122
+ import {
123
+ transaction,
124
+ inTransaction,
125
+ type TransactionHandle,
126
+ type TransactionConnection,
127
+ } from "@shaferllc/keel/core";
128
+
129
+ declare const order: Record<string, unknown>;
130
+ declare const item: Record<string, unknown>;
131
+ declare const entry: Record<string, unknown>;
132
+ declare const id: number;
133
+
134
+ export async function committing() {
135
+ await transaction(async () => {
136
+ await db("orders").insert(order);
137
+ await db("stock").where("id", id).update({ count: 0 });
138
+ });
139
+ }
140
+
141
+ export async function explicitHandle() {
142
+ return transaction(async (tx: TransactionHandle) => {
143
+ await tx.table("orders").insert(order);
144
+ await tx.write("UPDATE stock SET count = count - 1 WHERE id = ?", [id]);
145
+ void tx.depth;
146
+ void tx.dialect;
147
+ return "done";
148
+ });
149
+ }
150
+
151
+ export async function abandoning() {
152
+ await transaction(async (tx) => {
153
+ await tx.table("orders").insert(order);
154
+ await tx.rollback();
155
+ });
156
+ }
157
+
158
+ export async function nesting() {
159
+ await transaction(async () => {
160
+ await db("orders").insert(order);
161
+
162
+ try {
163
+ await transaction(async () => {
164
+ await db("items").insert(item);
165
+ throw new Error("out of stock");
166
+ });
167
+ } catch {
168
+ // only the inner work was rolled back
169
+ }
170
+
171
+ await db("audit").insert(entry);
172
+ });
173
+ }
174
+
175
+ export function checking(): boolean {
176
+ return inTransaction();
177
+ }
178
+
179
+ export async function onANamedConnection() {
180
+ await transaction(async () => {
181
+ await db("events", "reporting").insert(entry);
182
+ }, "reporting");
183
+ }
184
+
185
+ /** A driver that pools must implement begin() — see the guide. */
186
+ export function pooledDriver(): Connection {
187
+ const conn: Connection = {
188
+ async select() {
189
+ return [];
190
+ },
191
+ async write() {
192
+ return { rowsAffected: 0 };
193
+ },
194
+ async begin(): Promise<TransactionConnection> {
195
+ return {
196
+ async select() {
197
+ return [];
198
+ },
199
+ async write() {
200
+ return { rowsAffected: 0 };
201
+ },
202
+ async commit() {},
203
+ async rollback() {},
204
+ };
205
+ },
206
+ };
207
+ return conn;
208
+ }
@@ -0,0 +1,117 @@
1
+ // Type-check harness for docs/i18n.md. Compile-only — never executed.
2
+ import {
3
+ i18n,
4
+ t,
5
+ setI18n,
6
+ getI18n,
7
+ setTranslations,
8
+ detectLocale,
9
+ negotiateLocale,
10
+ formatMessage,
11
+ objectLoader,
12
+ I18n,
13
+ I18nManager,
14
+ HttpKernel,
15
+ ServiceProvider,
16
+ listen,
17
+ logger,
18
+ type TranslationsByLocale,
19
+ type TranslationLoader,
20
+ type I18nOptions,
21
+ type DetectLocaleOptions,
22
+ } from "@shaferllc/keel/core";
23
+
24
+ declare const order: { createdAt: Date };
25
+ declare const post: { publishedAt: Date };
26
+ declare const names: string[];
27
+
28
+ export class I18nServiceProvider extends ServiceProvider {
29
+ boot(): void {
30
+ setI18n(new I18nManager({ defaultLocale: "en" }));
31
+
32
+ setTranslations({
33
+ en: { "cart.items": "{count, plural, one {# item} other {# items}}" },
34
+ fr: { "cart.items": "{count, plural, one {# article} other {# articles}}" },
35
+ });
36
+
37
+ this.app.make(HttpKernel).use(detectLocale());
38
+ }
39
+ }
40
+
41
+ export function translating() {
42
+ return [t("cart.items", { count: 3 }), i18n("fr").t("cart.items", { count: 3 })];
43
+ }
44
+
45
+ export function formatters() {
46
+ const l: I18n = i18n();
47
+ return {
48
+ number: l.formatNumber(1234.5),
49
+ currency: l.formatCurrency(9.5, "USD"),
50
+ date: l.formatDate(order.createdAt),
51
+ time: l.formatTime(order.createdAt),
52
+ relative: l.formatRelativeTime(post.publishedAt),
53
+ relativeUnit: l.formatRelativeTime(post.publishedAt, "hour"),
54
+ list: l.formatList(["a", "b", "c"]),
55
+ or: l.formatList(names, { type: "disjunction" }),
56
+ plural: l.formatPlural(5),
57
+ display: l.formatDisplayName("fr"),
58
+ has: l.has("cart.items"),
59
+ locale: l.locale,
60
+ };
61
+ }
62
+
63
+ export function detection(): DetectLocaleOptions[] {
64
+ return [{}, { query: "lang" }, { cookie: "locale" }, { header: false }, { resolve: () => "fr" }];
65
+ }
66
+
67
+ export function middleware() {
68
+ return [detectLocale(), detectLocale({ query: "lang", cookie: "locale" })];
69
+ }
70
+
71
+ export function negotiating() {
72
+ return negotiateLocale("fr-CA,fr;q=0.9,en;q=0.8", ["en", "fr"], "en");
73
+ }
74
+
75
+ export function messages() {
76
+ return [
77
+ formatMessage("Hello {name}!", { name: "Ada" }),
78
+ formatMessage("{count, plural, =0 {Empty} one {# item} other {# items}}", { count: 3 }, "en"),
79
+ formatMessage("{n, selectordinal, one {#st} other {#th}}", { n: 1 }, "en"),
80
+ formatMessage("{gender, select, male {He} other {They}}", { gender: "male" }),
81
+ formatMessage("{n, number, ::currency/USD}", { n: 9.5 }, "en-US"),
82
+ ];
83
+ }
84
+
85
+ export function fallbacks() {
86
+ setTranslations({
87
+ es: { greeting: "Hola", chair: "silla" },
88
+ "es-MX": { chair: "banca" },
89
+ });
90
+
91
+ return [i18n("es-MX").t("chair"), i18n("es-MX").t("greeting")];
92
+ }
93
+
94
+ export function missingKeys() {
95
+ listen("i18n.missing", (payload) => {
96
+ const { key, locale } = payload as { key: string; locale: string };
97
+ logger().warn("missing translation", { key, locale });
98
+ });
99
+
100
+ const options: I18nOptions = {
101
+ defaultLocale: "en",
102
+ supportedLocales: ["en", "fr"],
103
+ fallbackLocales: { "es-MX": "es" },
104
+ missing: (key, locale) => `[${locale}:${key}]`,
105
+ };
106
+ return new I18nManager(options);
107
+ }
108
+
109
+ export async function loaders() {
110
+ const data: TranslationsByLocale = { en: { hi: "Hi" } };
111
+ const loader: TranslationLoader = objectLoader(data);
112
+
113
+ const manager = new I18nManager();
114
+ await manager.load(loader);
115
+
116
+ return { manager, supported: manager.supported(), active: getI18n().defaultLocale };
117
+ }
@@ -90,3 +90,77 @@ const options: LoggerOptions = {
90
90
  };
91
91
  const level: LogLevel = "warn";
92
92
  export { options, level };
93
+
94
+ /* --- Levels, sinks, named loggers, redaction options --- */
95
+
96
+ import {
97
+ MemorySink,
98
+ consoleSink,
99
+ setLogger,
100
+ namedLogger,
101
+ type Sink,
102
+ type LogRecord,
103
+ type RedactOptions,
104
+ } from "@shaferllc/keel/core";
105
+
106
+ declare function expensiveSnapshot(): unknown;
107
+ declare const auditSink: Sink;
108
+ declare const userId: number;
109
+
110
+ export function allLevels() {
111
+ const log = new Logger({ level: "trace" });
112
+ log.trace("t");
113
+ log.debug("d");
114
+ log.info("i");
115
+ log.warn("w");
116
+ log.error("e");
117
+ log.fatal("f");
118
+ log.log("warn", "dynamic");
119
+ }
120
+
121
+ export function gating() {
122
+ if (logger().isLevelEnabled("debug")) {
123
+ logger().debug("state", { snapshot: expensiveSnapshot() });
124
+ }
125
+ logger().ifLevelEnabled("debug", (log) =>
126
+ log.debug("state", { snapshot: expensiveSnapshot() }),
127
+ );
128
+ }
129
+
130
+ export function sinks() {
131
+ const httpSink: Sink = (record: LogRecord) => {
132
+ void fetch("https://logs.example.com", { method: "POST", body: JSON.stringify(record) });
133
+ };
134
+ new Logger({ sink: httpSink });
135
+ new Logger({ sink: consoleSink(true) });
136
+ new Logger({ enabled: false });
137
+ }
138
+
139
+ export function memorySink() {
140
+ const sink = new MemorySink();
141
+ const log = new Logger({ level: "trace", sink: sink.sink });
142
+
143
+ log.info("hello", { userId: 1 });
144
+
145
+ const messages = sink.messages();
146
+ const infos = sink.at("info");
147
+ const fields = sink.records[0]?.fields;
148
+ sink.clear();
149
+
150
+ return { messages, infos, fields };
151
+ }
152
+
153
+ export function named() {
154
+ setLogger(new Logger({ level: "trace", sink: auditSink }), "audit");
155
+ namedLogger("audit").trace("permission granted", { userId });
156
+ }
157
+
158
+ export function redactionOptions() {
159
+ new Logger({ redact: ["password", "req.headers.authorization"] });
160
+ new Logger({ redact: ["*.password", "creds.*.token"] });
161
+
162
+ const censor: RedactOptions = { paths: ["password"], censor: "***" };
163
+ const remove: RedactOptions = { paths: ["password"], remove: true };
164
+ new Logger({ redact: censor });
165
+ new Logger({ redact: remove });
166
+ }
@@ -158,3 +158,106 @@ const seamFetchOptions: FetchTransportOptions = {
158
158
  };
159
159
 
160
160
  export { seamMessage, seamTransport, seamOptions, seamFetchOptions };
161
+
162
+ /* --- Queueing, attachments, class mails, named mailers, faking, events --- */
163
+
164
+ import {
165
+ mailer as namedMailer,
166
+ send as sendMail,
167
+ sendLater as queueMail,
168
+ fakeMail,
169
+ restoreMail,
170
+ BaseMail,
171
+ listen,
172
+ logger,
173
+ type PendingMail as PendingMailType,
174
+ type Attachment,
175
+ type RecordedMail,
176
+ } from "@shaferllc/keel/core";
177
+
178
+ declare const user2: { email: string; name: string };
179
+ declare const body: string;
180
+ declare const pdfBytes: Uint8Array;
181
+ declare const logoBytes: Uint8Array;
182
+ declare const postmark: Transport;
183
+ declare const resend: Transport;
184
+
185
+ export async function queueing() {
186
+ await mail().to(user2.email).subject("Welcome").html(body).sendLater();
187
+ }
188
+
189
+ export async function attachments() {
190
+ await mail()
191
+ .to("ada@example.com")
192
+ .subject("Your invoice")
193
+ .html('<p>Attached. <img src="cid:logo"></p>')
194
+ .attach("invoice.pdf", pdfBytes)
195
+ .attach("data.csv", "a,b,c", "text/csv")
196
+ .embed("logo", logoBytes, "logo.png")
197
+ .send();
198
+ }
199
+
200
+ export class WelcomeEmail extends BaseMail {
201
+ constructor(private user: { email: string; name: string }) {
202
+ super();
203
+ }
204
+
205
+ build(message: PendingMailType): void {
206
+ message
207
+ .to(this.user.email)
208
+ .subject(`Welcome, ${this.user.name}`)
209
+ .html(`<h1>Hi ${this.user.name}</h1>`);
210
+ }
211
+ }
212
+
213
+ export async function classBasedMails() {
214
+ await sendMail(new WelcomeEmail(user2));
215
+ await queueMail(new WelcomeEmail(user2));
216
+ await sendMail(new WelcomeEmail(user2), "marketing");
217
+ }
218
+
219
+ export async function namedMailers() {
220
+ setMailer(postmark, { from: "hi@app.com" });
221
+ setMailer(resend, { from: "news@app.com" }, "marketing");
222
+
223
+ await mail().to(user2.email).subject("Receipt").text(body).send();
224
+ await mail("marketing").to(user2.email).subject("This month").html(body).send();
225
+
226
+ return namedMailer("marketing");
227
+ }
228
+
229
+ export async function faking() {
230
+ const faked = fakeMail();
231
+
232
+ faked.assertSent();
233
+ faked.assertSent((m) => m.subject === "Welcome");
234
+ faked.assertSentCount(1);
235
+ faked.assertQueued((m) => m.to.includes("ada@example.com"));
236
+ faked.assertNotSent((m) => m.subject === "Password reset");
237
+ faked.assertNotQueued();
238
+ faked.assertQueuedCount(0);
239
+ faked.assertNothingSent();
240
+
241
+ const sent: Message[] = faked.sent();
242
+ const queued: Message[] = faked.queued();
243
+ const all: RecordedMail[] = faked.mails;
244
+
245
+ restoreMail();
246
+ restoreMail("marketing");
247
+
248
+ return { sent, queued, all };
249
+ }
250
+
251
+ export function mailEvents() {
252
+ listen("mail.sent", (message) => {
253
+ logger().info("mail sent", { subject: (message as Message).subject });
254
+ });
255
+ }
256
+
257
+ export function attachmentType(a: Attachment): string | undefined {
258
+ return a.contentType;
259
+ }
260
+
261
+ export function composed(): Message {
262
+ return mail().to("a@b.com").subject("x").text("y").toMessage();
263
+ }
@@ -0,0 +1,82 @@
1
+ // Type-check harness for docs/pages.md. Compile-only — never executed.
2
+ // The .tsx page snippets in the guide are illustrative JSX; the exports they use
3
+ // are exercised here against the real types.
4
+ import {
5
+ pages,
6
+ definePages,
7
+ routePattern,
8
+ routeName,
9
+ db,
10
+ ServiceProvider,
11
+ authGuard,
12
+ Router,
13
+ make,
14
+ type Ctx,
15
+ type PageProps,
16
+ type PageModule,
17
+ type PagesOptions,
18
+ type RegisteredPage,
19
+ } from "@shaferllc/keel/core";
20
+
21
+ interface User {
22
+ id: number;
23
+ name: string;
24
+ }
25
+
26
+ export class PageServiceProvider extends ServiceProvider {
27
+ async boot(): Promise<void> {
28
+ await pages();
29
+ }
30
+ }
31
+
32
+ export async function registering(): Promise<RegisteredPage[]> {
33
+ const options: PagesOptions = {
34
+ dir: "app/pages",
35
+ prefix: "/app",
36
+ middleware: [authGuard()],
37
+ };
38
+
39
+ await pages();
40
+ await pages({ prefix: "/app" });
41
+ await pages({ dir: "app/pages" });
42
+ await pages({ middleware: [authGuard()] });
43
+
44
+ return pages(options);
45
+ }
46
+
47
+ /** What `definePages` takes — the shape `import.meta.glob(..., { eager: true })` gives. */
48
+ export function edgeManifest(modules: Record<string, PageModule<never, never>>) {
49
+ return definePages(modules, { dir: "resources/pages" });
50
+ }
51
+
52
+ /** A page module, spelled out — the same exports the .tsx files in the guide use. */
53
+ export const userPage: PageModule<{ id: string }, User> = {
54
+ name: "users.show",
55
+ path: "/users/:id",
56
+ middleware: [authGuard()],
57
+
58
+ loader: async (c: Ctx): Promise<User> => {
59
+ const row = await db("users").where("id", c.req.param("id")).first();
60
+ return row as unknown as User;
61
+ },
62
+
63
+ default: ({ params, data, ctx }: PageProps<{ id: string }, User>) => {
64
+ void ctx;
65
+ return `<h1>${data.name}</h1><p>User #${params.id}</p>`;
66
+ },
67
+ };
68
+
69
+ export function conventions() {
70
+ return {
71
+ root: routePattern("index.tsx"), // "/"
72
+ about: routePattern("about.tsx"), // "/about"
73
+ users: routePattern("users/index.tsx"), // "/users"
74
+ user: routePattern("users/[id].tsx"), // "/users/:id"
75
+ docs: routePattern("docs/[...slug].tsx"), // "/docs/:slug{.+}"
76
+ name: routeName("users/[id].tsx"), // "users.id"
77
+ };
78
+ }
79
+
80
+ export function urls(): string {
81
+ return make(Router).url("users.id", { id: 5 }); // "/users/5"
82
+ }