@shaferllc/keel 0.59.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
@@ -0,0 +1,530 @@
1
+ # Database
2
+
3
+ Keel ships a small, **driver-agnostic query builder**. It generates
4
+ parameterized SQL and runs it through a `Connection` you provide — so it works
5
+ with any driver (Cloudflare D1, Neon/Postgres, PlanetScale, Turso, better-sqlite3,
6
+ `pg`). The core never imports a database driver, so it stays edge-safe.
7
+
8
+ ## Connect
9
+
10
+ Register a connection once, in a service provider. The `Connection` interface is
11
+ two methods — adapt them to your driver:
12
+
13
+ ```ts
14
+ import { setConnection, type Connection } from "@shaferllc/keel/core";
15
+
16
+ const connection: Connection = {
17
+ select: (sql, bindings) => d1.prepare(sql).bind(...bindings).all().then((r) => r.results),
18
+ write: async (sql, bindings) => {
19
+ const r = await d1.prepare(sql).bind(...bindings).run();
20
+ return { rowsAffected: r.meta.changes, insertId: r.meta.last_row_id };
21
+ },
22
+ };
23
+
24
+ setConnection(connection, "sqlite"); // "sqlite" | "mysql" | "postgres"
25
+ ```
26
+
27
+ The dialect only affects placeholder style (`?` vs Postgres `$1`). `select`
28
+ returns the rows (`Row[]`); `write` returns a `WriteResult` (`rowsAffected`,
29
+ optional `insertId`). Everything the builder does bottoms out in these two
30
+ methods, which is why the same app runs on Node and the edge — only the
31
+ connection changes. The type parameter on `db<T>()` types the *results*; the
32
+ `Connection` itself just deals in `Row`s.
33
+
34
+ ### Ready-made adapters
35
+
36
+ You don't have to hand-write the bridge for the common drivers — Keel ships
37
+ `Connection` adapters as optional subpath imports. Each takes your driver
38
+ instance and returns a `Connection`. They import no driver themselves (the client
39
+ is duck-typed), so Keel's core stays dependency-free and nothing is bundled until
40
+ you import it:
41
+
42
+ ```ts
43
+ // Cloudflare D1 (sqlite)
44
+ import { d1Connection } from "@shaferllc/keel/db/d1";
45
+ setConnection(d1Connection(env.DB), "sqlite");
46
+
47
+ // Postgres — pg (Node) or @neondatabase/serverless (edge)
48
+ import { pgConnection } from "@shaferllc/keel/db/pg";
49
+ import { Pool } from "pg";
50
+ setConnection(pgConnection(new Pool({ connectionString })), "postgres");
51
+
52
+ // libSQL / Turso (sqlite, Node + edge)
53
+ import { libsqlConnection } from "@shaferllc/keel/db/libsql";
54
+ import { createClient } from "@libsql/client";
55
+ setConnection(libsqlConnection(createClient({ url, authToken })), "sqlite");
56
+ ```
57
+
58
+ Install only the driver you use — it's a peer, not a Keel dependency. **Postgres
59
+ note:** `INSERT` returns an id only with a `RETURNING` clause, so `insertGetId()`
60
+ needs `RETURNING id` on Postgres; the D1 and libSQL adapters return the last
61
+ insert id natively.
62
+
63
+ ## Multiple databases
64
+
65
+ `setConnection` registers the *default* connection. To talk to more than one
66
+ database at once — a Postgres primary and a SQLite/D1 cache, a separate reporting
67
+ warehouse, a per-tenant shard — register each by name with `addConnection`, and
68
+ each keeps its own dialect:
69
+
70
+ ```ts
71
+ import { setConnection, addConnection } from "@shaferllc/keel/core";
72
+
73
+ setConnection(primary, "postgres"); // the default
74
+ addConnection("reporting", warehouse, "postgres");
75
+ addConnection("cache", d1Cache, "sqlite");
76
+ ```
77
+
78
+ Route a single query with a second argument to `db()`:
79
+
80
+ ```ts
81
+ await db("users").where("active", true).get(); // default
82
+ await db("events", "reporting").where("kind", "signup").count();
83
+ ```
84
+
85
+ Or grab a reusable handle with `connection(name)` — it exposes `table()` plus the
86
+ raw `select`/`write` bridge, all dialect-adjusted for that database:
87
+
88
+ ```ts
89
+ import { connection } from "@shaferllc/keel/core";
90
+
91
+ const reporting = connection("reporting");
92
+ await reporting.table("events").latest().limit(100).get();
93
+ await reporting.write("REFRESH MATERIALIZED VIEW daily_signups", []);
94
+ ```
95
+
96
+ A whole [model](./models.md) can live on a connection — set `static connection`
97
+ and every query, save, and relation for that model routes there:
98
+
99
+ ```ts
100
+ class Event extends Model {
101
+ static table = "events";
102
+ static connection = "reporting"; // reads, writes, and relations use "reporting"
103
+ }
104
+ ```
105
+
106
+ `setDefaultConnection(name)` switches which registered connection the unnamed
107
+ `db(table)` (and any model without a `static connection`) uses — handy for
108
+ request-scoped tenant selection. `connectionNames()` lists what's registered.
109
+ An unregistered connection name doesn't fail when you *build* a query, only when
110
+ it runs — so a misconfigured name surfaces as a rejected read/write, not a
111
+ construction-time throw.
112
+
113
+ ## Querying
114
+
115
+ Start a query with `db(table)`, chain constraints, and finish with a terminal
116
+ method (`get`, `first`, `count`, `exists`):
117
+
118
+ ```ts
119
+ import { db } from "@shaferllc/keel/core";
120
+
121
+ await db("users").where("active", true).orderBy("name").get();
122
+ await db("users").where("id", 1).first(); // row | null
123
+ await db("users").where("age", ">", 18).count();
124
+ await db("posts").whereIn("id", [1, 2, 3]).get();
125
+ await db("posts").whereNull("deleted_at").limit(20).offset(40).get();
126
+
127
+ await db("orders")
128
+ .select("id", "total")
129
+ .where("status", "paid")
130
+ .orWhere("status", "shipped")
131
+ .get();
132
+ ```
133
+
134
+ Constraint methods return the builder, so they chain in any order; the query
135
+ isn't sent until you call a terminal method. Multiple `where` calls combine with
136
+ `AND`; `orWhere` joins with `OR`.
137
+
138
+ More `where` clauses and ordering shortcuts:
139
+
140
+ ```ts
141
+ await db("posts").whereBetween("views", [10, 100]).get();
142
+ await db("posts").whereNotIn("id", [4, 5]).get();
143
+ await db("posts").whereLike("title", "%keel%").get();
144
+ await db("posts").latest().get(); // ORDER BY created_at DESC
145
+ await db("posts").oldest("published_at").get();
146
+ ```
147
+
148
+ ## Aggregates, single values, and pagination
149
+
150
+ ```ts
151
+ await db("orders").where("paid", true).sum("total"); // number
152
+ await db("orders").avg("total");
153
+ await db("orders").min("total");
154
+ await db("orders").max("total");
155
+
156
+ await db("users").where("id", 1).value("email"); // one column, first row
157
+ await db("posts").pluck("title"); // string[] of one column
158
+
159
+ const page = await db("posts").latest().paginate(2, 15); // { data, total, perPage, currentPage, lastPage }
160
+ ```
161
+
162
+ `paginate(page, perPage)` runs a `COUNT` then a `LIMIT`/`OFFSET` query and returns
163
+ a `Paginated<T>` with the page and the metadata to render pager controls.
164
+
165
+ ## Writing
166
+
167
+ ```ts
168
+ const id = await db("users").insertGetId({ email, name });
169
+ await db("users").where("id", id).update({ name: "Grace" });
170
+ await db("users").where("id", id).delete();
171
+ ```
172
+
173
+ Everything is parameterized — values become bindings, never string-interpolated
174
+ SQL — so it's injection-safe by construction. Writes return a `WriteResult`;
175
+ `insertGetId` unwraps it to just the new id.
176
+
177
+ > **Guard your writes.** `update()` and `delete()` apply to every row that
178
+ > matches the current `where` clause — with no `where`, that's the whole table.
179
+ > Always scope a write with `where` unless you truly mean to touch every row.
180
+
181
+ ## Typed rows
182
+
183
+ Pass a row type for typed results — it flows through to `get()` and `first()`:
184
+
185
+ ```ts
186
+ type User = {
187
+ id: number;
188
+ email: string;
189
+ };
190
+ const user = await db<User>("users").where("id", 1).first(); // User | null
191
+ const all = await db<User>("users").get(); // User[]
192
+ ```
193
+
194
+ The type is a compile-time convenience; it doesn't validate the shape at runtime.
195
+
196
+ > Use a `type` alias, not an `interface`, for the row type. The builder's type
197
+ > parameter is constrained to `Row` (`Record<string, unknown>`), which a `type`
198
+ > satisfies via an implicit index signature but an `interface` does not.
199
+
200
+ ## Related
201
+
202
+ An active-record [`Model`](./models.md) layer and [migrations](./migrations.md)
203
+ build on this builder — reach for them for CRUD and schema work, and drop back to
204
+ `db()` for anything they don't cover.
205
+
206
+ ---
207
+
208
+ ## API reference
209
+
210
+ ### `db(table)`
211
+
212
+ `db<T extends Row = Row>(table: string, connectionName?: string): QueryBuilder<T>`
213
+
214
+ Starts a new query against `table`, on the default connection or a named one.
215
+ The optional type parameter types the rows returned by `get()`/`first()`.
216
+
217
+ ```ts
218
+ db("users"); // QueryBuilder<Row>, default connection
219
+ db<{ id: number }>("users"); // typed rows
220
+ db("events", "reporting"); // the "reporting" connection
221
+ ```
222
+
223
+ **Notes:** returns a fresh builder each call — nothing is shared between queries.
224
+ No SQL runs until a terminal method (`get`/`first`/`count`/`exists`) or a write
225
+ (`insert`/`update`/`delete`) is awaited.
226
+
227
+ ### `setConnection(conn, dialect?)`
228
+
229
+ `setConnection(conn: Connection, driverDialect?: Dialect): void`
230
+
231
+ Registers the connection every `db()` query runs through, plus the dialect
232
+ (default `"sqlite"`).
233
+
234
+ ```ts
235
+ setConnection(connection, "postgres");
236
+ ```
237
+
238
+ **Notes:** registers the `"default"` connection — the last call wins. Calling
239
+ `db()` before any connection is registered throws `No database connection…` on
240
+ the first query. The dialect only changes placeholder rendering (`?` → `$1, $2`
241
+ for Postgres).
242
+
243
+ ### `addConnection(name, conn, dialect?)`
244
+
245
+ `addConnection(name: string, conn: Connection, driverDialect?: Dialect): void`
246
+
247
+ Registers a *named* connection alongside the default and any others — the way to
248
+ use more than one database. Reach it with `db(table, name)`, `connection(name)`,
249
+ or a model's `static connection = name`.
250
+
251
+ ```ts
252
+ addConnection("reporting", warehouse, "postgres");
253
+ ```
254
+
255
+ ### `connection(name?)`
256
+
257
+ `connection(name?: string): ConnectionHandle`
258
+
259
+ Returns a handle to a registered connection (or the default): `table(name)` to
260
+ start a query, `select`/`write` for raw SQL (dialect-adjusted, `?` placeholders),
261
+ and `dialect`.
262
+
263
+ ```ts
264
+ const reporting = connection("reporting");
265
+ await reporting.table("events").count();
266
+ await reporting.select("SELECT 1", []);
267
+ ```
268
+
269
+ ### `setDefaultConnection(name)` · `connectionNames()` · `clearConnections()`
270
+
271
+ `setDefaultConnection(name: string)` picks which registered connection the
272
+ unnamed `db(table)` and connectionless models use (throws if `name` isn't
273
+ registered). `connectionNames()` returns the registered names.
274
+ `clearConnections()` unregisters everything — a test helper.
275
+
276
+ ### `QueryBuilder`
277
+
278
+ Returned by `db()`. Constraint methods return `this` (chainable); terminal
279
+ methods return a promise. You never construct it directly.
280
+
281
+ #### `select(...columns)`
282
+
283
+ `select(...columns: string[]): this`
284
+
285
+ Restricts the selected columns. With no arguments, selects `*`.
286
+
287
+ ```ts
288
+ db("users").select("id", "email").get();
289
+ ```
290
+
291
+ **Notes:** column names are interpolated as-is (they are not parameterized), so
292
+ never pass user input as a column name. Calling it again replaces the prior
293
+ selection.
294
+
295
+ #### `where(column, value)` / `where(column, operator, value)`
296
+
297
+ `where(column: string, value: unknown): this`
298
+ `where(column: string, operator: Operator, value: unknown): this`
299
+
300
+ Adds an `AND` condition. The two-argument form uses `=`; the three-argument form
301
+ takes an explicit operator.
302
+
303
+ ```ts
304
+ db("users").where("active", true);
305
+ db("users").where("age", ">", 18);
306
+ db("users").where("email", "like", "%@example.com");
307
+ ```
308
+
309
+ **Notes:** `Operator` is `"=" | "!=" | "<" | "<=" | ">" | ">=" | "like"`. Values
310
+ are always parameterized. Chaining multiple `where`s combines them with `AND`.
311
+
312
+ #### `orWhere(column, value)` / `orWhere(column, operator, value)`
313
+
314
+ `orWhere(column: string, value: unknown): this`
315
+ `orWhere(column: string, operator: Operator, value: unknown): this`
316
+
317
+ Same as `where`, but joins the condition with `OR`.
318
+
319
+ ```ts
320
+ db("orders").where("status", "paid").orWhere("status", "shipped").get();
321
+ ```
322
+
323
+ **Notes:** conditions are combined left-to-right without grouping parentheses, so
324
+ mixing `where` and `orWhere` follows SQL's `AND`/`OR` precedence — group complex
325
+ logic in separate queries if you need explicit parenthesization.
326
+
327
+ #### `whereIn(column, values)`
328
+
329
+ `whereIn(column: string, values: unknown[]): this`
330
+
331
+ Matches rows where `column` is any of `values` (`AND`-joined).
332
+
333
+ ```ts
334
+ db("posts").whereIn("id", [1, 2, 3]).get();
335
+ ```
336
+
337
+ **Notes:** each value becomes its own placeholder. An empty array produces
338
+ `IN ()`, which most engines reject — guard against empty lists yourself.
339
+
340
+ #### `whereNull(column)` / `whereNotNull(column)`
341
+
342
+ `whereNull(column: string): this`
343
+ `whereNotNull(column: string): this`
344
+
345
+ Adds an `AND` `IS NULL` / `IS NOT NULL` condition — no binding.
346
+
347
+ ```ts
348
+ db("posts").whereNull("deleted_at").get();
349
+ db("users").whereNotNull("verified_at").get();
350
+ ```
351
+
352
+ #### `orderBy(column, direction?)`
353
+
354
+ `orderBy(column: string, direction?: "asc" | "desc"): this`
355
+
356
+ Adds an `ORDER BY` clause (default `"asc"`). Call it repeatedly for multiple sort
357
+ keys, applied in call order.
358
+
359
+ ```ts
360
+ db("users").orderBy("last_name").orderBy("created_at", "desc").get();
361
+ ```
362
+
363
+ **Notes:** the column is interpolated, not parameterized — don't pass user input.
364
+
365
+ #### `limit(n)` / `offset(n)`
366
+
367
+ `limit(n: number): this`
368
+ `offset(n: number): this`
369
+
370
+ Caps the number of rows / skips the first `n`. Together they paginate.
371
+
372
+ ```ts
373
+ db("posts").limit(20).offset(40).get(); // page 3, 20 per page
374
+ ```
375
+
376
+ **Notes:** `first()` sets `limit(1)` internally, overriding any prior `limit`.
377
+
378
+ #### `get()`
379
+
380
+ `get(): Promise<T[]>`
381
+
382
+ Runs the SELECT and returns all matching rows.
383
+
384
+ ```ts
385
+ const rows = await db("users").where("active", true).get();
386
+ ```
387
+
388
+ #### `first()`
389
+
390
+ `first(): Promise<T | null>`
391
+
392
+ Runs the SELECT with `LIMIT 1` and returns the first row, or `null`.
393
+
394
+ ```ts
395
+ const user = await db("users").where("email", email).first();
396
+ ```
397
+
398
+ **Notes:** overrides any `limit` you set. Returns `null` (not `undefined`) when
399
+ nothing matches.
400
+
401
+ #### `count()`
402
+
403
+ `count(): Promise<number>`
404
+
405
+ Returns `COUNT(*)` for the current `where` clause.
406
+
407
+ ```ts
408
+ const active = await db("users").where("active", true).count();
409
+ ```
410
+
411
+ **Notes:** ignores `select`, `orderBy`, `limit`, and `offset` — it counts matching
412
+ rows, not the paginated slice.
413
+
414
+ #### `exists()`
415
+
416
+ `exists(): Promise<boolean>`
417
+
418
+ `true` when at least one row matches — a `count() > 0` shorthand.
419
+
420
+ ```ts
421
+ if (await db("users").where("email", email).exists()) { /* taken */ }
422
+ ```
423
+
424
+ #### `insert(data)`
425
+
426
+ `insert(data: Row): Promise<WriteResult>`
427
+
428
+ Inserts one row and returns write metadata.
429
+
430
+ ```ts
431
+ const result = await db("users").insert({ email, name });
432
+ result.rowsAffected; // 1
433
+ result.insertId; // driver-dependent
434
+ ```
435
+
436
+ **Notes:** column order follows `Object.keys(data)`. `insertId` is only populated
437
+ if the driver reports it in `WriteResult`.
438
+
439
+ #### `insertGetId(data)`
440
+
441
+ `insertGetId(data: Row): Promise<number | string | undefined>`
442
+
443
+ Inserts one row and returns just its new id (`insert` unwrapped).
444
+
445
+ ```ts
446
+ const id = await db("users").insertGetId({ email, name });
447
+ ```
448
+
449
+ **Notes:** returns `undefined` when the driver doesn't report an `insertId`.
450
+
451
+ #### `update(data)`
452
+
453
+ `update(data: Row): Promise<WriteResult>`
454
+
455
+ Updates every row matching the `where` clause, setting the given columns.
456
+
457
+ ```ts
458
+ const r = await db("users").where("id", 1).update({ name: "Grace" });
459
+ r.rowsAffected; // rows changed
460
+ ```
461
+
462
+ **Notes:** with no `where`, updates the entire table. Bindings are the new values
463
+ followed by the where-clause values.
464
+
465
+ #### `delete()`
466
+
467
+ `delete(): Promise<WriteResult>`
468
+
469
+ Deletes every row matching the `where` clause.
470
+
471
+ ```ts
472
+ await db("sessions").where("expires_at", "<", now).delete();
473
+ ```
474
+
475
+ **Notes:** with no `where`, empties the table. There's no soft-delete here — pair
476
+ with a `deleted_at` column and `whereNull` if you want one.
477
+
478
+ ### Interfaces & types
479
+
480
+ #### `Connection`
481
+
482
+ ```ts
483
+ interface Connection {
484
+ select<T = Row>(sql: string, bindings: unknown[]): Promise<T[]>;
485
+ write(sql: string, bindings: unknown[]): Promise<WriteResult>;
486
+ }
487
+ ```
488
+
489
+ The seam between the builder and your driver. `select` runs any row-returning
490
+ query and resolves to the rows; `write` runs an INSERT/UPDATE/DELETE and resolves
491
+ to a `WriteResult`. Implement it once per driver — the two-method surface is
492
+ deliberately tiny so any driver (or a mock in tests) fits.
493
+
494
+ ```ts
495
+ const mock: Connection = {
496
+ select: async () => [{ id: 1 }],
497
+ write: async () => ({ rowsAffected: 1, insertId: 1 }),
498
+ };
499
+ ```
500
+
501
+ #### `WriteResult`
502
+
503
+ ```ts
504
+ interface WriteResult {
505
+ rowsAffected: number;
506
+ insertId?: number | string;
507
+ }
508
+ ```
509
+
510
+ Returned by `write` (and thus `insert`/`update`/`delete`). `insertId` is optional
511
+ because not every driver or statement produces one.
512
+
513
+ #### `Row`
514
+
515
+ `type Row = Record<string, unknown>`
516
+
517
+ A database row — the default shape for query results and write payloads.
518
+
519
+ #### `Dialect`
520
+
521
+ `type Dialect = "sqlite" | "mysql" | "postgres"`
522
+
523
+ Selects placeholder rendering. Only Postgres differs (`$1, $2, …`); the others
524
+ use `?`.
525
+
526
+ #### `Operator`
527
+
528
+ `type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like"`
529
+
530
+ The comparison operators accepted by the three-argument `where`/`orWhere`.
@@ -0,0 +1,129 @@
1
+ # Debugging
2
+
3
+ Two helpers for the moments you'd otherwise reach for `console.log`. Both are
4
+ edge-safe — `dump()` is a plain `console.log`, and `dd()` throws a
5
+ self-rendering exception, so neither needs any runtime-specific support.
6
+
7
+ ## dump
8
+
9
+ `dump(...values)` prints to the console and **returns its first argument**, so
10
+ you can drop it inline without restructuring code:
11
+
12
+ ```ts
13
+ import { dump } from "@shaferllc/keel/core";
14
+
15
+ dump(user, order); // logs both, execution continues
16
+
17
+ const total = dump(computeTotal()); // logs the total AND uses it
18
+ ```
19
+
20
+ Every log is prefixed with `⚓ dump →` so your probes are easy to spot (and
21
+ easy to grep out later). Because it returns the first value unchanged, you can
22
+ wrap it around any expression — an argument, a return value, a link in a chain —
23
+ without changing what the code does:
24
+
25
+ ```ts
26
+ return dump(await user.save()); // inspect the saved model, still return it
27
+ ```
28
+
29
+ `dump()` hands your values straight to `console.log`, so the runtime's own
30
+ formatter renders them — objects stay inspectable, not flattened to a string.
31
+ (The safe JSON rendering below is `dd()`'s job, not `dump()`'s.)
32
+
33
+ > `dump()` requires at least one argument — its signature is
34
+ > `(...values: [T, ...unknown[]])`. Calling `dump()` with no arguments is a type
35
+ > error, which stops you from leaving a probe that prints nothing.
36
+
37
+ ## dd — dump and die
38
+
39
+ `dd(...values)` dumps to the **browser** and halts the request — a readable HTML
40
+ page with each value pretty-printed. Perfect for inspecting state mid-request:
41
+
42
+ ```ts
43
+ import { dd } from "@shaferllc/keel/core";
44
+
45
+ store() {
46
+ dd(await request.all(), request.headers());
47
+ // never reached
48
+ }
49
+ ```
50
+
51
+ Its return type is `never`: `dd()` throws, so nothing after it runs and
52
+ TypeScript knows the following code is unreachable. Each value is serialized
53
+ with a **safe** JSON stringifier before it hits the page, so the usual
54
+ `JSON.stringify` hazards don't crash the dump:
55
+
56
+ - **circular references** render as `[Circular]` instead of throwing;
57
+ - **functions** render as `[Function: name]` (or `[Function: anonymous]`);
58
+ - **bigints** render as `123n` instead of throwing;
59
+ - **`undefined`** renders as `[undefined]` instead of vanishing.
60
+
61
+ Values are HTML-escaped before rendering, so dumping a string full of `<`, `>`,
62
+ or `&` shows the literal text rather than injecting markup.
63
+
64
+ Under the hood `dd()` throws a self-rendering exception (see
65
+ [Errors](./errors.md)), so it works the same on Node and the edge — no special
66
+ runtime support needed. The exception carries a **200** status, so the dump page
67
+ returns `200 OK`, not an error status — this is a deliberate inspection tool,
68
+ not an error path.
69
+
70
+ > **Shared references, not just cycles.** The safe stringifier tracks every
71
+ > object it has seen and never forgets one, so the *same* object appearing twice
72
+ > in unrelated places (siblings, not an actual cycle) renders as `[Circular]` on
73
+ > its second appearance. If a dump shows an unexpected `[Circular]`, that's why —
74
+ > the value isn't necessarily cyclic, just repeated.
75
+
76
+ ## Turning on framework debug output
77
+
78
+ Set `APP_DEBUG=true` (i.e. `config('app.debug')`) to get full error pages with
79
+ stack traces from the kernel. Turn it off in production so internals stay hidden.
80
+ See [Errors](./errors.md) for how responses change between debug and production.
81
+
82
+ ---
83
+
84
+ ## API reference
85
+
86
+ Both functions are top-level exports — there are no classes to construct or
87
+ interfaces to implement. The `DumpException` that `dd()` throws is internal; you
88
+ never reference it directly.
89
+
90
+ ### `dump(...values)`
91
+
92
+ `dump<T>(...values: [T, ...unknown[]]): T`
93
+
94
+ Logs all values to the console (prefixed `⚓ dump →`) and returns the first one,
95
+ so it can be dropped inline.
96
+
97
+ ```ts
98
+ import { dump } from "@shaferllc/keel/core";
99
+
100
+ const total = dump(computeTotal()); // logs, then flows the value onward
101
+ dump(user, order, request); // logs all three, returns `user`
102
+ ```
103
+
104
+ **Notes:** requires at least one argument (the tuple type `[T, ...unknown[]]`
105
+ enforces it). Returns `values[0]` unchanged — never a copy — so it's safe to wrap
106
+ around any expression. Uses `console.log` directly, so formatting is the
107
+ runtime's, not the safe stringifier; it does not halt execution.
108
+
109
+ ### `dd(...values)`
110
+
111
+ `dd(...values: unknown[]): never`
112
+
113
+ Dumps every value to a self-rendering HTML page and halts the request — "dump
114
+ and die".
115
+
116
+ ```ts
117
+ import { dd } from "@shaferllc/keel/core";
118
+
119
+ dd(await request.all(), request.headers());
120
+ // unreachable — dd() throws
121
+ ```
122
+
123
+ **Notes:** returns `never` — it throws an internal `DumpException` (a
124
+ self-handling `HttpException`) rather than returning, so any code after it is
125
+ unreachable. The rendered page returns status **200**, not an error code. Values
126
+ are serialized with the safe stringifier (circular refs → `[Circular]`,
127
+ functions → `[Function: …]`, bigints → `123n`, `undefined` → `[undefined]`) and
128
+ HTML-escaped before rendering. Accepts zero or more arguments; `dd()` with no
129
+ arguments still halts the request and renders an empty page.