@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.
Files changed (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,58 @@
1
+ // Type-check harness for docs/templates.md. Every type-checkable snippet is
2
+ // exercised here against the real exports, so a renamed method or wrong argument
3
+ // type fails `npm run typecheck:docs`. Compile-only — never executed.
4
+ import {
5
+ templates,
6
+ render,
7
+ setTemplateEngine,
8
+ escapeHtml,
9
+ TemplateEngine,
10
+ html,
11
+ type Filter,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ declare const user: { name: string; admin: boolean };
15
+ declare function readFile(path: string, enc: string): Promise<string>;
16
+
17
+ export async function rendering() {
18
+ templates().register("greeting", "Hello, {{ name }}!");
19
+ const out = await render("greeting", { name: "Ada" });
20
+ return html(out);
21
+ }
22
+
23
+ export async function registerAll() {
24
+ templates().registerAll({
25
+ layout: await readFile("views/layout.html", "utf8"),
26
+ home: await readFile("views/home.html", "utf8"),
27
+ });
28
+ }
29
+
30
+ export function customFilter() {
31
+ templates().filter("currency", (v, code) =>
32
+ new Intl.NumberFormat("en-US", { style: "currency", currency: String(code) }).format(Number(v)),
33
+ );
34
+ }
35
+
36
+ export function globals() {
37
+ templates()
38
+ .global("appName", "Keel")
39
+ .global("asset", (path: string) => `/static/${path}`);
40
+ }
41
+
42
+ export function ownEngine() {
43
+ const engine = new TemplateEngine();
44
+ engine.register("home", "…");
45
+ engine.registerAll({ a: "A", b: "B" });
46
+ engine.has("home");
47
+ engine.global("appName", "Keel");
48
+ engine.filter("upper", (v) => String(v).toUpperCase());
49
+ setTemplateEngine(engine);
50
+ return engine.render("home", { user });
51
+ }
52
+
53
+ // Interfaces & types
54
+ const filter: Filter = (value, ...args) => `${String(value)}${args.length}`;
55
+
56
+ export function escaping() {
57
+ return [escapeHtml("<b>&\"'</b>"), escapeHtml(null), escapeHtml(42), filter("x", 1)];
58
+ }
@@ -0,0 +1,215 @@
1
+ // Type-check harness for docs/testing.md. Compile-only — never executed.
2
+ import {
3
+ Application,
4
+ Router,
5
+ HttpKernel,
6
+ json,
7
+ testClient,
8
+ sessionMiddleware,
9
+ requestLogger,
10
+ type TestClient,
11
+ type TestResponse,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ declare const body: unknown;
15
+
16
+ type User = { id: number; email: string };
17
+
18
+ async function makeApp() {
19
+ const app = new Application();
20
+ await app.boot([], { discoverConfig: false, config: { app: {} } });
21
+ app.make(Router).get("/health", () => json({ ok: true }));
22
+ return app;
23
+ }
24
+
25
+ export async function basic() {
26
+ const client: TestClient = testClient(await makeApp());
27
+ const res: TestResponse = await client.get("/health");
28
+ res.assertStatus(200).assertJson({ ok: true });
29
+ }
30
+
31
+ export async function requests(client: TestClient) {
32
+ await client.get("/users");
33
+ await client.get("/users?active=true");
34
+ await client.post("/users", { email: "a@b.com", name: "Ada" });
35
+ await client.put("/users/1", { name: "Grace" });
36
+ await client.delete("/users/1");
37
+ await client.request("/users", { method: "POST", headers: { authorization: "Bearer x" }, body: JSON.stringify(body) });
38
+ }
39
+
40
+ export async function responseShape(client: TestClient) {
41
+ const res = await client.get("/user");
42
+ const s: number = res.status;
43
+ const ct = res.header("content-type");
44
+ const t: string = res.text();
45
+ const u = res.json<User>();
46
+ return { s, ct, t, u, raw: res.raw };
47
+ }
48
+
49
+ export async function assertions(client: TestClient) {
50
+ const res = await client.post("/users", body);
51
+ res
52
+ .assertStatus(201)
53
+ .assertOk()
54
+ .assertJson({ id: 2 })
55
+ .assertText("")
56
+ .assertHeader("content-type", "application/json")
57
+ .assertRedirect("/login");
58
+ }
59
+
60
+ export async function withMiddleware() {
61
+ const app = await makeApp();
62
+ const kernel = new HttpKernel(app);
63
+ kernel.use(sessionMiddleware());
64
+ kernel.use(requestLogger());
65
+ return testClient(kernel);
66
+ }
67
+
68
+ /* --- Auth, forms, assertions, doubles, time, state, db, console --- */
69
+
70
+ import {
71
+ spy,
72
+ spyOn,
73
+ restoreSpies,
74
+ freezeTime,
75
+ timeTravel,
76
+ restoreTime,
77
+ resetState,
78
+ truncate,
79
+ assertDatabaseHas,
80
+ assertDatabaseMissing,
81
+ assertDatabaseCount,
82
+ assertDatabaseEmpty,
83
+ runCommand,
84
+ fakeMail,
85
+ fakeQueue,
86
+ swap,
87
+ jwt,
88
+ Job,
89
+ type Spy,
90
+ type CommandResult,
91
+ } from "@shaferllc/keel/core";
92
+
93
+ declare const bytes: Blob;
94
+ declare function notify(send: (message: string) => void): void;
95
+ declare function registerUser(user: { email: string }): Promise<void>;
96
+ declare const gateway: { charge(amount: number): string };
97
+ declare const receipt: string;
98
+ declare class PaymentGateway {}
99
+ declare class FakeGateway {}
100
+ declare class SendWelcome extends Job {
101
+ handle(): Promise<void>;
102
+ }
103
+
104
+ export async function authenticatedRequests(client: TestClient) {
105
+ const authed = client.withToken("tok_123");
106
+ await authed.get("/me");
107
+ await client.withBasicAuth("ada", "s3cret").get("/me");
108
+ await client.withHeader("x-tenant", "acme").get("/me");
109
+ await client.withHeaders({ "x-a": "1", "x-b": "2" }).get("/me");
110
+ await client.withCookie("session", "abc").get("/me");
111
+ await client.withCookies({ session: "abc" }).get("/me");
112
+ await client.acceptJson().get("/me");
113
+ }
114
+
115
+ export async function formsAndUploads(client: TestClient) {
116
+ await client.form("/login", { email: "a@b.com", password: "s3cret" });
117
+ await client.multipart("/avatar", { file: bytes, name: "ada" });
118
+ }
119
+
120
+ export async function moreAssertions(client: TestClient) {
121
+ const res = await client.get("/users/1");
122
+
123
+ res.assertOk();
124
+ res.assertCreated();
125
+ res.assertNoContent();
126
+ res.assertBadRequest();
127
+ res.assertUnauthorized();
128
+ res.assertForbidden();
129
+ res.assertNotFound();
130
+ res.assertUnprocessable();
131
+ res.assertServerError();
132
+
133
+ res.assertJsonContains({ user: { email: "a@b.com" } });
134
+ res.assertSee("Welcome back");
135
+ res.assertDontSee("Sign up");
136
+
137
+ res.assertHeader("content-type", "application/json");
138
+ res.assertHeaderMissing("x-debug");
139
+
140
+ res.assertCookie("session");
141
+ res.assertCookie("session", "abc123");
142
+ res.assertCookieMissing("admin");
143
+
144
+ res.assertValidationErrors("email", "password");
145
+ res.assertNoValidationError("name");
146
+
147
+ res.dump();
148
+ return res.cookies();
149
+ }
150
+
151
+ export async function doubles() {
152
+ const mailer = fakeMail();
153
+ const queue = fakeQueue();
154
+
155
+ await registerUser({ email: "ada@example.com" });
156
+
157
+ mailer.assertQueued((m) => m.subject === "Welcome");
158
+ queue.assertPushed(SendWelcome);
159
+
160
+ swap(PaymentGateway, () => new FakeGateway());
161
+ }
162
+
163
+ export function spies() {
164
+ const send: Spy<[string], void> = spy<[string], void>();
165
+ notify(send);
166
+ void send.callCount;
167
+ void send.calledWith("hello");
168
+
169
+ const charge = spyOn(gateway, "charge");
170
+ charge.returns(receipt);
171
+ restoreSpies();
172
+ }
173
+
174
+ export async function controllingTime() {
175
+ freezeTime("2026-07-11T12:00:00Z");
176
+
177
+ const token = await jwt.sign({ sub: "1" }, { expiresIn: "1h" });
178
+ await jwt.verify(token);
179
+
180
+ timeTravel(61 * 60 * 1000);
181
+ await jwt.verify(token);
182
+
183
+ restoreTime();
184
+ }
185
+
186
+ export async function stateReset() {
187
+ resetState();
188
+ await truncate("comments", "posts", "users");
189
+ }
190
+
191
+ export async function databaseAssertions() {
192
+ await assertDatabaseHas("users", { email: "ada@example.com" });
193
+ await assertDatabaseHas("users", { active: 1 }, 1);
194
+ await assertDatabaseMissing("users", { email: "deleted@example.com" });
195
+ await assertDatabaseCount("users", 1);
196
+ await assertDatabaseEmpty("sessions");
197
+ }
198
+
199
+ declare function run(argv: string[]): Promise<void>;
200
+
201
+ export async function consoleTests(): Promise<CommandResult> {
202
+ const result = await runCommand(() => run(["node", "keel", "routes"]));
203
+
204
+ result
205
+ .assertSucceeded()
206
+ .assertOutputContains("GET /users")
207
+ .assertOutputMatches(/POST\s+\/users/);
208
+
209
+ result.assertExitCode(0);
210
+ void result.stdout;
211
+ void result.stderr;
212
+ void result.exitCode;
213
+
214
+ return result;
215
+ }
@@ -0,0 +1,141 @@
1
+ // Type-check harness for docs/transformers.md. Every type-checkable snippet in
2
+ // the guide 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
+ Transformer,
6
+ Model,
7
+ auth,
8
+ json,
9
+ type Attributes,
10
+ type DocumentOptions,
11
+ type Ctx,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ class Post extends Model {
15
+ static table = "posts";
16
+ declare id: number;
17
+ declare title: string;
18
+ declare author: User;
19
+ }
20
+
21
+ class User extends Model {
22
+ static table = "users";
23
+ declare id: number;
24
+ declare name: string;
25
+ declare email: string;
26
+ declare created_at: string;
27
+ declare admin: boolean;
28
+ declare role: string;
29
+ declare permissions: string[];
30
+ }
31
+
32
+ class PostTransformer extends Transformer<Post> {
33
+ transform(post: Post): Attributes {
34
+ return { id: post.id, title: post.title };
35
+ }
36
+ }
37
+
38
+ // Defining a transformer
39
+ export class BasicUserTransformer extends Transformer<User> {
40
+ transform(user: User): Attributes {
41
+ return {
42
+ id: user.id,
43
+ name: user.name,
44
+ joined: user.created_at,
45
+ };
46
+ }
47
+ }
48
+
49
+ // Constructor context + conditional fields + nesting + relations
50
+ export class UserTransformer extends Transformer<User> {
51
+ constructor(private viewerId: string | null) {
52
+ super();
53
+ }
54
+
55
+ transform(user: User): Attributes {
56
+ return {
57
+ id: user.id,
58
+ name: user.name,
59
+ email: this.when(String(user.id) === this.viewerId, user.email),
60
+ token: this.when(fresh, () => mintToken(user), null),
61
+ ...this.mergeWhen(user.admin, { role: user.role, permissions: user.permissions }),
62
+ author: new UserTransformer(this.viewerId).item(user),
63
+ posts: this.whenLoaded(user, "posts", new PostTransformer()),
64
+ roles: this.whenLoaded(user, "roles", (roles: { name: string }[]) =>
65
+ roles.map((r) => r.name),
66
+ ),
67
+ };
68
+ }
69
+ }
70
+
71
+ class WrappedUserTransformer extends Transformer<User> {
72
+ wrapKey = "user";
73
+ transform(user: User): Attributes {
74
+ return { id: user.id, name: user.name };
75
+ }
76
+ }
77
+
78
+ declare const user: User;
79
+ declare const list: User[];
80
+ declare const fresh: boolean;
81
+ declare const fetchedAt: string;
82
+ declare function mintToken(user: User): string;
83
+
84
+ // Transforming
85
+ export function transforming() {
86
+ const users = new UserTransformer(null);
87
+ json(users.item(user));
88
+ json(users.collection(list));
89
+ json(users.document(list, { meta: { total: list.length } }));
90
+ }
91
+
92
+ // item null passthrough
93
+ export function nullable() {
94
+ const out: Attributes | null = new BasicUserTransformer().item(null);
95
+ return out;
96
+ }
97
+
98
+ // Passing viewer context through the constructor
99
+ export function withViewer() {
100
+ return json(new UserTransformer(auth().id()).collection(list));
101
+ }
102
+
103
+ // Nesting / relations — load first, then transform
104
+ export async function relations() {
105
+ const users = await User.all<User>();
106
+ await User.load(users, "posts");
107
+ return json(new UserTransformer(null).collection(users));
108
+ }
109
+
110
+ // Response documents
111
+ export async function documents() {
112
+ const page = await User.all<User>();
113
+ json(
114
+ new UserTransformer(null).document(page, {
115
+ meta: { total: page.length, page: 1 },
116
+ }),
117
+ );
118
+
119
+ new WrappedUserTransformer().document(user); // { user: { … } }
120
+ new UserTransformer(null).document(user, { key: null, meta: { fetchedAt } });
121
+ new UserTransformer(null).document(user, { key: "records", meta: { total: 42 } });
122
+ }
123
+
124
+ // In a controller
125
+ export class UserController {
126
+ async show(c: Ctx) {
127
+ const found = await User.findOrFail<User>(c.req.param("id"));
128
+ return c.json(new UserTransformer(auth().id()).item(found));
129
+ }
130
+
131
+ async index(c: Ctx) {
132
+ const users = await User.all<User>();
133
+ await User.load(users, "posts");
134
+ return c.json(new UserTransformer(auth().id()).document(users));
135
+ }
136
+ }
137
+
138
+ // Types
139
+ const options: DocumentOptions = { key: "records", meta: { total: 42 } };
140
+ const shape: Attributes = { id: 1, name: "Ada" };
141
+ export { options, shape };
@@ -0,0 +1,49 @@
1
+ // Type-check harness for docs/transformers.md. Exercises every type-checkable
2
+ // snippet against the real exports so a renamed method or wrong signature fails
3
+ // `npm run typecheck:docs`. Compile-only — never executed.
4
+ import {
5
+ Transformer,
6
+ json,
7
+ type Attributes,
8
+ type DocumentOptions,
9
+ } from "@shaferllc/keel/core";
10
+
11
+ type User = { id: number; name: string; email: string; role: string; admin: boolean };
12
+ type Post = { id: number; title: string };
13
+
14
+ declare const isSelf: boolean;
15
+ declare const isAdmin: boolean;
16
+ declare const fresh: boolean;
17
+ declare function mint(): string;
18
+
19
+ class PostTransformer extends Transformer<Post> {
20
+ transform(post: Post): Attributes {
21
+ return { id: post.id, title: post.title };
22
+ }
23
+ }
24
+
25
+ class UserTransformer extends Transformer<User> {
26
+ transform(user: User): Attributes {
27
+ return {
28
+ id: user.id,
29
+ name: user.name,
30
+ email: this.when(isSelf, user.email), // key vanishes for others
31
+ token: this.when(fresh, () => mint(), null), // explicit fallback
32
+ ...this.mergeWhen(isAdmin, { role: user.role }),
33
+ posts: this.whenLoaded(user, "posts", new PostTransformer()),
34
+ roles: this.whenLoaded(user, "roles", (rs: { name: string }[]) => rs.map((r) => r.name)),
35
+ };
36
+ }
37
+ }
38
+
39
+ export function usage(user: User, users: User[]) {
40
+ const one: Attributes | null = new UserTransformer().item(user);
41
+ const many: Attributes[] = new UserTransformer().collection(users);
42
+ const wrapped: Attributes = new UserTransformer().document(users, {
43
+ meta: { total: users.length },
44
+ });
45
+ const opts: DocumentOptions = { key: "data", meta: { page: 1 } };
46
+ const custom: Attributes = new UserTransformer().document(user, opts);
47
+ const unwrapped: Attributes = new UserTransformer().document(user, { key: null });
48
+ return [json(one), json(many), json(wrapped), json(custom), json(unwrapped)];
49
+ }
@@ -0,0 +1,86 @@
1
+ // Type-check harness for docs/url-builder.md. Every type-checkable snippet in
2
+ // the reference is exercised here against the real exports, so a renamed method
3
+ // or wrong argument type fails `npm run typecheck:docs`. Compile-only — never
4
+ // executed.
5
+ import {
6
+ Router,
7
+ Container,
8
+ matchers,
9
+ type UrlOptions,
10
+ type SignedUrlOptions,
11
+ type Matcher,
12
+ } from "@shaferllc/keel/core";
13
+
14
+ // The router is resolved from the container in a real app; construct it
15
+ // directly here (as the test suite does).
16
+ const router = new Router(new Container());
17
+
18
+ // Controllers referenced by the doc's route registrations.
19
+ class UserController {}
20
+ class FileController {}
21
+ class PostController {}
22
+ class TeamController {}
23
+ class CodeController {}
24
+ class XController {}
25
+
26
+ export function building() {
27
+ router.get("/users/:id", [UserController, "show"]).name("users.show");
28
+
29
+ const a: string = router.url("users.show", { id: 42 });
30
+ const b: string = router.url("users.show", { id: 42 }, { qs: { tab: "posts", page: 2 } });
31
+
32
+ router.get("/files/:name", [FileController]).name("files.show");
33
+ const c: string = router.url("files.show", { name: "a/b c.txt" });
34
+
35
+ return { a, b, c };
36
+ }
37
+
38
+ export function optionalParams() {
39
+ router.get("/posts/:id?", [PostController, "show"]).name("posts.show");
40
+ const withId: string = router.url("posts.show", { id: 7 });
41
+ const without: string = router.url("posts.show", {});
42
+ return { withId, without };
43
+ }
44
+
45
+ export function errors() {
46
+ // Throws at runtime; type-checks fine.
47
+ return router.url("nope");
48
+ }
49
+
50
+ export async function signing() {
51
+ const url: string = await router.signedUrl("download", { id: 7 });
52
+ const expiring: string = await router.signedUrl(
53
+ "download",
54
+ { id: 7 },
55
+ { expiresIn: 3600 },
56
+ );
57
+ const valid: boolean = await router.hasValidSignature();
58
+ return { url, expiring, valid };
59
+ }
60
+
61
+ export function constraints() {
62
+ router.get("/users/:id", [UserController]).where("id", /\d+/);
63
+ router.get("/p/:slug", [PostController]).where("slug", { match: /[a-z0-9-]+/ });
64
+
65
+ router.get("/n/:id", [UserController]).where("id", matchers.number());
66
+ router.get("/t/:id", [TeamController]).where("id", matchers.uuid());
67
+ router.get("/s/:slug", [PostController]).where("slug", matchers.slug());
68
+ router.get("/c/:code", [CodeController]).where("code", matchers.alpha());
69
+
70
+ // Also reachable off the instance.
71
+ router.get("/i/:id", [XController]).where("id", router.matchers.number());
72
+ }
73
+
74
+ export function typeSeams() {
75
+ const opts: UrlOptions = { qs: { page: 2, tab: "posts" } };
76
+ router.url("users.show", { id: 1 }, opts);
77
+
78
+ const signed: SignedUrlOptions = { qs: { plan: "pro" }, expiresIn: 3600 };
79
+
80
+ const a: Matcher = /\d+/;
81
+ const b: Matcher = "[0-9]+";
82
+ const c: Matcher = { match: /[a-z-]+/ };
83
+ router.get("/x/:id", [XController]).where("id", a);
84
+
85
+ return { opts, signed, a, b, c };
86
+ }
@@ -0,0 +1,102 @@
1
+ // Type-check harness for docs/validation.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
+ json,
6
+ validate,
7
+ validateRequest,
8
+ validated,
9
+ request,
10
+ response,
11
+ ValidationException,
12
+ Router,
13
+ type Schema,
14
+ } from "@shaferllc/keel/core";
15
+ import { z } from "zod";
16
+
17
+ declare const Users: new (...a: unknown[]) => object;
18
+ declare const Posts: new (...a: unknown[]) => object;
19
+
20
+ export function declarative(router: Router) {
21
+ const NewUser = z.object({ email: z.string().email(), name: z.string().min(1) });
22
+ router.post("/users", [Users, "store"]).middleware([validateRequest({ body: NewUser })]);
23
+ router.get("/posts/:id", [Posts, "show"]).middleware([
24
+ validateRequest({
25
+ params: z.object({ id: z.coerce.number() }),
26
+ query: z.object({ page: z.coerce.number().min(1).default(1) }),
27
+ }),
28
+ ]);
29
+ }
30
+
31
+ export function readValidated() {
32
+ const user = validated<{ email: string; name: string }>("body");
33
+ const id = validated<{ id: number }>("params");
34
+ const page = validated<{ page: number }>("query");
35
+ return { user, id, page };
36
+ }
37
+
38
+ declare const email: string;
39
+ declare const age: number;
40
+ declare const err: unknown;
41
+
42
+ const NewUser = z.object({
43
+ email: z.string().email(),
44
+ age: z.number().min(18),
45
+ });
46
+
47
+ export class UserController {
48
+ async store() {
49
+ const data = await validate(NewUser); // { email: string; age: number }
50
+ return json({ created: data.email }, 201);
51
+ }
52
+ }
53
+
54
+ const Search = z.object({ q: z.string().min(1), page: z.coerce.number().default(1) });
55
+
56
+ export async function search() {
57
+ const { q, page } = await validate(Search, request.query());
58
+ return { q, page };
59
+ }
60
+
61
+ export async function mergedInput() {
62
+ const data = await validate(NewUser, await request.all());
63
+ return data;
64
+ }
65
+
66
+ export function handleError() {
67
+ if (err instanceof ValidationException) {
68
+ return response.json({ fields: err.errors }, 422);
69
+ }
70
+ }
71
+
72
+ export async function reference() {
73
+ const fromBody = await validate(NewUser);
74
+ const fromData = await validate(NewUser, { email, age });
75
+ return { fromBody, fromData };
76
+ }
77
+
78
+ // Schema<T> — hand-rolled implementation
79
+ const Positive: Schema<number> = {
80
+ safeParse: (data) =>
81
+ typeof data === "number" && data > 0
82
+ ? { success: true, data }
83
+ : { success: false, error: { issues: [{ path: [], message: "must be > 0" }] } },
84
+ };
85
+
86
+ export async function customSchema() {
87
+ const n = await validate(Positive, 42); // number
88
+ return n;
89
+ }
90
+
91
+ // ValidationException seam
92
+ export async function exceptionShape() {
93
+ try {
94
+ await validate(NewUser);
95
+ } catch (e) {
96
+ if (e instanceof ValidationException) {
97
+ const status: number = e.status;
98
+ const errors: Record<string, string[]> = e.errors;
99
+ return { status, errors };
100
+ }
101
+ }
102
+ }