@shaferllc/keel 0.82.0 → 0.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +2 -2
  2. package/dist/billing/billable.d.ts +2 -3
  3. package/dist/billing/billable.js +2 -3
  4. package/dist/billing/billing.config.stub +1 -1
  5. package/dist/billing/builder.d.ts +2 -2
  6. package/dist/billing/builder.js +2 -2
  7. package/dist/billing/provider.d.ts +1 -1
  8. package/dist/billing/provider.js +2 -2
  9. package/dist/billing/subscription-item.d.ts +0 -1
  10. package/dist/billing/subscription-item.js +0 -1
  11. package/dist/core/decorators.d.ts +5 -7
  12. package/dist/core/decorators.js +5 -7
  13. package/dist/core/exceptions.d.ts +2 -3
  14. package/dist/core/exceptions.js +2 -3
  15. package/dist/core/model-query.d.ts +1 -1
  16. package/dist/core/model-query.js +1 -1
  17. package/dist/core/request-logger.d.ts +2 -2
  18. package/dist/core/request-logger.js +2 -2
  19. package/dist/core/request.js +1 -1
  20. package/dist/core/testing.d.ts +3 -3
  21. package/dist/core/testing.js +3 -3
  22. package/dist/db/d1-http.d.ts +34 -0
  23. package/dist/db/d1-http.js +61 -0
  24. package/dist/db/libsql.d.ts +13 -3
  25. package/dist/teams/models.js +51 -5
  26. package/docs/ai-manifest.json +23 -2
  27. package/docs/billing.md +5 -6
  28. package/docs/changelog.md +106 -21
  29. package/docs/database.md +5 -522
  30. package/docs/decorators.md +2 -6
  31. package/docs/errors.md +1 -1
  32. package/docs/hooks.md +1 -3
  33. package/docs/logger.md +2 -2
  34. package/docs/models.md +5 -2
  35. package/docs/orm.md +57 -0
  36. package/docs/packages.md +3 -3
  37. package/docs/providers.md +6 -6
  38. package/docs/query-builder.md +533 -0
  39. package/docs/request-response.md +2 -2
  40. package/docs/starter-kits.md +88 -0
  41. package/docs/testing.md +1 -2
  42. package/docs/validation.md +1 -2
  43. package/llms-full.txt +5301 -5126
  44. package/llms.txt +4 -1
  45. package/package.json +7 -2
  46. package/templates/api/.env.example +12 -0
  47. package/templates/api/app/Controllers/PostController.ts +37 -0
  48. package/templates/api/app/Http/Kernel.ts +13 -0
  49. package/templates/api/app/Http/Middleware/requestLogger.ts +8 -0
  50. package/templates/api/app/Models/Post.ts +12 -0
  51. package/templates/api/app/Providers/AppServiceProvider.ts +8 -0
  52. package/templates/api/app/Providers/DatabaseServiceProvider.ts +52 -0
  53. package/templates/api/bin/keel.ts +15 -0
  54. package/templates/api/bootstrap/app.ts +24 -0
  55. package/templates/api/bootstrap/providers.edge.ts +14 -0
  56. package/templates/api/bootstrap/providers.ts +7 -0
  57. package/templates/api/config/app.ts +10 -0
  58. package/templates/api/config/database.ts +42 -0
  59. package/templates/api/database/migrations/0001_create_posts.ts +20 -0
  60. package/templates/api/package.json +30 -0
  61. package/templates/api/routes/web.ts +12 -0
  62. package/templates/api/tests/posts.test.ts +30 -0
  63. package/templates/api/tsconfig.json +16 -0
  64. package/templates/api/worker.ts +33 -0
  65. package/templates/api/wrangler.jsonc +22 -0
  66. package/templates/app/.env.example +12 -0
  67. package/templates/app/app/Controllers/AuthController.ts +117 -0
  68. package/templates/app/app/Controllers/DashboardController.ts +37 -0
  69. package/templates/app/app/Controllers/HomeController.ts +10 -0
  70. package/templates/app/app/Http/Kernel.ts +21 -0
  71. package/templates/app/app/Http/Middleware/requestLogger.ts +8 -0
  72. package/templates/app/app/Models/User.ts +15 -0
  73. package/templates/app/app/Providers/AppServiceProvider.ts +12 -0
  74. package/templates/app/app/Providers/DatabaseServiceProvider.ts +52 -0
  75. package/templates/app/bin/keel.ts +15 -0
  76. package/templates/app/bootstrap/app.ts +24 -0
  77. package/templates/app/bootstrap/providers.edge.ts +15 -0
  78. package/templates/app/bootstrap/providers.ts +18 -0
  79. package/templates/app/config/app.ts +10 -0
  80. package/templates/app/config/database.ts +42 -0
  81. package/templates/app/database/migrations/0001_create_users.ts +21 -0
  82. package/templates/app/package.json +35 -0
  83. package/templates/app/public/.gitkeep +2 -0
  84. package/templates/app/resources/css/app.css +1 -0
  85. package/templates/app/resources/views/auth/forgot.tsx +30 -0
  86. package/templates/app/resources/views/auth/login.tsx +24 -0
  87. package/templates/app/resources/views/auth/register.tsx +24 -0
  88. package/templates/app/resources/views/auth/two-factor.tsx +22 -0
  89. package/templates/app/resources/views/dashboard.tsx +20 -0
  90. package/templates/app/resources/views/layout.tsx +15 -0
  91. package/templates/app/resources/views/welcome.tsx +29 -0
  92. package/templates/app/routes/web.ts +35 -0
  93. package/templates/app/tests/auth.test.ts +47 -0
  94. package/templates/app/tsconfig.json +31 -0
  95. package/templates/app/worker.ts +33 -0
  96. package/templates/app/wrangler.jsonc +25 -0
  97. package/templates/minimal/.env.example +8 -0
  98. package/templates/minimal/app/Controllers/HomeController.ts +14 -0
  99. package/templates/minimal/app/Http/Kernel.ts +22 -0
  100. package/templates/minimal/app/Http/Middleware/requestLogger.ts +8 -0
  101. package/templates/minimal/app/Providers/AppServiceProvider.ts +8 -0
  102. package/templates/minimal/bin/keel.ts +15 -0
  103. package/templates/minimal/bootstrap/app.ts +24 -0
  104. package/templates/minimal/bootstrap/providers.ts +6 -0
  105. package/templates/minimal/config/app.ts +10 -0
  106. package/templates/minimal/package.json +29 -0
  107. package/templates/minimal/public/.gitkeep +2 -0
  108. package/templates/minimal/resources/css/app.css +1 -0
  109. package/templates/minimal/resources/views/layout.tsx +15 -0
  110. package/templates/minimal/resources/views/welcome.tsx +15 -0
  111. package/templates/minimal/routes/web.ts +13 -0
  112. package/templates/minimal/tsconfig.json +18 -0
  113. package/templates/minimal/worker.ts +23 -0
  114. package/templates/minimal/wrangler.jsonc +10 -0
  115. package/templates/saas/.env.example +12 -0
  116. package/templates/saas/app/Controllers/AuthController.ts +125 -0
  117. package/templates/saas/app/Controllers/DashboardController.ts +37 -0
  118. package/templates/saas/app/Controllers/HomeController.ts +10 -0
  119. package/templates/saas/app/Controllers/TeamController.ts +88 -0
  120. package/templates/saas/app/Http/Kernel.ts +27 -0
  121. package/templates/saas/app/Http/Middleware/requestLogger.ts +8 -0
  122. package/templates/saas/app/Models/Project.ts +20 -0
  123. package/templates/saas/app/Models/User.ts +15 -0
  124. package/templates/saas/app/Providers/AppServiceProvider.ts +12 -0
  125. package/templates/saas/app/Providers/DatabaseServiceProvider.ts +52 -0
  126. package/templates/saas/bin/keel.ts +15 -0
  127. package/templates/saas/bootstrap/app.ts +24 -0
  128. package/templates/saas/bootstrap/providers.edge.ts +18 -0
  129. package/templates/saas/bootstrap/providers.ts +22 -0
  130. package/templates/saas/config/app.ts +10 -0
  131. package/templates/saas/config/database.ts +42 -0
  132. package/templates/saas/database/migrations/0001_create_users.ts +21 -0
  133. package/templates/saas/database/migrations/0002_create_projects.ts +20 -0
  134. package/templates/saas/package.json +35 -0
  135. package/templates/saas/public/.gitkeep +2 -0
  136. package/templates/saas/resources/css/app.css +1 -0
  137. package/templates/saas/resources/views/auth/forgot.tsx +30 -0
  138. package/templates/saas/resources/views/auth/login.tsx +24 -0
  139. package/templates/saas/resources/views/auth/register.tsx +24 -0
  140. package/templates/saas/resources/views/auth/two-factor.tsx +22 -0
  141. package/templates/saas/resources/views/dashboard.tsx +20 -0
  142. package/templates/saas/resources/views/layout.tsx +15 -0
  143. package/templates/saas/resources/views/teams/index.tsx +85 -0
  144. package/templates/saas/resources/views/welcome.tsx +29 -0
  145. package/templates/saas/routes/web.ts +44 -0
  146. package/templates/saas/tests/auth.test.ts +47 -0
  147. package/templates/saas/tests/teams.test.ts +27 -0
  148. package/templates/saas/tsconfig.json +31 -0
  149. package/templates/saas/worker.ts +33 -0
  150. 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
