@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.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- package/package.json +42 -7
package/docs/database.md
ADDED
|
@@ -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`.
|