@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,631 @@
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
+ ## Transactions
182
+
183
+ Two related writes should either both land or neither should. `transaction()`
184
+ commits when your callback returns and **rolls back if it throws**:
185
+
186
+ ```ts
187
+ import { transaction, db } from "@shaferllc/keel/core";
188
+
189
+ await transaction(async () => {
190
+ await db("orders").insert(order);
191
+ await db("stock").where("id", id).decrement("count"); // a throw here undoes the insert
192
+ });
193
+ ```
194
+
195
+ The error still reaches you — it's rethrown after the rollback. Nothing is
196
+ swallowed.
197
+
198
+ ### Queries inside are ambient
199
+
200
+ You don't have to thread a transaction object through your code. `db()`, models,
201
+ and relations all pick up the open transaction automatically:
202
+
203
+ ```ts
204
+ await transaction(async () => {
205
+ const user = await User.create({ email }); // the model is in the transaction
206
+ await user.related("posts").create({ title }); // so is the relation
207
+ await db("audit").insert({ userId: user.id }); // and the raw builder
208
+ });
209
+ ```
210
+
211
+ That works because the transaction lives in `AsyncLocalStorage`, not a module
212
+ global — so two requests running transactions at the same time can't steal each
213
+ other's connection.
214
+
215
+ If you'd rather be explicit, the callback gets a handle:
216
+
217
+ ```ts
218
+ await transaction(async (tx) => {
219
+ await tx.table("orders").insert(order);
220
+ await tx.write("UPDATE stock SET count = count - 1 WHERE id = ?", [id]);
221
+ });
222
+ ```
223
+
224
+ `tx.rollback()` abandons the transaction without committing. `inTransaction()`
225
+ tells you whether one is open.
226
+
227
+ ### Nesting uses savepoints
228
+
229
+ A `transaction()` inside another doesn't open a second one — databases don't have
230
+ those. It takes a **savepoint**, so an inner failure rolls back only the inner
231
+ work and the outer transaction carries on:
232
+
233
+ ```ts
234
+ await transaction(async () => {
235
+ await db("orders").insert(order); // survives
236
+
237
+ try {
238
+ await transaction(async () => {
239
+ await db("items").insert(item);
240
+ throw new Error("out of stock"); // only this is rolled back
241
+ });
242
+ } catch {
243
+ // handle it
244
+ }
245
+
246
+ await db("audit").insert(entry); // still in the outer transaction
247
+ });
248
+
249
+ // the outer transaction commits: the order and the audit row are both saved
250
+ ```
251
+
252
+ Without savepoints, a nested helper's failure would silently abandon its caller's
253
+ writes too — which is the sort of bug you find in production, months later.
254
+
255
+ ### Drivers and the pooling trap
256
+
257
+ A transaction needs every statement to run on **one** connection. A connection
258
+ *pool* hands each statement to whichever connection is free — so issuing `BEGIN`
259
+ through a pool wraps nothing: the `INSERT` after it can land on a different
260
+ connection entirely, the `COMMIT` commits nothing, and a failure half-writes.
261
+ It looks like it works. It doesn't.
262
+
263
+ So a pooled driver implements `begin()` on its `Connection`, checking one
264
+ connection out and running the whole transaction on it. Keel's Postgres adapter
265
+ does this automatically when you hand it a `Pool` (it checks for `connect()`), and
266
+ releases the connection afterwards even if the `COMMIT` throws.
267
+
268
+ | Driver | Transactions |
269
+ |--------|--------------|
270
+ | Postgres (`Pool`) | ✅ a dedicated connection is checked out |
271
+ | Postgres (`Client`), SQLite, libSQL | ✅ `BEGIN` / `COMMIT` on the one connection they have |
272
+ | **Cloudflare D1** | ❌ — no interactive transactions; use `database.batch([...])` |
273
+
274
+ D1 can't hold a transaction open across awaits, so `transaction()` on it **throws
275
+ a clear error** rather than letting a `BEGIN` fail cryptically. A transaction that
276
+ quietly isn't one is far worse than one that refuses to start.
277
+
278
+ Writing your own driver? Implement `begin(): Promise<TransactionConnection>` if it
279
+ pools. If it owns a single connection, you can leave it out and Keel will use
280
+ `BEGIN`/`COMMIT`/`ROLLBACK`.
281
+
282
+ ## Typed rows
283
+
284
+ Pass a row type for typed results — it flows through to `get()` and `first()`:
285
+
286
+ ```ts
287
+ type User = {
288
+ id: number;
289
+ email: string;
290
+ };
291
+ const user = await db<User>("users").where("id", 1).first(); // User | null
292
+ const all = await db<User>("users").get(); // User[]
293
+ ```
294
+
295
+ The type is a compile-time convenience; it doesn't validate the shape at runtime.
296
+
297
+ > Use a `type` alias, not an `interface`, for the row type. The builder's type
298
+ > parameter is constrained to `Row` (`Record<string, unknown>`), which a `type`
299
+ > satisfies via an implicit index signature but an `interface` does not.
300
+
301
+ ## Related
302
+
303
+ An active-record [`Model`](./models.md) layer and [migrations](./migrations.md)
304
+ build on this builder — reach for them for CRUD and schema work, and drop back to
305
+ `db()` for anything they don't cover.
306
+
307
+ ---
308
+
309
+ ## API reference
310
+
311
+ ### `db(table)`
312
+
313
+ `db<T extends Row = Row>(table: string, connectionName?: string): QueryBuilder<T>`
314
+
315
+ Starts a new query against `table`, on the default connection or a named one.
316
+ The optional type parameter types the rows returned by `get()`/`first()`.
317
+
318
+ ```ts
319
+ db("users"); // QueryBuilder<Row>, default connection
320
+ db<{ id: number }>("users"); // typed rows
321
+ db("events", "reporting"); // the "reporting" connection
322
+ ```
323
+
324
+ **Notes:** returns a fresh builder each call — nothing is shared between queries.
325
+ No SQL runs until a terminal method (`get`/`first`/`count`/`exists`) or a write
326
+ (`insert`/`update`/`delete`) is awaited.
327
+
328
+ ### `setConnection(conn, dialect?)`
329
+
330
+ `setConnection(conn: Connection, driverDialect?: Dialect): void`
331
+
332
+ Registers the connection every `db()` query runs through, plus the dialect
333
+ (default `"sqlite"`).
334
+
335
+ ```ts
336
+ setConnection(connection, "postgres");
337
+ ```
338
+
339
+ **Notes:** registers the `"default"` connection — the last call wins. Calling
340
+ `db()` before any connection is registered throws `No database connection…` on
341
+ the first query. The dialect only changes placeholder rendering (`?` → `$1, $2`
342
+ for Postgres).
343
+
344
+ ### `addConnection(name, conn, dialect?)`
345
+
346
+ `addConnection(name: string, conn: Connection, driverDialect?: Dialect): void`
347
+
348
+ Registers a *named* connection alongside the default and any others — the way to
349
+ use more than one database. Reach it with `db(table, name)`, `connection(name)`,
350
+ or a model's `static connection = name`.
351
+
352
+ ```ts
353
+ addConnection("reporting", warehouse, "postgres");
354
+ ```
355
+
356
+ ### `connection(name?)`
357
+
358
+ `connection(name?: string): ConnectionHandle`
359
+
360
+ Returns a handle to a registered connection (or the default): `table(name)` to
361
+ start a query, `select`/`write` for raw SQL (dialect-adjusted, `?` placeholders),
362
+ and `dialect`.
363
+
364
+ ```ts
365
+ const reporting = connection("reporting");
366
+ await reporting.table("events").count();
367
+ await reporting.select("SELECT 1", []);
368
+ ```
369
+
370
+ ### `setDefaultConnection(name)` · `connectionNames()` · `clearConnections()`
371
+
372
+ `setDefaultConnection(name: string)` picks which registered connection the
373
+ unnamed `db(table)` and connectionless models use (throws if `name` isn't
374
+ registered). `connectionNames()` returns the registered names.
375
+ `clearConnections()` unregisters everything — a test helper.
376
+
377
+ ### `QueryBuilder`
378
+
379
+ Returned by `db()`. Constraint methods return `this` (chainable); terminal
380
+ methods return a promise. You never construct it directly.
381
+
382
+ #### `select(...columns)`
383
+
384
+ `select(...columns: string[]): this`
385
+
386
+ Restricts the selected columns. With no arguments, selects `*`.
387
+
388
+ ```ts
389
+ db("users").select("id", "email").get();
390
+ ```
391
+
392
+ **Notes:** column names are interpolated as-is (they are not parameterized), so
393
+ never pass user input as a column name. Calling it again replaces the prior
394
+ selection.
395
+
396
+ #### `where(column, value)` / `where(column, operator, value)`
397
+
398
+ `where(column: string, value: unknown): this`
399
+ `where(column: string, operator: Operator, value: unknown): this`
400
+
401
+ Adds an `AND` condition. The two-argument form uses `=`; the three-argument form
402
+ takes an explicit operator.
403
+
404
+ ```ts
405
+ db("users").where("active", true);
406
+ db("users").where("age", ">", 18);
407
+ db("users").where("email", "like", "%@example.com");
408
+ ```
409
+
410
+ **Notes:** `Operator` is `"=" | "!=" | "<" | "<=" | ">" | ">=" | "like"`. Values
411
+ are always parameterized. Chaining multiple `where`s combines them with `AND`.
412
+
413
+ #### `orWhere(column, value)` / `orWhere(column, operator, value)`
414
+
415
+ `orWhere(column: string, value: unknown): this`
416
+ `orWhere(column: string, operator: Operator, value: unknown): this`
417
+
418
+ Same as `where`, but joins the condition with `OR`.
419
+
420
+ ```ts
421
+ db("orders").where("status", "paid").orWhere("status", "shipped").get();
422
+ ```
423
+
424
+ **Notes:** conditions are combined left-to-right without grouping parentheses, so
425
+ mixing `where` and `orWhere` follows SQL's `AND`/`OR` precedence — group complex
426
+ logic in separate queries if you need explicit parenthesization.
427
+
428
+ #### `whereIn(column, values)`
429
+
430
+ `whereIn(column: string, values: unknown[]): this`
431
+
432
+ Matches rows where `column` is any of `values` (`AND`-joined).
433
+
434
+ ```ts
435
+ db("posts").whereIn("id", [1, 2, 3]).get();
436
+ ```
437
+
438
+ **Notes:** each value becomes its own placeholder. An empty array produces
439
+ `IN ()`, which most engines reject — guard against empty lists yourself.
440
+
441
+ #### `whereNull(column)` / `whereNotNull(column)`
442
+
443
+ `whereNull(column: string): this`
444
+ `whereNotNull(column: string): this`
445
+
446
+ Adds an `AND` `IS NULL` / `IS NOT NULL` condition — no binding.
447
+
448
+ ```ts
449
+ db("posts").whereNull("deleted_at").get();
450
+ db("users").whereNotNull("verified_at").get();
451
+ ```
452
+
453
+ #### `orderBy(column, direction?)`
454
+
455
+ `orderBy(column: string, direction?: "asc" | "desc"): this`
456
+
457
+ Adds an `ORDER BY` clause (default `"asc"`). Call it repeatedly for multiple sort
458
+ keys, applied in call order.
459
+
460
+ ```ts
461
+ db("users").orderBy("last_name").orderBy("created_at", "desc").get();
462
+ ```
463
+
464
+ **Notes:** the column is interpolated, not parameterized — don't pass user input.
465
+
466
+ #### `limit(n)` / `offset(n)`
467
+
468
+ `limit(n: number): this`
469
+ `offset(n: number): this`
470
+
471
+ Caps the number of rows / skips the first `n`. Together they paginate.
472
+
473
+ ```ts
474
+ db("posts").limit(20).offset(40).get(); // page 3, 20 per page
475
+ ```
476
+
477
+ **Notes:** `first()` sets `limit(1)` internally, overriding any prior `limit`.
478
+
479
+ #### `get()`
480
+
481
+ `get(): Promise<T[]>`
482
+
483
+ Runs the SELECT and returns all matching rows.
484
+
485
+ ```ts
486
+ const rows = await db("users").where("active", true).get();
487
+ ```
488
+
489
+ #### `first()`
490
+
491
+ `first(): Promise<T | null>`
492
+
493
+ Runs the SELECT with `LIMIT 1` and returns the first row, or `null`.
494
+
495
+ ```ts
496
+ const user = await db("users").where("email", email).first();
497
+ ```
498
+
499
+ **Notes:** overrides any `limit` you set. Returns `null` (not `undefined`) when
500
+ nothing matches.
501
+
502
+ #### `count()`
503
+
504
+ `count(): Promise<number>`
505
+
506
+ Returns `COUNT(*)` for the current `where` clause.
507
+
508
+ ```ts
509
+ const active = await db("users").where("active", true).count();
510
+ ```
511
+
512
+ **Notes:** ignores `select`, `orderBy`, `limit`, and `offset` — it counts matching
513
+ rows, not the paginated slice.
514
+
515
+ #### `exists()`
516
+
517
+ `exists(): Promise<boolean>`
518
+
519
+ `true` when at least one row matches — a `count() > 0` shorthand.
520
+
521
+ ```ts
522
+ if (await db("users").where("email", email).exists()) { /* taken */ }
523
+ ```
524
+
525
+ #### `insert(data)`
526
+
527
+ `insert(data: Row): Promise<WriteResult>`
528
+
529
+ Inserts one row and returns write metadata.
530
+
531
+ ```ts
532
+ const result = await db("users").insert({ email, name });
533
+ result.rowsAffected; // 1
534
+ result.insertId; // driver-dependent
535
+ ```
536
+
537
+ **Notes:** column order follows `Object.keys(data)`. `insertId` is only populated
538
+ if the driver reports it in `WriteResult`.
539
+
540
+ #### `insertGetId(data)`
541
+
542
+ `insertGetId(data: Row): Promise<number | string | undefined>`
543
+
544
+ Inserts one row and returns just its new id (`insert` unwrapped).
545
+
546
+ ```ts
547
+ const id = await db("users").insertGetId({ email, name });
548
+ ```
549
+
550
+ **Notes:** returns `undefined` when the driver doesn't report an `insertId`.
551
+
552
+ #### `update(data)`
553
+
554
+ `update(data: Row): Promise<WriteResult>`
555
+
556
+ Updates every row matching the `where` clause, setting the given columns.
557
+
558
+ ```ts
559
+ const r = await db("users").where("id", 1).update({ name: "Grace" });
560
+ r.rowsAffected; // rows changed
561
+ ```
562
+
563
+ **Notes:** with no `where`, updates the entire table. Bindings are the new values
564
+ followed by the where-clause values.
565
+
566
+ #### `delete()`
567
+
568
+ `delete(): Promise<WriteResult>`
569
+
570
+ Deletes every row matching the `where` clause.
571
+
572
+ ```ts
573
+ await db("sessions").where("expires_at", "<", now).delete();
574
+ ```
575
+
576
+ **Notes:** with no `where`, empties the table. There's no soft-delete here — pair
577
+ with a `deleted_at` column and `whereNull` if you want one.
578
+
579
+ ### Interfaces & types
580
+
581
+ #### `Connection`
582
+
583
+ ```ts
584
+ interface Connection {
585
+ select<T = Row>(sql: string, bindings: unknown[]): Promise<T[]>;
586
+ write(sql: string, bindings: unknown[]): Promise<WriteResult>;
587
+ }
588
+ ```
589
+
590
+ The seam between the builder and your driver. `select` runs any row-returning
591
+ query and resolves to the rows; `write` runs an INSERT/UPDATE/DELETE and resolves
592
+ to a `WriteResult`. Implement it once per driver — the two-method surface is
593
+ deliberately tiny so any driver (or a mock in tests) fits.
594
+
595
+ ```ts
596
+ const mock: Connection = {
597
+ select: async () => [{ id: 1 }],
598
+ write: async () => ({ rowsAffected: 1, insertId: 1 }),
599
+ };
600
+ ```
601
+
602
+ #### `WriteResult`
603
+
604
+ ```ts
605
+ interface WriteResult {
606
+ rowsAffected: number;
607
+ insertId?: number | string;
608
+ }
609
+ ```
610
+
611
+ Returned by `write` (and thus `insert`/`update`/`delete`). `insertId` is optional
612
+ because not every driver or statement produces one.
613
+
614
+ #### `Row`
615
+
616
+ `type Row = Record<string, unknown>`
617
+
618
+ A database row — the default shape for query results and write payloads.
619
+
620
+ #### `Dialect`
621
+
622
+ `type Dialect = "sqlite" | "mysql" | "postgres"`
623
+
624
+ Selects placeholder rendering. Only Postgres differs (`$1, $2, …`); the others
625
+ use `?`.
626
+
627
+ #### `Operator`
628
+
629
+ `type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like"`
630
+
631
+ The comparison operators accepted by the three-argument `where`/`orWhere`.