@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.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
package/docs/models.md ADDED
@@ -0,0 +1,810 @@
1
+ # Models
2
+
3
+ `Model` is a tiny active-record layer over the [query builder](./database.md).
4
+ Subclass it, point it at a table, and you get `find` / `all` / `create` /
5
+ `save` / `delete` — no ORM to configure. It runs through whatever
6
+ [connection](./database.md) you registered, so it works on Node and the edge.
7
+
8
+ ## Define a model
9
+
10
+ ```ts
11
+ import { Model } from "@shaferllc/keel/core";
12
+
13
+ export class User extends Model {
14
+ static table = "users";
15
+ static primaryKey = "id"; // default
16
+
17
+ declare id: number;
18
+ declare email: string;
19
+ declare name: string;
20
+ }
21
+ ```
22
+
23
+ Use `declare` for columns — it types the properties without emitting fields that
24
+ would shadow the row values the model is hydrated with.
25
+
26
+ ## Reading
27
+
28
+ ```ts
29
+ await User.all(); // User[]
30
+ await User.find(1); // User | null
31
+ await User.findOrFail(1); // User (throws NotFoundException if missing)
32
+ await User.first(); // User | null
33
+ await User.where("active", true); // User[]
34
+ ```
35
+
36
+ For anything richer, `User.query()` returns the underlying query builder:
37
+
38
+ ```ts
39
+ const rows = await User.query().where("age", ">", 18).orderBy("name").limit(10).get();
40
+ ```
41
+
42
+ ## Writing
43
+
44
+ ```ts
45
+ // create
46
+ const user = await User.create({ email: "a@b.com", name: "Ada" });
47
+
48
+ // update — change attributes, then save
49
+ user.name = "Grace";
50
+ await user.save();
51
+
52
+ // new instance — save() inserts and back-fills the primary key
53
+ const draft = new User({ email: "new@x.com" });
54
+ await draft.save();
55
+ draft.id; // now set
56
+
57
+ // delete
58
+ await user.delete();
59
+ ```
60
+
61
+ `save()` inserts when there's no primary key and updates when there is — one
62
+ method for both. `update(attrs)` is `fill` + `save`, and `refresh()` reloads a
63
+ model's columns from the database:
64
+
65
+ ```ts
66
+ await user.update({ name: "Grace" }); // mass-assign + save
67
+ await user.refresh(); // re-read the row
68
+ ```
69
+
70
+ ### Find-or-create
71
+
72
+ ```ts
73
+ // Return the first matching row, or create it from { ...match, ...values }.
74
+ const tag = await Tag.firstOrCreate({ slug: "keel" }, { name: "Keel" });
75
+ // Update the match if it exists, otherwise create it.
76
+ const sub = await Subscription.updateOrCreate({ user_id: 1 }, { plan: "pro" });
77
+ ```
78
+
79
+ ## Timestamps
80
+
81
+ Set `static timestamps = true` and Keel manages `created_at` / `updated_at` — both
82
+ on insert, just `updated_at` on update:
83
+
84
+ ```ts
85
+ class Post extends Model {
86
+ static table = "posts";
87
+ static timestamps = true;
88
+ // override the column names if yours differ:
89
+ // static createdAtColumn = "inserted_at";
90
+ // static updatedAtColumn = "modified_at";
91
+ }
92
+
93
+ const post = await Post.create({ title: "Hi" });
94
+ post.created_at; // set
95
+ post.updated_at; // set (same instant)
96
+ ```
97
+
98
+ ## Pagination
99
+
100
+ `Model.paginate(page, perPage)` returns a page of models plus metadata:
101
+
102
+ ```ts
103
+ const { data, total, currentPage, lastPage, perPage } = await Post.paginate(2, 15);
104
+ ```
105
+
106
+ `data` is `Post[]`; the rest is pagination state (defaults: page `1`, `15` per
107
+ page). The query builder has the same `paginate()` if you're not using models.
108
+
109
+ ## Attribute casts
110
+
111
+ By default columns are whatever the driver returns (SQLite hands back `1`/`0`
112
+ for booleans, strings for JSON). Declare `static casts` and values round-trip as
113
+ real JS types — cast when read (from the database or `fill`) and back to storable
114
+ primitives when written:
115
+
116
+ ```ts
117
+ class Post extends Model {
118
+ static table = "posts";
119
+ static casts = {
120
+ published: "boolean", // 1/0 <-> true/false
121
+ views: "int", // "10" -> 10
122
+ meta: "json", // '{"a":1}' <-> { a: 1 } (also "array")
123
+ posted_at: "date", // ISO string <-> Date
124
+ } as const;
125
+ }
126
+
127
+ const post = await Post.find(1);
128
+ post.published; // true (a real boolean)
129
+ post.meta; // { … } (a real object)
130
+ post.published = false;
131
+ await post.save(); // stored as 0; meta re-serialized to a JSON string
132
+ ```
133
+
134
+ The `as const` keeps the string literals from widening to `string` so the map
135
+ still satisfies `Casts` (`Record<string, CastType>`) — without it TypeScript
136
+ infers `string` values and the assignment to the base `static casts` fails.
137
+
138
+ Casts are what let a `boolean` or `json` column bind cleanly on real drivers,
139
+ which reject JS booleans and objects as parameters. Supported types: `int` (alias
140
+ `integer`), `float` (alias `number`), `boolean` (alias `bool`), `string`, `json`
141
+ / `array`, `date`. Both directions are null-safe — `null`/`undefined` pass
142
+ through uncast — and reads are tolerant of already-cast input, so hydrating a row
143
+ twice or casting a value that's already a `Date` is a no-op.
144
+
145
+ ## Mass assignment
146
+
147
+ `create()` and `fill()` take untrusted input (often a request body), so they're
148
+ guarded. Whitelist columns with `static fillable`, or blacklist with
149
+ `static guarded` — columns outside the allowance are silently dropped:
150
+
151
+ ```ts
152
+ class Post extends Model {
153
+ static table = "posts";
154
+ static fillable = ["title", "body"]; // only these are mass-assignable
155
+ // — or —
156
+ static guarded = ["is_admin"]; // everything except these
157
+ }
158
+
159
+ await Post.create({ title: "Hi", is_admin: true }); // is_admin dropped
160
+ post.fill(request.all()); // safe from over-posting
161
+ post.forceFill({ is_admin: true }); // explicit bypass
162
+ ```
163
+
164
+ With neither declared, all attributes are assignable (the default). Direct
165
+ property assignment (`post.is_admin = true`) is never guarded — guarding is only
166
+ about *mass* assignment from untrusted data.
167
+
168
+ ## Serializing
169
+
170
+ ```ts
171
+ user.toJSON(); // a plain object of the (cast) attributes + loaded relations
172
+ return json(user); // works directly — json() serializes it
173
+ user.fill({ name: "X" }); // merge mass-assignable attributes without saving
174
+ ```
175
+
176
+ ## Relationships
177
+
178
+ Define a relationship as a method that returns one of `hasMany` / `hasOne` /
179
+ `belongsTo` / `belongsToMany`. Keys follow conventions (the owning model's name
180
+ plus its primary key — `user_id`) but every one is overridable.
181
+
182
+ ```ts
183
+ class User extends Model {
184
+ static table = "users";
185
+ posts() { return this.hasMany(Post); } // posts.user_id = users.id
186
+ profile() { return this.hasOne(Profile); } // profiles.user_id = users.id
187
+ roles() { return this.belongsToMany(Role); } // role_user pivot
188
+ }
189
+
190
+ class Post extends Model {
191
+ static table = "posts";
192
+ author() { return this.belongsTo(User); } // posts.user_id -> users.id
193
+ }
194
+ ```
195
+
196
+ Relations are **awaitable** — read them lazily with `await`:
197
+
198
+ ```ts
199
+ const posts = await user.posts(); // Post[]
200
+ const author = await post.author(); // User | null
201
+ ```
202
+
203
+ Need to constrain or sort? `.query()` hands back the underlying query builder:
204
+
205
+ ```ts
206
+ const recent = await user.posts().query().orderBy("created_at", "desc").limit(5).get();
207
+ ```
208
+
209
+ ### Eager loading (avoiding N+1)
210
+
211
+ Loading a relation per model in a loop is N+1 queries. `Model.load()` fetches
212
+ them all with one extra query per relation, using `whereIn`:
213
+
214
+ ```ts
215
+ const users = await User.all();
216
+ await User.load(users, "posts", "roles"); // 2 extra queries total, not 2×N
217
+
218
+ users[0].getRelation("posts"); // Post[]
219
+ users[0].toJSON(); // includes `posts` and `roles`
220
+ ```
221
+
222
+ Loaded relations are stored off the model, so they never leak into `save()`,
223
+ and `toJSON()` serializes them (nested models included).
224
+
225
+ ### Many-to-many
226
+
227
+ `belongsToMany` reads through a pivot table (default name: the two table names
228
+ sorted and joined, e.g. `role_user`) and can write it too:
229
+
230
+ ```ts
231
+ await user.roles().attach(roleId); // insert a pivot row
232
+ await user.roles().detach(roleId); // remove one (or all, with no argument)
233
+ await user.roles().sync([1, 2, 3]); // make the pivot contain exactly these
234
+ ```
235
+
236
+ Every relation runs on the driver-agnostic query builder — no JOINs, no driver
237
+ imports — so relationships stay edge-safe. Overriding keys:
238
+
239
+ ```ts
240
+ this.hasMany(Post, "authored_by", "id");
241
+ this.belongsTo(User, "owner_id", "id");
242
+ this.belongsToMany(Role, "user_roles", "user_id", "role_id");
243
+ ```
244
+
245
+ ## What this is (and isn't)
246
+
247
+ This is a deliberately small active-record — enough for CRUD, relationships,
248
+ casts, and simple queries without an ORM dependency. Nested eager loading
249
+ (`posts.comments`) and query-time `with()` aren't here yet. For complex schemas
250
+ you can always drop to `db()` or your driver directly.
251
+
252
+ ---
253
+
254
+ ## API reference
255
+
256
+ Everything below imports from `@shaferllc/keel/core`.
257
+
258
+ ### `Model` — static methods
259
+
260
+ You call these on your subclass (`User.find(1)`), not on `Model` itself. Each
261
+ read hydrates rows into instances of the class it was called on.
262
+
263
+ #### `Model.query()`
264
+
265
+ `static query(): QueryBuilder`
266
+
267
+ Returns a raw [query builder](./database.md) scoped to the model's table — the
268
+ escape hatch for anything the finders don't cover.
269
+
270
+ ```ts
271
+ const rows = await User.query().where("age", ">", 18).orderBy("name").limit(10).get();
272
+ ```
273
+
274
+ **Notes:** returns plain `Row`s, not hydrated models — map them through
275
+ `new User(row)` yourself if you need instances.
276
+
277
+ #### `Model.all()`
278
+
279
+ `static all<T extends Model>(this: ModelClass<T>): Promise<T[]>`
280
+
281
+ Fetches every row in the table as hydrated models.
282
+
283
+ ```ts
284
+ const users = await User.all(); // User[]
285
+ ```
286
+
287
+ **Notes:** no `where`, no `limit` — it reads the whole table. Reach for `query()`
288
+ when that's too much.
289
+
290
+ #### `Model.find(id)`
291
+
292
+ `static find<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T | null>`
293
+
294
+ Looks a model up by primary key. Resolves to `null` when nothing matches.
295
+
296
+ ```ts
297
+ const user = await User.find(1); // User | null
298
+ ```
299
+
300
+ **Notes:** matches on `static primaryKey` (default `"id"`). Returns `null`, not
301
+ `undefined`.
302
+
303
+ #### `Model.findOrFail(id)`
304
+
305
+ `static findOrFail<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T>`
306
+
307
+ Like `find`, but throws instead of returning `null`.
308
+
309
+ ```ts
310
+ const user = await User.findOrFail(1); // User (or throws)
311
+ ```
312
+
313
+ **Notes:** throws `NotFoundException` with message `"<ClassName> <id> not
314
+ found"`. `NotFoundException` is an `HttpException` (status 404), so an HTTP
315
+ handler surfaces it as a 404 without extra work.
316
+
317
+ #### `Model.first()`
318
+
319
+ `static first<T extends Model>(this: ModelClass<T>): Promise<T | null>`
320
+
321
+ Returns the first row in the table (no ordering), or `null`.
322
+
323
+ ```ts
324
+ const anyUser = await User.first(); // User | null
325
+ ```
326
+
327
+ **Notes:** unordered — the "first" row is whatever the driver returns first. Add
328
+ your own `orderBy` via `query().first()` when order matters.
329
+
330
+ #### `Model.where(column, value)`
331
+
332
+ `static where<T extends Model>(this: ModelClass<T>, column: string, value: unknown): Promise<T[]>`
333
+
334
+ A convenience finder for a single equality condition. Runs immediately and
335
+ returns hydrated models.
336
+
337
+ ```ts
338
+ const active = await User.where("active", true); // User[]
339
+ ```
340
+
341
+ **Notes:** equality only, and it's a terminal call — it returns a `Promise`, not
342
+ a builder, so you can't chain more constraints onto it. Use `query()` for
343
+ operators, `OR`, ordering, or limits.
344
+
345
+ #### `Model.create(attributes)`
346
+
347
+ `static create<T extends Model>(this: ModelClass<T>, attributes: Row): Promise<T>`
348
+
349
+ Mass-assigns `attributes` (filtered through `fillable`/`guarded`), inserts one
350
+ row, and returns the hydrated model with its new primary key set.
351
+
352
+ ```ts
353
+ const user = await User.create({ email: "a@b.com", name: "Ada" });
354
+ user.id; // populated from insertId
355
+ ```
356
+
357
+ **Notes:** attributes outside the mass-assignment allowance are silently dropped
358
+ before the insert. Values are cast to storable primitives on the way in. If the
359
+ driver doesn't report an `insertId`, the primary key stays unset.
360
+
361
+ #### `Model.load(models, ...names)`
362
+
363
+ `static load<T extends Model>(models: T[], ...names: string[]): Promise<T[]>`
364
+
365
+ Eager-loads one or more relationships onto an array of already-fetched models —
366
+ one extra query per relation, the fix for N+1. Returns the same array.
367
+
368
+ ```ts
369
+ const users = await User.all();
370
+ await User.load(users, "posts", "roles"); // 2 extra queries, not 2×N
371
+ users[0].getRelation("posts"); // Post[]
372
+ ```
373
+
374
+ **Notes:** each name must be a relationship method on the model; an unknown name
375
+ throws `"<ClassName> has no relation "<name>""`. An empty `models` array is
376
+ returned untouched (no queries). Loaded results are stored off the model (see
377
+ `getRelation`) and never leak into `save()`.
378
+
379
+ #### `Model.filterFillable(attributes)`
380
+
381
+ `static filterFillable(attributes: Row): Row`
382
+
383
+ Returns a copy of `attributes` keeping only what mass-assignment allows — the
384
+ guard `create`/`fill` apply. Rarely called directly.
385
+
386
+ ```ts
387
+ const safe = Post.filterFillable(request.all());
388
+ ```
389
+
390
+ **Notes:** if `fillable` is non-empty it's an allowlist; else if `guarded` is
391
+ non-empty it's a denylist; with neither, everything passes. `fillable` wins when
392
+ both are set.
393
+
394
+ #### `Model.toDatabase(attributes)`
395
+
396
+ `static toDatabase(attributes: Row): Row`
397
+
398
+ Casts `attributes` to their storable primitives (via `castSet`) for a write.
399
+ Rarely called directly — `create`/`save` use it internally.
400
+
401
+ ```ts
402
+ const storable = Post.toDatabase({ published: true }); // { published: 1 }
403
+ ```
404
+
405
+ ### `Model` — configuration statics
406
+
407
+ Set these on the subclass to configure it. All have defaults.
408
+
409
+ #### `static table`
410
+
411
+ `static table: string`
412
+
413
+ The table the model reads and writes. Required — defaults to `""`.
414
+
415
+ ```ts
416
+ class User extends Model { static table = "users"; }
417
+ ```
418
+
419
+ #### `static primaryKey`
420
+
421
+ `static primaryKey: string`
422
+
423
+ The primary-key column used by `find`, `save`, and `delete`. Defaults to `"id"`.
424
+
425
+ ```ts
426
+ class Session extends Model { static table = "sessions"; static primaryKey = "token"; }
427
+ ```
428
+
429
+ #### `static fillable`
430
+
431
+ `static fillable: string[]`
432
+
433
+ Allowlist of mass-assignable columns. Defaults to `[]` (meaning "not an
434
+ allowlist" — see `filterFillable`).
435
+
436
+ ```ts
437
+ class Post extends Model { static table = "posts"; static fillable = ["title", "body"]; }
438
+ ```
439
+
440
+ #### `static guarded`
441
+
442
+ `static guarded: string[]`
443
+
444
+ Denylist of columns that mass-assignment must never set. Ignored when `fillable`
445
+ is non-empty. Defaults to `[]`.
446
+
447
+ ```ts
448
+ class Post extends Model { static table = "posts"; static guarded = ["is_admin"]; }
449
+ ```
450
+
451
+ #### `static casts`
452
+
453
+ `static casts: Casts`
454
+
455
+ Maps columns to cast types so values round-trip as real JS types. Declare it
456
+ `as const` so the literals don't widen to `string`. Defaults to `{}`.
457
+
458
+ ```ts
459
+ class Post extends Model {
460
+ static table = "posts";
461
+ static casts = { published: "boolean", meta: "json" } as const;
462
+ }
463
+ ```
464
+
465
+ ### `Model` — instance methods
466
+
467
+ #### `new Model(attributes?)`
468
+
469
+ `constructor(attributes?: Row)`
470
+
471
+ Hydrates a model from a row. Assignment is unguarded (rows come from the
472
+ database) but every column named in `casts` is cast on the way in.
473
+
474
+ ```ts
475
+ const draft = new User({ email: "new@x.com" });
476
+ ```
477
+
478
+ **Notes:** hydration bypasses `fillable`/`guarded` — it's for trusted rows, not
479
+ request bodies. Use `create`/`fill` for untrusted input.
480
+
481
+ #### `save()`
482
+
483
+ `save(): Promise<this>`
484
+
485
+ Inserts when the primary key is absent, updates when it's present — one method
486
+ for both. Back-fills the primary key after an insert.
487
+
488
+ ```ts
489
+ const u = new User({ email: "a@b.com" });
490
+ await u.save(); // INSERT; u.id now set
491
+ u.name = "Grace";
492
+ await u.save(); // UPDATE where id = u.id
493
+ ```
494
+
495
+ **Notes:** writes every own column (cast to storable primitives); loaded
496
+ relations live off-instance and never leak in. An update with no changed columns
497
+ still issues the query.
498
+
499
+ #### `delete()`
500
+
501
+ `delete(): Promise<void>`
502
+
503
+ Deletes the row matching this model's primary key.
504
+
505
+ ```ts
506
+ await user.delete();
507
+ ```
508
+
509
+ **Notes:** keys off the current `primaryKey` value; on a model without one, the
510
+ `where` binds `undefined`. Hard delete only — there's no soft-delete built in.
511
+
512
+ #### `fill(attributes)`
513
+
514
+ `fill(attributes: Row): this`
515
+
516
+ Merges mass-assignable attributes into the model (filtered + cast), without
517
+ saving. Returns `this` for chaining.
518
+
519
+ ```ts
520
+ user.fill(request.all()).save();
521
+ ```
522
+
523
+ **Notes:** respects `fillable`/`guarded` — safe for request bodies. Doesn't touch
524
+ the database until you call `save()`.
525
+
526
+ #### `forceFill(attributes)`
527
+
528
+ `forceFill(attributes: Row): this`
529
+
530
+ Like `fill`, but bypasses mass-assignment guarding. Still casts.
531
+
532
+ ```ts
533
+ user.forceFill({ is_admin: true }); // deliberate over-post
534
+ ```
535
+
536
+ **Notes:** the explicit escape hatch — only use it with trusted data.
537
+
538
+ #### `toJSON()`
539
+
540
+ `toJSON(): Row`
541
+
542
+ Returns a plain object of the model's (cast) attributes plus any loaded
543
+ relations, nested models included. `JSON.stringify` and `json()` call it
544
+ automatically.
545
+
546
+ ```ts
547
+ return json(user); // toJSON() runs under the hood
548
+ user.toJSON(); // { id, email, …, posts: [...] } if `posts` was loaded
549
+ ```
550
+
551
+ **Notes:** only *loaded* relations appear — unloaded relationship methods are not
552
+ invoked. Relations serialize recursively via each nested model's `toJSON`.
553
+
554
+ #### `getRelation(name)`
555
+
556
+ `getRelation<T = unknown>(name: string): T | undefined`
557
+
558
+ Reads a relation previously loaded by `Model.load` (or `setRelation`). Returns
559
+ `undefined` if it was never loaded.
560
+
561
+ ```ts
562
+ const posts = users[0].getRelation<Post[]>("posts");
563
+ ```
564
+
565
+ **Notes:** does not trigger a query — it only reads what's already cached. Awaiting
566
+ the relationship method (`await user.posts()`) is the lazy alternative.
567
+
568
+ #### `setRelation(name, value)`
569
+
570
+ `setRelation(name: string, value: unknown): this`
571
+
572
+ Stores a relation result under `name` (what eager loading uses under the hood).
573
+ Returns `this`.
574
+
575
+ ```ts
576
+ user.setRelation("posts", await user.posts());
577
+ ```
578
+
579
+ **Notes:** the store is keyed off the instance (a `WeakMap`), so it never leaks
580
+ into `save()`; `toJSON()` picks it up.
581
+
582
+ #### `hasMany(related, foreignKey?, localKey?)`
583
+
584
+ `hasMany<T extends Model>(related: ModelClass<T>, foreignKey?: string, localKey?: string): HasMany<T>`
585
+
586
+ Declares a one-to-many: this model has many `related` rows joined by a foreign
587
+ key on the related table. Call it from a relationship method.
588
+
589
+ ```ts
590
+ posts() { return this.hasMany(Post); } // posts.user_id = users.id
591
+ authored() { return this.hasMany(Post, "authored_by", "id"); }
592
+ ```
593
+
594
+ **Notes:** `foreignKey` defaults to `<thismodel>_<primaryKey>` (e.g. `user_id`);
595
+ `localKey` defaults to this model's primary key.
596
+
597
+ #### `hasOne(related, foreignKey?, localKey?)`
598
+
599
+ `hasOne<T extends Model>(related: ModelClass<T>, foreignKey?: string, localKey?: string): HasOne<T>`
600
+
601
+ Declares a one-to-one, same key conventions as `hasMany`.
602
+
603
+ ```ts
604
+ profile() { return this.hasOne(Profile); } // profiles.user_id = users.id
605
+ ```
606
+
607
+ **Notes:** resolves to a single model or `null` (the first matching row).
608
+
609
+ #### `belongsTo(related, foreignKey?, ownerKey?)`
610
+
611
+ `belongsTo<T extends Model>(related: ModelClass<T>, foreignKey?: string, ownerKey?: string): BelongsTo<T>`
612
+
613
+ Declares the inverse: this model carries the foreign key pointing at `related`.
614
+
615
+ ```ts
616
+ author() { return this.belongsTo(User); } // posts.user_id -> users.id
617
+ owner() { return this.belongsTo(User, "owner_id", "id"); }
618
+ ```
619
+
620
+ **Notes:** `foreignKey` defaults to `<related>_<related.primaryKey>` (a column on
621
+ *this* table); `ownerKey` defaults to the related model's primary key. Resolves to
622
+ `null` when the foreign key is null.
623
+
624
+ #### `belongsToMany(related, pivotTable?, foreignPivotKey?, relatedPivotKey?, parentKey?, relatedKey?)`
625
+
626
+ `belongsToMany<T extends Model>(related: ModelClass<T>, pivotTable?: string, foreignPivotKey?: string, relatedPivotKey?: string, parentKey?: string, relatedKey?: string): BelongsToMany<T>`
627
+
628
+ Declares a many-to-many through a pivot table.
629
+
630
+ ```ts
631
+ roles() { return this.belongsToMany(Role); } // role_user pivot
632
+ roles() { return this.belongsToMany(Role, "user_roles", "user_id", "role_id"); }
633
+ ```
634
+
635
+ **Notes:** `pivotTable` defaults to the two model names lowercased, sorted, and
636
+ joined with `_` (User + Role → `role_user`). The pivot keys default to
637
+ `<model>_<primaryKey>`. Reads as two `whereIn` queries (no JOIN), so it stays
638
+ edge-safe.
639
+
640
+ ### Relations
641
+
642
+ You never `new` these — a relationship method (`user.posts()`) returns one. Each
643
+ is **awaitable**: `await`ing it runs the query and resolves to the result. All
644
+ four share the `Relation` base contract (`query`, `get`, `eager`, `then`);
645
+ `BelongsToMany` adds pivot writes.
646
+
647
+ #### `Relation` (abstract base)
648
+
649
+ `abstract class Relation<TRelated extends Model, TResult> implements PromiseLike<TResult>`
650
+
651
+ The shared base. Because it's `PromiseLike`, a relation resolves through `await`
652
+ or `.then()` without calling `get()` explicitly.
653
+
654
+ ```ts
655
+ const posts = await user.posts(); // then() → get()
656
+ const post = await user.posts().get(); // same thing, explicit
657
+ ```
658
+
659
+ ##### `query()`
660
+
661
+ `query(): QueryBuilder`
662
+
663
+ Returns the underlying query builder with the relationship constraint applied —
664
+ constrain, sort, or paginate before fetching.
665
+
666
+ ```ts
667
+ const recent = await user.posts().query().orderBy("created_at", "desc").limit(5).get();
668
+ ```
669
+
670
+ **Notes:** for `belongsToMany`, `query()` is the related-table builder *without*
671
+ the pivot filter — prefer `get()`/`await` for the full pivot-aware read.
672
+
673
+ ##### `get()`
674
+
675
+ `get(): Promise<TResult>`
676
+
677
+ Runs the relationship and returns its result — the type depends on the subclass
678
+ (see below).
679
+
680
+ ##### `eager(models, name)`
681
+
682
+ `eager(models: Model[], name: string): Promise<void>`
683
+
684
+ Batch-loads this relationship onto many parents and stores each result via
685
+ `setRelation`. Called by `Model.load` — you rarely call it directly.
686
+
687
+ ##### `then(onFulfilled?, onRejected?)`
688
+
689
+ `then<R1, R2>(onFulfilled?, onRejected?): PromiseLike<R1 | R2>`
690
+
691
+ The `PromiseLike` hook that makes a relation awaitable; it delegates to `get()`.
692
+
693
+ #### `HasMany.get()`
694
+
695
+ `get(): Promise<T[]>`
696
+
697
+ Returns all related rows as hydrated models (empty array when none).
698
+
699
+ ```ts
700
+ const posts: Post[] = await user.posts();
701
+ ```
702
+
703
+ #### `HasOne.get()`
704
+
705
+ `get(): Promise<T | null>`
706
+
707
+ Returns the single related model, or `null`.
708
+
709
+ ```ts
710
+ const profile = await user.profile(); // Profile | null
711
+ ```
712
+
713
+ #### `BelongsTo.get()`
714
+
715
+ `get(): Promise<T | null>`
716
+
717
+ Returns the owner model, or `null` when this model's foreign key is null.
718
+
719
+ ```ts
720
+ const author = await post.author(); // User | null
721
+ ```
722
+
723
+ #### `BelongsToMany.get()`
724
+
725
+ `get(): Promise<T[]>`
726
+
727
+ Reads the pivot rows, then the related rows they point at, as hydrated models.
728
+
729
+ ```ts
730
+ const roles: Role[] = await user.roles();
731
+ ```
732
+
733
+ **Notes:** related ids are de-duplicated, so a row linked twice through the pivot
734
+ appears once.
735
+
736
+ #### `BelongsToMany.attach(id, extra?)`
737
+
738
+ `attach(id: unknown, extra?: Row): Promise<void>`
739
+
740
+ Inserts one pivot row linking the parent to `id`, plus any `extra` pivot columns.
741
+
742
+ ```ts
743
+ await user.roles().attach(roleId);
744
+ await user.roles().attach(roleId, { assigned_at: now });
745
+ ```
746
+
747
+ **Notes:** no uniqueness check — attaching the same id twice inserts two pivot
748
+ rows unless the table constrains it.
749
+
750
+ #### `BelongsToMany.detach(id?)`
751
+
752
+ `detach(id?: unknown): Promise<void>`
753
+
754
+ Removes the pivot row for `id`, or **all** the parent's pivot rows when called
755
+ with no argument.
756
+
757
+ ```ts
758
+ await user.roles().detach(roleId); // one link
759
+ await user.roles().detach(); // every link for this user
760
+ ```
761
+
762
+ #### `BelongsToMany.sync(ids)`
763
+
764
+ `sync(ids: unknown[]): Promise<void>`
765
+
766
+ Makes the pivot contain exactly `ids` — detaches everything, then attaches each.
767
+
768
+ ```ts
769
+ await user.roles().sync([1, 2, 3]);
770
+ ```
771
+
772
+ **Notes:** not diff-based — it detaches all then re-attaches, so passing `[]`
773
+ clears every link. Runs one delete plus one insert per id (not a transaction).
774
+
775
+ ### Interfaces & types
776
+
777
+ #### `CastType`
778
+
779
+ ```ts
780
+ type CastType =
781
+ | "int" | "integer" | "float" | "number"
782
+ | "boolean" | "bool" | "string"
783
+ | "json" | "array" | "date";
784
+ ```
785
+
786
+ The supported cast kinds — the values in a `casts` map. Aliases pair up
787
+ (`int`/`integer`, `float`/`number`, `boolean`/`bool`, `json`/`array`).
788
+
789
+ ```ts
790
+ const kind: CastType = "boolean";
791
+ ```
792
+
793
+ #### `Casts`
794
+
795
+ `type Casts = Record<string, CastType>`
796
+
797
+ A column-to-cast-type map — the shape of `static casts`. Declare literal maps
798
+ `as const` so the string values don't widen past `CastType`.
799
+
800
+ ```ts
801
+ const casts: Casts = { published: "boolean", meta: "json" };
802
+ ```
803
+
804
+ ### Casting internals
805
+
806
+ `castGet`, `castSet`, and `applyCasts` (in `src/core/casts.ts`) are the functions
807
+ that power casting — `castGet` maps storage → JS, `castSet` maps JS → storage, and
808
+ `applyCasts` runs one of them over the keys named in a `Casts` map. They're
809
+ internal plumbing: the `Model` uses them for you and they aren't re-exported from
810
+ `@shaferllc/keel/core`, so declaring `static casts` is all you need.