@shaferllc/keel 0.82.0 → 0.83.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/dist/db/d1-http.d.ts +34 -0
- package/dist/db/d1-http.js +61 -0
- package/dist/db/libsql.d.ts +13 -3
- package/dist/teams/models.js +51 -5
- package/docs/ai-manifest.json +22 -1
- package/docs/database.md +5 -522
- package/docs/models.md +5 -2
- package/docs/orm.md +57 -0
- package/docs/query-builder.md +533 -0
- package/docs/starter-kits.md +88 -0
- package/llms-full.txt +5299 -5115
- package/llms.txt +3 -0
- package/package.json +7 -2
- package/templates/api/.env.example +12 -0
- package/templates/api/app/Controllers/PostController.ts +37 -0
- package/templates/api/app/Http/Kernel.ts +13 -0
- package/templates/api/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/api/app/Models/Post.ts +12 -0
- package/templates/api/app/Providers/AppServiceProvider.ts +8 -0
- package/templates/api/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/api/bin/keel.ts +15 -0
- package/templates/api/bootstrap/app.ts +24 -0
- package/templates/api/bootstrap/providers.edge.ts +14 -0
- package/templates/api/bootstrap/providers.ts +7 -0
- package/templates/api/config/app.ts +10 -0
- package/templates/api/config/database.ts +42 -0
- package/templates/api/database/migrations/0001_create_posts.ts +20 -0
- package/templates/api/package.json +30 -0
- package/templates/api/routes/web.ts +12 -0
- package/templates/api/tests/posts.test.ts +30 -0
- package/templates/api/tsconfig.json +16 -0
- package/templates/api/worker.ts +33 -0
- package/templates/api/wrangler.jsonc +22 -0
- package/templates/app/.env.example +12 -0
- package/templates/app/app/Controllers/AuthController.ts +117 -0
- package/templates/app/app/Controllers/DashboardController.ts +37 -0
- package/templates/app/app/Controllers/HomeController.ts +10 -0
- package/templates/app/app/Http/Kernel.ts +21 -0
- package/templates/app/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/app/app/Models/User.ts +15 -0
- package/templates/app/app/Providers/AppServiceProvider.ts +12 -0
- package/templates/app/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/app/bin/keel.ts +15 -0
- package/templates/app/bootstrap/app.ts +24 -0
- package/templates/app/bootstrap/providers.edge.ts +15 -0
- package/templates/app/bootstrap/providers.ts +18 -0
- package/templates/app/config/app.ts +10 -0
- package/templates/app/config/database.ts +42 -0
- package/templates/app/database/migrations/0001_create_users.ts +21 -0
- package/templates/app/package.json +35 -0
- package/templates/app/public/.gitkeep +2 -0
- package/templates/app/resources/css/app.css +1 -0
- package/templates/app/resources/views/auth/forgot.tsx +30 -0
- package/templates/app/resources/views/auth/login.tsx +24 -0
- package/templates/app/resources/views/auth/register.tsx +24 -0
- package/templates/app/resources/views/auth/two-factor.tsx +22 -0
- package/templates/app/resources/views/dashboard.tsx +20 -0
- package/templates/app/resources/views/layout.tsx +15 -0
- package/templates/app/resources/views/welcome.tsx +29 -0
- package/templates/app/routes/web.ts +35 -0
- package/templates/app/tests/auth.test.ts +47 -0
- package/templates/app/tsconfig.json +31 -0
- package/templates/app/worker.ts +33 -0
- package/templates/app/wrangler.jsonc +25 -0
- package/templates/minimal/.env.example +8 -0
- package/templates/minimal/app/Controllers/HomeController.ts +14 -0
- package/templates/minimal/app/Http/Kernel.ts +22 -0
- package/templates/minimal/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/minimal/app/Providers/AppServiceProvider.ts +8 -0
- package/templates/minimal/bin/keel.ts +15 -0
- package/templates/minimal/bootstrap/app.ts +24 -0
- package/templates/minimal/bootstrap/providers.ts +6 -0
- package/templates/minimal/config/app.ts +10 -0
- package/templates/minimal/package.json +29 -0
- package/templates/minimal/public/.gitkeep +2 -0
- package/templates/minimal/resources/css/app.css +1 -0
- package/templates/minimal/resources/views/layout.tsx +15 -0
- package/templates/minimal/resources/views/welcome.tsx +15 -0
- package/templates/minimal/routes/web.ts +13 -0
- package/templates/minimal/tsconfig.json +18 -0
- package/templates/minimal/worker.ts +23 -0
- package/templates/minimal/wrangler.jsonc +10 -0
- package/templates/saas/.env.example +12 -0
- package/templates/saas/app/Controllers/AuthController.ts +125 -0
- package/templates/saas/app/Controllers/DashboardController.ts +37 -0
- package/templates/saas/app/Controllers/HomeController.ts +10 -0
- package/templates/saas/app/Controllers/TeamController.ts +88 -0
- package/templates/saas/app/Http/Kernel.ts +27 -0
- package/templates/saas/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/saas/app/Models/Project.ts +20 -0
- package/templates/saas/app/Models/User.ts +15 -0
- package/templates/saas/app/Providers/AppServiceProvider.ts +12 -0
- package/templates/saas/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/saas/bin/keel.ts +15 -0
- package/templates/saas/bootstrap/app.ts +24 -0
- package/templates/saas/bootstrap/providers.edge.ts +18 -0
- package/templates/saas/bootstrap/providers.ts +22 -0
- package/templates/saas/config/app.ts +10 -0
- package/templates/saas/config/database.ts +42 -0
- package/templates/saas/database/migrations/0001_create_users.ts +21 -0
- package/templates/saas/database/migrations/0002_create_projects.ts +20 -0
- package/templates/saas/package.json +35 -0
- package/templates/saas/public/.gitkeep +2 -0
- package/templates/saas/resources/css/app.css +1 -0
- package/templates/saas/resources/views/auth/forgot.tsx +30 -0
- package/templates/saas/resources/views/auth/login.tsx +24 -0
- package/templates/saas/resources/views/auth/register.tsx +24 -0
- package/templates/saas/resources/views/auth/two-factor.tsx +22 -0
- package/templates/saas/resources/views/dashboard.tsx +20 -0
- package/templates/saas/resources/views/layout.tsx +15 -0
- package/templates/saas/resources/views/teams/index.tsx +85 -0
- package/templates/saas/resources/views/welcome.tsx +29 -0
- package/templates/saas/routes/web.ts +44 -0
- package/templates/saas/tests/auth.test.ts +47 -0
- package/templates/saas/tests/teams.test.ts +27 -0
- package/templates/saas/tsconfig.json +31 -0
- package/templates/saas/worker.ts +33 -0
- package/templates/saas/wrangler.jsonc +25 -0
package/docs/database.md
CHANGED
|
@@ -110,218 +110,12 @@ An unregistered connection name doesn't fail when you *build* a query, only when
|
|
|
110
110
|
it runs — so a misconfigured name surfaces as a rejected read/write, not a
|
|
111
111
|
construction-time throw.
|
|
112
112
|
|
|
113
|
-
##
|
|
113
|
+
## Queries & models
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
driver-agnostic and edge-safe: the same chain compiles for sqlite, MySQL, and
|
|
120
|
-
Postgres.
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
import { db } from "@shaferllc/keel/core";
|
|
124
|
-
|
|
125
|
-
const active = await db("users")
|
|
126
|
-
.where("active", true)
|
|
127
|
-
.where("age", ">", 18)
|
|
128
|
-
.orderBy("name")
|
|
129
|
-
.limit(20)
|
|
130
|
-
.get();
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Retrieving results
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
await db("users").get(); // Row[]
|
|
137
|
-
await db("users").where("id", 1).first(); // Row | null
|
|
138
|
-
await db("users").where("id", 1).firstOrFail(); // Row, or throws NotFoundException
|
|
139
|
-
await db("users").find(1); // by primary key (default "id")
|
|
140
|
-
await db("users").where("email", e).sole(); // exactly one, else throws
|
|
141
|
-
await db("users").where("id", 1).value("email"); // one column of the first row
|
|
142
|
-
await db("posts").pluck("title"); // string[] of one column
|
|
143
|
-
await db("tags").orderBy("name").implode("name", ", "); // "a, b, c"
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
For large sets, `chunk` pages through without loading everything (return `false`
|
|
147
|
-
to stop early):
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
await db("users").orderBy("id").chunk(500, async (rows) => {
|
|
151
|
-
for (const row of rows) await process(row);
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Aggregates
|
|
156
|
-
|
|
157
|
-
```ts
|
|
158
|
-
await db("orders").count();
|
|
159
|
-
await db("orders").where("paid", true).sum("total");
|
|
160
|
-
await db("orders").avg("total"); // also min(col), max(col)
|
|
161
|
-
await db("users").where("email", e).exists(); // boolean
|
|
162
|
-
await db("users").where("banned", true).doesntExist();
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Selects
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
db("users").select("id", "email");
|
|
169
|
-
db("users").select("id").addSelect("email"); // append, don't replace
|
|
170
|
-
db("orders").selectRaw("SUM(total) AS revenue");
|
|
171
|
-
db("users").distinct().select("country");
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Where clauses
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
db("users").where("votes", 100); // = is the default operator
|
|
178
|
-
db("users").where("votes", ">=", 100);
|
|
179
|
-
db("users").where("name", "like", "T%");
|
|
180
|
-
|
|
181
|
-
db("users").where("votes", 100).orWhere("name", "John");
|
|
182
|
-
db("users").whereNot("status", "cancelled");
|
|
183
|
-
|
|
184
|
-
db("users").whereIn("id", [1, 2, 3]).whereNotIn("id", [4]);
|
|
185
|
-
db("users").whereNull("deleted_at").whereNotNull("email_verified_at");
|
|
186
|
-
db("products").whereBetween("price", [10, 100]).whereNotBetween("stock", [0, 5]);
|
|
187
|
-
db("posts").whereLike("title", "%keel%");
|
|
188
|
-
db("events").whereColumn("updated_at", ">", "created_at"); // column vs column
|
|
189
|
-
db("users").whereRaw("score >= ? AND score <= ?", [10, 90]);
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
Every clause has an `orWhere…` twin — `orWhereIn`, `orWhereNull`,
|
|
193
|
-
`orWhereNotNull`, `orWhereBetween`, `orWhereColumn`, `orWhereLike`, `orWhereRaw`,
|
|
194
|
-
`orWhereNotIn`.
|
|
195
|
-
|
|
196
|
-
**Grouped clauses.** Pass a callback to `where`/`orWhere` to parenthesize a set
|
|
197
|
-
of conditions — the way to express `A AND (B OR C)`:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
await db("users")
|
|
201
|
-
.where("active", true)
|
|
202
|
-
.where((q) => q.where("role", "admin").orWhere("role", "owner"))
|
|
203
|
-
.get();
|
|
204
|
-
// … WHERE active = ? AND (role = ? OR role = ?)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
### Ordering, grouping, limit & offset
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
db("users").orderBy("name").orderByDesc("created_at");
|
|
211
|
-
db("posts").latest(); // ORDER BY created_at DESC (oldest() for ASC)
|
|
212
|
-
db("posts").orderByRaw("LENGTH(title) DESC");
|
|
213
|
-
db("users").inRandomOrder(); // dialect-aware RANDOM()/RAND()
|
|
214
|
-
db("users").reorder("name"); // clear existing ordering, then set
|
|
215
|
-
|
|
216
|
-
db("orders")
|
|
217
|
-
.select("user_id")
|
|
218
|
-
.selectRaw("SUM(total) AS spent")
|
|
219
|
-
.groupBy("user_id")
|
|
220
|
-
.having("spent", ">", 1000) // also havingRaw(...), havingBetween(...)
|
|
221
|
-
.get();
|
|
222
|
-
|
|
223
|
-
db("users").limit(10).offset(20); // take(10)/skip(20) are aliases
|
|
224
|
-
db("users").forPage(3, 15); // page 3, 15 per page
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### Joins
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
await db("posts")
|
|
231
|
-
.join("users", "posts.user_id", "users.id") // INNER JOIN on equality
|
|
232
|
-
.leftJoin("images", "images.post_id", "posts.id")
|
|
233
|
-
.select("posts.title", "users.name")
|
|
234
|
-
.get();
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
`rightJoin` and `crossJoin` round out the set. Joins with several `ON`
|
|
238
|
-
conditions aren't modelled — use `whereRaw` or a view.
|
|
239
|
-
|
|
240
|
-
### Conditional clauses
|
|
241
|
-
|
|
242
|
-
`when` / `unless` apply a callback based on a runtime value, so you build a query
|
|
243
|
-
without breaking the chain into `if`s. The callback receives the value:
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
await db("users")
|
|
247
|
-
.when(search, (q, term) => q.whereLike("name", `%${term}%`))
|
|
248
|
-
.unless(includeArchived, (q) => q.whereNull("archived_at"))
|
|
249
|
-
.get();
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Inserts
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
await db("users").insert({ email, name });
|
|
256
|
-
const id = await db("users").insertGetId({ email, name }); // new primary key
|
|
257
|
-
await db("logs").insertOrIgnore({ key, value }); // skip unique conflicts
|
|
258
|
-
await db("users").upsert([{ id: 1, name: "Ada" }], ["id"], ["name"]); // insert/update
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
`upsert(rows, uniqueBy, update?)` inserts, updating the `update` columns (default:
|
|
262
|
-
everything not in `uniqueBy`) on a conflict — dialect-aware (`ON CONFLICT` /
|
|
263
|
-
`ON DUPLICATE KEY UPDATE`).
|
|
264
|
-
|
|
265
|
-
### Updates
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
await db("users").where("id", id).update({ name: "Grace" });
|
|
269
|
-
await db("users").updateOrInsert({ email }, { name }); // update match, else insert
|
|
270
|
-
await db("posts").where("id", id).increment("views"); // += 1
|
|
271
|
-
await db("posts").where("id", id).decrement("stock", 3, { updated_at: now });
|
|
272
|
-
await db("counters").incrementEach({ hits: 1, misses: 2 }); // several columns at once
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### Deletes
|
|
276
|
-
|
|
277
|
-
```ts
|
|
278
|
-
await db("sessions").where("expires_at", "<", now).delete();
|
|
279
|
-
await db("cache").truncate(); // empty the table (DELETE on sqlite)
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
> **Guard your writes.** `update()`, `delete()`, and the increments apply to
|
|
283
|
-
> every row matching the current `where` clause — with none, that's the whole
|
|
284
|
-
> table. Scope every write unless you truly mean to touch every row.
|
|
285
|
-
|
|
286
|
-
### Pagination
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
const page = await db("posts").latest().paginate(2, 15);
|
|
290
|
-
// { data, total, perPage, currentPage, lastPage } — a COUNT plus a page query
|
|
291
|
-
|
|
292
|
-
const feed = await db("posts").latest().simplePaginate(2, 15);
|
|
293
|
-
// { data, perPage, currentPage, hasMore } — no COUNT; one extra row tells hasMore
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Pessimistic locking
|
|
297
|
-
|
|
298
|
-
Inside a [transaction](#transactions), lock the selected rows against concurrent
|
|
299
|
-
writes. No-ops on sqlite (which locks the whole database anyway):
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
await transaction(async () => {
|
|
303
|
-
const row = await db("accounts").where("id", id).lockForUpdate().first(); // FOR UPDATE
|
|
304
|
-
await db("accounts").where("id", id).update({ balance: row.balance - 10 });
|
|
305
|
-
});
|
|
306
|
-
// sharedLock() takes a read lock (FOR SHARE) instead.
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### Debugging
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
db("users").where("active", true).toSql(); // "SELECT * FROM users WHERE active = ?"
|
|
313
|
-
db("users").where("active", true).getBindings(); // [true]
|
|
314
|
-
db("users").where("active", true).dump(); // logs SQL + bindings, returns the builder
|
|
315
|
-
db("users").where("active", true).dd(); // logs and throws (dump-and-die)
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Not (yet) modelled
|
|
319
|
-
|
|
320
|
-
Kept out on purpose, to stay driver-agnostic and honest about what compiles
|
|
321
|
-
everywhere: unions, subquery `where`/join builders (`whereExists`, `joinSub`),
|
|
322
|
-
the `whereDate`/`whereMonth`/… date-function family (no portable form across
|
|
323
|
-
dialects), and `cursor`/`lazy` streaming. Reach for `whereRaw`, a raw
|
|
324
|
-
`connection().select(sql)`, or a database view when you need them.
|
|
115
|
+
Building and running queries — `where`, joins, aggregates, inserts, updates,
|
|
116
|
+
pagination — lives in the [Query Builder](./query-builder.md) guide. The
|
|
117
|
+
[ORM](./orm.md) adds an active-record layer (models, relationships, events,
|
|
118
|
+
scopes) on top of the same builder.
|
|
325
119
|
|
|
326
120
|
## Transactions
|
|
327
121
|
|
|
@@ -519,317 +313,6 @@ unnamed `db(table)` and connectionless models use (throws if `name` isn't
|
|
|
519
313
|
registered). `connectionNames()` returns the registered names.
|
|
520
314
|
`clearConnections()` unregisters everything — a test helper.
|
|
521
315
|
|
|
522
|
-
### `QueryBuilder`
|
|
523
|
-
|
|
524
|
-
Returned by `db()`. Constraint methods return `this` (chainable); terminal
|
|
525
|
-
methods return a promise. You never construct it directly.
|
|
526
|
-
|
|
527
|
-
#### `select(...columns)`
|
|
528
|
-
|
|
529
|
-
`select(...columns: string[]): this`
|
|
530
|
-
|
|
531
|
-
Restricts the selected columns. With no arguments, selects `*`.
|
|
532
|
-
|
|
533
|
-
```ts
|
|
534
|
-
db("users").select("id", "email").get();
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
**Notes:** column names are interpolated as-is (they are not parameterized), so
|
|
538
|
-
never pass user input as a column name. Calling it again replaces the prior
|
|
539
|
-
selection.
|
|
540
|
-
|
|
541
|
-
#### `where(column, value)` / `where(column, operator, value)`
|
|
542
|
-
|
|
543
|
-
`where(column: string, value: unknown): this`
|
|
544
|
-
`where(column: string, operator: Operator, value: unknown): this`
|
|
545
|
-
|
|
546
|
-
Adds an `AND` condition. The two-argument form uses `=`; the three-argument form
|
|
547
|
-
takes an explicit operator.
|
|
548
|
-
|
|
549
|
-
```ts
|
|
550
|
-
db("users").where("active", true);
|
|
551
|
-
db("users").where("age", ">", 18);
|
|
552
|
-
db("users").where("email", "like", "%@example.com");
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
**Notes:** `Operator` is `"=" | "!=" | "<" | "<=" | ">" | ">=" | "like"`. Values
|
|
556
|
-
are always parameterized. Chaining multiple `where`s combines them with `AND`.
|
|
557
|
-
|
|
558
|
-
#### `orWhere(column, value)` / `orWhere(column, operator, value)`
|
|
559
|
-
|
|
560
|
-
`orWhere(column: string, value: unknown): this`
|
|
561
|
-
`orWhere(column: string, operator: Operator, value: unknown): this`
|
|
562
|
-
|
|
563
|
-
Same as `where`, but joins the condition with `OR`.
|
|
564
|
-
|
|
565
|
-
```ts
|
|
566
|
-
db("orders").where("status", "paid").orWhere("status", "shipped").get();
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
**Notes:** conditions are combined left-to-right without grouping parentheses, so
|
|
570
|
-
mixing `where` and `orWhere` follows SQL's `AND`/`OR` precedence — group complex
|
|
571
|
-
logic in separate queries if you need explicit parenthesization.
|
|
572
|
-
|
|
573
|
-
#### `whereIn(column, values)`
|
|
574
|
-
|
|
575
|
-
`whereIn(column: string, values: unknown[]): this`
|
|
576
|
-
|
|
577
|
-
Matches rows where `column` is any of `values` (`AND`-joined).
|
|
578
|
-
|
|
579
|
-
```ts
|
|
580
|
-
db("posts").whereIn("id", [1, 2, 3]).get();
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
**Notes:** each value becomes its own placeholder. An empty array produces
|
|
584
|
-
`IN ()`, which most engines reject — guard against empty lists yourself.
|
|
585
|
-
|
|
586
|
-
#### `whereNull(column)` / `whereNotNull(column)`
|
|
587
|
-
|
|
588
|
-
`whereNull(column: string): this`
|
|
589
|
-
`whereNotNull(column: string): this`
|
|
590
|
-
|
|
591
|
-
Adds an `AND` `IS NULL` / `IS NOT NULL` condition — no binding.
|
|
592
|
-
|
|
593
|
-
```ts
|
|
594
|
-
db("posts").whereNull("deleted_at").get();
|
|
595
|
-
db("users").whereNotNull("verified_at").get();
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
#### `orderBy(column, direction?)`
|
|
599
|
-
|
|
600
|
-
`orderBy(column: string, direction?: "asc" | "desc"): this`
|
|
601
|
-
|
|
602
|
-
Adds an `ORDER BY` clause (default `"asc"`). Call it repeatedly for multiple sort
|
|
603
|
-
keys, applied in call order.
|
|
604
|
-
|
|
605
|
-
```ts
|
|
606
|
-
db("users").orderBy("last_name").orderBy("created_at", "desc").get();
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
**Notes:** the column is interpolated, not parameterized — don't pass user input.
|
|
610
|
-
|
|
611
|
-
#### `limit(n)` / `offset(n)`
|
|
612
|
-
|
|
613
|
-
`limit(n: number): this`
|
|
614
|
-
`offset(n: number): this`
|
|
615
|
-
|
|
616
|
-
Caps the number of rows / skips the first `n`. Together they paginate.
|
|
617
|
-
|
|
618
|
-
```ts
|
|
619
|
-
db("posts").limit(20).offset(40).get(); // page 3, 20 per page
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
**Notes:** `first()` sets `limit(1)` internally, overriding any prior `limit`.
|
|
623
|
-
|
|
624
|
-
#### `get()`
|
|
625
|
-
|
|
626
|
-
`get(): Promise<T[]>`
|
|
627
|
-
|
|
628
|
-
Runs the SELECT and returns all matching rows.
|
|
629
|
-
|
|
630
|
-
```ts
|
|
631
|
-
const rows = await db("users").where("active", true).get();
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
#### `first()`
|
|
635
|
-
|
|
636
|
-
`first(): Promise<T | null>`
|
|
637
|
-
|
|
638
|
-
Runs the SELECT with `LIMIT 1` and returns the first row, or `null`.
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
const user = await db("users").where("email", email).first();
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
**Notes:** overrides any `limit` you set. Returns `null` (not `undefined`) when
|
|
645
|
-
nothing matches.
|
|
646
|
-
|
|
647
|
-
#### `count()`
|
|
648
|
-
|
|
649
|
-
`count(): Promise<number>`
|
|
650
|
-
|
|
651
|
-
Returns `COUNT(*)` for the current `where` clause.
|
|
652
|
-
|
|
653
|
-
```ts
|
|
654
|
-
const active = await db("users").where("active", true).count();
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
**Notes:** ignores `select`, `orderBy`, `limit`, and `offset` — it counts matching
|
|
658
|
-
rows, not the paginated slice.
|
|
659
|
-
|
|
660
|
-
#### `exists()`
|
|
661
|
-
|
|
662
|
-
`exists(): Promise<boolean>`
|
|
663
|
-
|
|
664
|
-
`true` when at least one row matches — a `count() > 0` shorthand.
|
|
665
|
-
|
|
666
|
-
```ts
|
|
667
|
-
if (await db("users").where("email", email).exists()) { /* taken */ }
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
#### `insert(data)`
|
|
671
|
-
|
|
672
|
-
`insert(data: Row): Promise<WriteResult>`
|
|
673
|
-
|
|
674
|
-
Inserts one row and returns write metadata.
|
|
675
|
-
|
|
676
|
-
```ts
|
|
677
|
-
const result = await db("users").insert({ email, name });
|
|
678
|
-
result.rowsAffected; // 1
|
|
679
|
-
result.insertId; // driver-dependent
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
**Notes:** column order follows `Object.keys(data)`. `insertId` is only populated
|
|
683
|
-
if the driver reports it in `WriteResult`.
|
|
684
|
-
|
|
685
|
-
#### `insertGetId(data)`
|
|
686
|
-
|
|
687
|
-
`insertGetId(data: Row): Promise<number | string | undefined>`
|
|
688
|
-
|
|
689
|
-
Inserts one row and returns just its new id (`insert` unwrapped).
|
|
690
|
-
|
|
691
|
-
```ts
|
|
692
|
-
const id = await db("users").insertGetId({ email, name });
|
|
693
|
-
```
|
|
694
|
-
|
|
695
|
-
**Notes:** returns `undefined` when the driver doesn't report an `insertId`.
|
|
696
|
-
|
|
697
|
-
#### `update(data)`
|
|
698
|
-
|
|
699
|
-
`update(data: Row): Promise<WriteResult>`
|
|
700
|
-
|
|
701
|
-
Updates every row matching the `where` clause, setting the given columns.
|
|
702
|
-
|
|
703
|
-
```ts
|
|
704
|
-
const r = await db("users").where("id", 1).update({ name: "Grace" });
|
|
705
|
-
r.rowsAffected; // rows changed
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
**Notes:** with no `where`, updates the entire table. Bindings are the new values
|
|
709
|
-
followed by the where-clause values.
|
|
710
|
-
|
|
711
|
-
#### `delete()`
|
|
712
|
-
|
|
713
|
-
`delete(): Promise<WriteResult>`
|
|
714
|
-
|
|
715
|
-
Deletes every row matching the `where` clause.
|
|
716
|
-
|
|
717
|
-
```ts
|
|
718
|
-
await db("sessions").where("expires_at", "<", now).delete();
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
**Notes:** with no `where`, empties the table. There's no soft-delete here — pair
|
|
722
|
-
with a `deleted_at` column and `whereNull` if you want one.
|
|
723
|
-
|
|
724
|
-
#### `whereColumn(first, operator?, second)` · `whereRaw(sql, bindings?)`
|
|
725
|
-
|
|
726
|
-
Compare two columns (no binding) or add a raw WHERE fragment with its own
|
|
727
|
-
bindings. `whereColumn("updated_at", ">", "created_at")`; `whereRaw("score >= ?", [10])`.
|
|
728
|
-
|
|
729
|
-
#### `join(table, first, operator?, second)` · `leftJoin(...)`
|
|
730
|
-
|
|
731
|
-
Add an `INNER JOIN` / `LEFT JOIN` on an equality (or the given operator).
|
|
732
|
-
Included in `get`, `count`, and aggregates. Qualify ambiguous columns
|
|
733
|
-
(`"posts.user_id"`).
|
|
734
|
-
|
|
735
|
-
#### `groupBy(...columns)` · `having(column, operator?, value)` · `distinct()`
|
|
736
|
-
|
|
737
|
-
`GROUP BY`, a bound `HAVING` predicate, and `SELECT DISTINCT`.
|
|
738
|
-
|
|
739
|
-
#### `orderByRaw(sql)` · `when(condition, then, otherwise?)`
|
|
740
|
-
|
|
741
|
-
A raw `ORDER BY` fragment; and conditional building — `then(query, value)` runs
|
|
742
|
-
only when `condition` is truthy, else `otherwise`.
|
|
743
|
-
|
|
744
|
-
#### `increment(column, amount?, extra?)` · `decrement(column, amount?, extra?)`
|
|
745
|
-
|
|
746
|
-
`increment(column: string, amount = 1, extra: Row = {}): Promise<WriteResult>`
|
|
747
|
-
|
|
748
|
-
Atomically `column = column ± amount` on matching rows, optionally setting other
|
|
749
|
-
columns in the same statement. Scope with `where`.
|
|
750
|
-
|
|
751
|
-
#### `upsert(rows, uniqueBy, update?)`
|
|
752
|
-
|
|
753
|
-
`upsert(rows: Row | Row[], uniqueBy: string[], update?: string[]): Promise<WriteResult>`
|
|
754
|
-
|
|
755
|
-
Insert rows, updating `update` columns (default: all non-unique) on a conflict
|
|
756
|
-
against `uniqueBy`. Dialect-aware: `ON CONFLICT … DO UPDATE` (sqlite/postgres) or
|
|
757
|
-
`ON DUPLICATE KEY UPDATE` (mysql).
|
|
758
|
-
|
|
759
|
-
#### `insertOrIgnore(rows)`
|
|
760
|
-
|
|
761
|
-
Insert one or more rows, skipping any that violate a unique constraint
|
|
762
|
-
(`INSERT OR IGNORE` / `INSERT IGNORE` / `ON CONFLICT DO NOTHING`).
|
|
763
|
-
|
|
764
|
-
#### `chunk(size, callback)`
|
|
765
|
-
|
|
766
|
-
`chunk(size: number, callback: (rows: T[]) => void | boolean | Promise<void | boolean>): Promise<void>`
|
|
767
|
-
|
|
768
|
-
Process results a page at a time so a large table never loads at once. Return
|
|
769
|
-
`false` from the callback to stop early. Pair with `orderBy` for a stable order.
|
|
770
|
-
|
|
771
|
-
#### `addSelect(...columns)` · `selectRaw(sql)`
|
|
772
|
-
|
|
773
|
-
Append columns to the SELECT list without replacing it; `selectRaw` appends a raw
|
|
774
|
-
expression (`selectRaw("SUM(total) AS revenue")`).
|
|
775
|
-
|
|
776
|
-
#### `orWhere` family · `whereNot(...)` · `whereNotBetween(column, [min, max])`
|
|
777
|
-
|
|
778
|
-
Every `where…` clause has an `orWhere…` twin joined with `OR` — `orWhereIn`,
|
|
779
|
-
`orWhereNotIn`, `orWhereNull`, `orWhereNotNull`, `orWhereBetween`,
|
|
780
|
-
`orWhereColumn`, `orWhereLike`, `orWhereRaw`. `whereNot` negates a comparison;
|
|
781
|
-
`whereNotBetween` is the inverse of `whereBetween`. Passing a **callback** to
|
|
782
|
-
`where`/`orWhere` groups its conditions in parentheses.
|
|
783
|
-
|
|
784
|
-
#### `orderByDesc(column)` · `reorder(column?, direction?)` · `inRandomOrder()`
|
|
785
|
-
|
|
786
|
-
Descending order; clear existing ordering (optionally setting a new one); random
|
|
787
|
-
order (dialect-aware `RANDOM()`/`RAND()`).
|
|
788
|
-
|
|
789
|
-
#### `groupByRaw(sql)` · `havingRaw(sql, bindings?)` · `havingBetween(column, [min, max])`
|
|
790
|
-
|
|
791
|
-
Raw `GROUP BY`, a raw/bound `HAVING`, and a `HAVING … BETWEEN`.
|
|
792
|
-
|
|
793
|
-
#### `take(n)` · `skip(n)` · `forPage(page, perPage?)`
|
|
794
|
-
|
|
795
|
-
Aliases for `limit`/`offset`, and limit+offset for a 1-based page.
|
|
796
|
-
|
|
797
|
-
#### `rightJoin(...)` · `crossJoin(table)`
|
|
798
|
-
|
|
799
|
-
`RIGHT JOIN` on an equality; `CROSS JOIN`.
|
|
800
|
-
|
|
801
|
-
#### `unless(condition, then, otherwise?)`
|
|
802
|
-
|
|
803
|
-
The inverse of `when` — runs `then` only when `condition` is falsy.
|
|
804
|
-
|
|
805
|
-
#### `find(id, key?)` · `firstOrFail()` · `sole()` · `doesntExist()` · `implode(column, glue?)`
|
|
806
|
-
|
|
807
|
-
Find by key (default `"id"`); first-or-throw; exactly-one-or-throw; the negation
|
|
808
|
-
of `exists`; and join one column's values into a string.
|
|
809
|
-
|
|
810
|
-
#### `simplePaginate(page?, perPage?)`
|
|
811
|
-
|
|
812
|
-
`simplePaginate(page = 1, perPage = 15): Promise<SimplePaginated<T>>`
|
|
813
|
-
|
|
814
|
-
A page without a `COUNT` — fetches one extra row to set `hasMore`. Cheaper than
|
|
815
|
-
`paginate` for "load more" UIs.
|
|
816
|
-
|
|
817
|
-
#### `lockForUpdate()` · `sharedLock()`
|
|
818
|
-
|
|
819
|
-
Add `FOR UPDATE` / `FOR SHARE` to the SELECT (inside a transaction). Ignored on
|
|
820
|
-
sqlite.
|
|
821
|
-
|
|
822
|
-
#### `updateOrInsert(match, values?)` · `truncate()` · `incrementEach(cols, extra?)` · `decrementEach(cols, extra?)`
|
|
823
|
-
|
|
824
|
-
Update the first match or insert `{ ...match, ...values }`; empty the table
|
|
825
|
-
(`DELETE` on sqlite); and step several numeric columns in one statement (`cols` is
|
|
826
|
-
an array — each by 1 — or a `{ column: amount }` map).
|
|
827
|
-
|
|
828
|
-
#### `toSql()` · `getBindings()` · `dump()` · `dd()`
|
|
829
|
-
|
|
830
|
-
The compiled `?`-placeholder SQL and its bindings, without executing; `dump` logs
|
|
831
|
-
them and returns the builder; `dd` logs and throws.
|
|
832
|
-
|
|
833
316
|
### Interfaces & types
|
|
834
317
|
|
|
835
318
|
#### `Connection`
|
package/docs/models.md
CHANGED
|
@@ -205,8 +205,11 @@ User.observe({
|
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
Events: `retrieved`, `creating`/`created`, `updating`/`updated`,
|
|
208
|
-
`saving`/`saved`, `deleting`/`deleted`, `restoring`/`restored`.
|
|
209
|
-
|
|
208
|
+
`saving`/`saved`, `deleting`/`deleted`, `restoring`/`restored`.
|
|
209
|
+
|
|
210
|
+
Hooks **inherit**, ancestors first: a hook on a base class fires for every model
|
|
211
|
+
that extends it. That's what lets a base class do real work — a `creating` hook
|
|
212
|
+
that stamps a tenant id is useless if subclasses never fire it.
|
|
210
213
|
|
|
211
214
|
## Query scopes
|
|
212
215
|
|
package/docs/orm.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# ORM
|
|
2
|
+
|
|
3
|
+
Keel's ORM is a compact **active record** over the [query builder](./query-builder.md):
|
|
4
|
+
a model is a class pointed at a table, and its rows come back as typed objects
|
|
5
|
+
with methods. There's no mapper to configure and no separate schema layer — a
|
|
6
|
+
model *is* the row plus behaviour. It runs on whatever [connection](./database.md)
|
|
7
|
+
you registered, so the same code works on Node and the edge.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Model } from "@shaferllc/keel/core";
|
|
11
|
+
|
|
12
|
+
class User extends Model {
|
|
13
|
+
static table = "users";
|
|
14
|
+
declare id: number;
|
|
15
|
+
declare email: string;
|
|
16
|
+
|
|
17
|
+
posts() { return this.hasMany(Post); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const user = await User.find(1);
|
|
21
|
+
await user.posts(); // relations are awaitable
|
|
22
|
+
if (await user.subscribed()) { /* … */ }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This page is the map; each capability has a deep-dive in **[Models](./models.md)**.
|
|
26
|
+
|
|
27
|
+
## What the ORM gives you
|
|
28
|
+
|
|
29
|
+
| Area | What you get | Guide |
|
|
30
|
+
|------|--------------|-------|
|
|
31
|
+
| **CRUD** | `find` / `all` / `create` / `save` / `update` / `delete`, `firstOrCreate`, `updateOrCreate` | [Models → Reading/Writing](./models.md#reading) |
|
|
32
|
+
| **Casts** | `boolean` / `int` / `json` / `date` … columns round-trip as real JS types | [Models → Attribute casts](./models.md#attribute-casts) |
|
|
33
|
+
| **Mass assignment** | `fillable` / `guarded` allow/deny lists guard untrusted input | [Models → Mass assignment](./models.md#mass-assignment) |
|
|
34
|
+
| **Serialization** | `hidden` / `visible` / `appends` shape `toJSON()` | [Models → Serializing](./models.md#serializing) |
|
|
35
|
+
| **Relationships** | `hasOne` / `hasMany` / `belongsTo` / `belongsToMany` + polymorphic `morphOne` / `morphMany` / `morphTo` | [Models → Relationships](./models.md#relationships) |
|
|
36
|
+
| **Eager loading** | `with("posts.comments")` (nested), `withCount`, `Model.load` — no N+1 | [Models → Eager loading](./models.md#eager-loading-avoiding-n1) |
|
|
37
|
+
| **Relationship queries** | `whereHas` / `has` / `doesntHave` | [Models → Querying relationships](./models.md#querying-relationships-with-withcount-wherehas) |
|
|
38
|
+
| **Lifecycle events** | `creating`/`saved`/`deleting`/… hooks and observers, inherited by subclasses | [Models → Lifecycle events](./models.md#lifecycle-events) |
|
|
39
|
+
| **Scopes** | global scopes (tenancy, published-only) + local scope methods | [Models → Query scopes](./models.md#query-scopes) |
|
|
40
|
+
| **Soft deletes** | `deleted_at`, `withTrashed` / `onlyTrashed` / `restore` / `forceDelete` | [Models → Soft deletes](./models.md#soft-deletes) |
|
|
41
|
+
|
|
42
|
+
## How it relates to the rest
|
|
43
|
+
|
|
44
|
+
- The **[query builder](./query-builder.md)** is the layer underneath. `Model.query()`
|
|
45
|
+
returns a model-aware builder, and everything an ORM query can't express
|
|
46
|
+
(raw joins, aggregates, bulk writes) is one `db()` call away.
|
|
47
|
+
- **[Migrations](./migrations.md)** define the tables models read and write.
|
|
48
|
+
- **[Factories & seeders](./factories.md)** generate model rows for tests and demos.
|
|
49
|
+
- **[API resources](./api-resources.md)** turn models into a REST API; **[transformers](./transformers.md)**
|
|
50
|
+
control their serialized shape at the boundary.
|
|
51
|
+
|
|
52
|
+
## When to drop down
|
|
53
|
+
|
|
54
|
+
The ORM is deliberately small — enough for CRUD, relationships, and the common
|
|
55
|
+
query shapes without an ORM dependency. For a gnarly one-off report, reach for
|
|
56
|
+
the [query builder](./query-builder.md) or a raw `connection().select(sql)`; the
|
|
57
|
+
model layer never gets in the way.
|