@shaferllc/keel 0.66.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.
- package/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -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/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -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 +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -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 +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -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/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -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/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/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -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/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -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/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 +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -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/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
package/docs/database.md
ADDED
|
@@ -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.
|