- ## Query builder
113
+ ## Queries & models
114
114
 
115
- Start a query with `db(table)`, chain constraints (they return the builder, so
116
- order doesn't matter), and finish with a terminal method. Nothing hits the
117
- database until a terminal runs. Every value becomes a **binding**, never
118
- string-interpolated SQL the builder is injection-safe by construction. It's
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`
@@ -2,10 +2,9 @@
2
2
 
3
3
  Attach named, computed values to the current request — `request.user`,
4
4
  `request.tenant`, `request.locale` — resolved **lazily** and **memoized for the
5
- life of the request**. Inspired by [Fastify's decorators](https://fastify.dev/docs/latest/Reference/Decorators/),
6
- but without the footguns: you register a resolver once, and Keel computes it on
5
+ life of the request**. You register a resolver once, and Keel computes it on
7
6
  first access and caches it per request. No null-placeholder declaration, no
8
- `onRequest` hook to remember, no shared-state leak between requests.
7
+ shared-state leak between requests.
9
8
 
10
9
  > Decorating the **application** is already the [service container's](./container.md)
11
10
  > job — `bind` / `singleton` / `instance` / `make`, with `bound()` as
@@ -53,9 +52,6 @@ setRequestValue("user", theAuthenticatedUser);
53
52
  const user = await decorated("user"); // returns the value set above, no re-lookup
54
53
  ```
55
54
 
56
- This is the clean version of Fastify's "declare a placeholder, set it in an
57
- `onRequest` hook" pattern.
58
-
59
55
  ## Why lazy + memoized
60
56
 
61
57
  Resolving the current user (or tenant, or locale, or a feature-flag set) is the
package/docs/errors.md CHANGED
@@ -185,7 +185,7 @@ subclasses don't define either — they render through the default path.
185
185
 
186
186
  When all you want is a coded error class — a stable `code`, a message, a status —
187
187
  skip the boilerplate and mint one with `createError`. It's the ergonomic shortcut
188
- for the common case, inspired by Fastify's `@fastify/error`:
188
+ for the common case:
189
189
 
190
190
  ```ts
191
191
  import { createError } from "@shaferllc/keel/core";
package/docs/hooks.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # Lifecycle Hooks
2
2
 
3
3
  Tap into the **application lifecycle** — run code once the app is ready, clean up
4
- on shutdown, and observe route registration. Inspired by
5
- [Fastify's hooks](https://fastify.dev/docs/latest/Reference/Hooks/), scoped to the
6
- parts Keel doesn't already cover.
4
+ on shutdown, and observe route registration.
7
5
 
8
6
  > **Request-lifecycle hooks** (before/after a request, on error) are
9
7
  > [middleware](./middleware.md) in Keel — `HttpKernel.use()`, route/group
package/docs/logger.md CHANGED
@@ -156,8 +156,8 @@ With no options it defaults to `level: "info"`, `pretty: false`, and no bindings
156
156
 
157
157
  `requestLogger()` is a built-in middleware that binds a **child logger with a
158
158
  generated `reqId` to each request**, so every log line within a request
159
- correlates — Fastify's `request.log`. Install it in your HTTP kernel, then reach
160
- the request's logger anywhere with `requestLog()`:
159
+ correlates. Install it in your HTTP kernel, then reach the request's logger
160
+ anywhere with `requestLog()`:
161
161
 
162
162
  ```ts
163
163
  import { requestLogger, requestLog } from "@shaferllc/keel/core";
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`. They're keyed by
209
- the exact class (subclasses don't inherit a parent's hooks).
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.
package/docs/packages.md CHANGED
@@ -7,9 +7,9 @@ adds the conventions a *shippable* package needs so it can carry its own schema
7
7
  and assets instead of asking the app to wire them by hand.
8
8
 
9
9
  [Keel Watch](./watch.md) — the debug dashboard — is a first-party package and the
10
- reference implementation of everything below. [Billing](./billing.md) (a Cashier
11
- port for Stripe and Paddle) is another, and shows a package contributing models,
12
- a schema migration, gateway drivers, and verified webhook routes.
10
+ reference implementation of everything below. [Billing](./billing.md) (Stripe and
11
+ Paddle subscriptions) is another, and shows a package contributing models, a
12
+ schema migration, gateway drivers, and verified webhook routes.
13
13
 
14
14
  ## The shape of a package
15
15