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