@shaferllc/keel 0.80.0 → 0.81.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.
- package/dist/accounts/accounts.config.stub +50 -0
- package/dist/accounts/config.d.ts +46 -0
- package/dist/accounts/config.js +39 -0
- package/dist/accounts/flows.d.ts +50 -0
- package/dist/accounts/flows.js +133 -0
- package/dist/accounts/index.d.ts +28 -0
- package/dist/accounts/index.js +23 -0
- package/dist/accounts/migration.d.ts +14 -0
- package/dist/accounts/migration.js +39 -0
- package/dist/accounts/provider.d.ts +18 -0
- package/dist/accounts/provider.js +37 -0
- package/dist/accounts/routes.d.ts +15 -0
- package/dist/accounts/routes.js +116 -0
- package/dist/accounts/store.d.ts +33 -0
- package/dist/accounts/store.js +37 -0
- package/dist/accounts/tokens.d.ts +60 -0
- package/dist/accounts/tokens.js +116 -0
- package/dist/accounts/totp.d.ts +58 -0
- package/dist/accounts/totp.js +134 -0
- package/dist/accounts/two-factor.d.ts +56 -0
- package/dist/accounts/two-factor.js +146 -0
- package/dist/core/database.d.ts +36 -0
- package/dist/core/database.js +141 -4
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.js +3 -2
- package/dist/core/migrations.d.ts +52 -2
- package/dist/core/migrations.js +134 -3
- package/dist/core/model-events.d.ts +34 -0
- package/dist/core/model-events.js +89 -0
- package/dist/core/model-query.d.ts +68 -0
- package/dist/core/model-query.js +234 -0
- package/dist/core/model.d.ts +109 -4
- package/dist/core/model.js +263 -32
- package/dist/core/relations.d.ts +53 -0
- package/dist/core/relations.js +242 -0
- package/dist/teams/config.d.ts +27 -0
- package/dist/teams/config.js +23 -0
- package/dist/teams/context.d.ts +54 -0
- package/dist/teams/context.js +73 -0
- package/dist/teams/index.d.ts +25 -0
- package/dist/teams/index.js +20 -0
- package/dist/teams/invitations.d.ts +38 -0
- package/dist/teams/invitations.js +123 -0
- package/dist/teams/middleware.d.ts +30 -0
- package/dist/teams/middleware.js +92 -0
- package/dist/teams/migration.d.ts +9 -0
- package/dist/teams/migration.js +52 -0
- package/dist/teams/models.d.ts +54 -0
- package/dist/teams/models.js +85 -0
- package/dist/teams/provider.d.ts +17 -0
- package/dist/teams/provider.js +27 -0
- package/dist/teams/teams.config.stub +24 -0
- package/dist/teams/tenant.d.ts +25 -0
- package/dist/teams/tenant.js +45 -0
- package/docs/accounts.md +214 -0
- package/docs/ai-manifest.json +70 -1
- package/docs/database.md +80 -0
- package/docs/examples/accounts.ts +150 -0
- package/docs/examples/teams.ts +101 -0
- package/docs/migrations.md +86 -6
- package/docs/models.md +279 -6
- package/docs/teams.md +176 -0
- package/llms-full.txt +849 -12
- package/llms.txt +4 -0
- package/package.json +10 -2
package/dist/core/model.js
CHANGED
|
@@ -17,8 +17,41 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { db } from "./database.js";
|
|
19
19
|
import { NotFoundException } from "./exceptions.js";
|
|
20
|
-
import { BelongsTo, BelongsToMany, HasMany, HasOne } from "./relations.js";
|
|
20
|
+
import { BelongsTo, BelongsToMany, HasMany, HasOne, MorphMany, MorphOne, MorphTo } from "./relations.js";
|
|
21
21
|
import { applyCasts, castGet, castSet } from "./casts.js";
|
|
22
|
+
import { addModelHook, addModelObserver, fireModelEvent, } from "./model-events.js";
|
|
23
|
+
import { ModelQuery } from "./model-query.js";
|
|
24
|
+
/** Registered global scopes, keyed by the model class they were declared on. */
|
|
25
|
+
const globalScopes = new WeakMap();
|
|
26
|
+
/**
|
|
27
|
+
* Every scope that applies to `cls`, including ones declared on its ancestors.
|
|
28
|
+
*
|
|
29
|
+
* Inheritance is the whole point: a base class exists so its subclasses are
|
|
30
|
+
* constrained by it.
|
|
31
|
+
*
|
|
32
|
+
* class TenantModel extends Model {}
|
|
33
|
+
* TenantModel.addGlobalScope("tenant", (q) => q.where("teamId", currentTeam()));
|
|
34
|
+
* class Post extends TenantModel {} // Post must be scoped too
|
|
35
|
+
*
|
|
36
|
+
* Looking only at the concrete class would leave `Post.query()` completely
|
|
37
|
+
* unconstrained — and a scope that silently does nothing fails *open*, which for
|
|
38
|
+
* a tenancy scope means returning every customer's rows.
|
|
39
|
+
*
|
|
40
|
+
* Walked root-first so a subclass can override an ancestor's scope by reusing its
|
|
41
|
+
* name — the nearest declaration of a given name wins.
|
|
42
|
+
*/
|
|
43
|
+
function scopesFor(cls) {
|
|
44
|
+
const chain = [];
|
|
45
|
+
for (let c = cls; c && c !== Function.prototype; c = Object.getPrototypeOf(c)) {
|
|
46
|
+
chain.unshift(c);
|
|
47
|
+
}
|
|
48
|
+
const merged = new Map();
|
|
49
|
+
for (const link of chain) {
|
|
50
|
+
for (const [name, scope] of globalScopes.get(link) ?? [])
|
|
51
|
+
merged.set(name, scope);
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
22
55
|
/**
|
|
23
56
|
* Loaded relations live off the model itself so they never leak into `save()`
|
|
24
57
|
* (which spreads own columns) — they're keyed here by the owning instance.
|
|
@@ -46,14 +79,137 @@ export class Model {
|
|
|
46
79
|
static timestamps = false;
|
|
47
80
|
static createdAtColumn = "created_at";
|
|
48
81
|
static updatedAtColumn = "updated_at";
|
|
82
|
+
/** Columns stripped from `toJSON()` output (e.g. `["password"]`). */
|
|
83
|
+
static hidden = [];
|
|
84
|
+
/** If set, ONLY these keys survive `toJSON()` — an allowlist that wins over `hidden`. */
|
|
85
|
+
static visible = [];
|
|
86
|
+
/** Computed accessor names (getters or zero-arg methods) added to `toJSON()`. */
|
|
87
|
+
static appends = [];
|
|
88
|
+
/** Soft deletes: `delete()` sets `deleted_at` instead of removing the row. */
|
|
89
|
+
static softDeletes = false;
|
|
90
|
+
static deletedAtColumn = "deleted_at";
|
|
49
91
|
constructor(attributes = {}) {
|
|
50
92
|
// Hydration is unguarded (rows come from the database) but always cast.
|
|
51
93
|
Object.assign(this, applyCasts(attributes, this.constructor.casts, castGet));
|
|
52
94
|
}
|
|
53
95
|
/* ------------------------------ static -------------------------------- */
|
|
54
|
-
|
|
96
|
+
/* ---------------------------- scopes & events ------------------------- */
|
|
97
|
+
/** Register a global scope — a constraint applied to every query this model builds. */
|
|
98
|
+
static addGlobalScope(name, scope) {
|
|
99
|
+
let map = globalScopes.get(this);
|
|
100
|
+
if (!map)
|
|
101
|
+
globalScopes.set(this, (map = new Map()));
|
|
102
|
+
map.set(name, scope);
|
|
103
|
+
}
|
|
104
|
+
/** Build this model's base query, applying global scopes and the soft-delete filter. */
|
|
105
|
+
static baseQuery(trashed = "exclude", skip) {
|
|
106
|
+
const query = db(this.table, this.connection);
|
|
107
|
+
for (const [name, scope] of scopesFor(this)) {
|
|
108
|
+
if (skip?.has(name))
|
|
109
|
+
continue;
|
|
110
|
+
scope(query, this);
|
|
111
|
+
}
|
|
112
|
+
if (this.softDeletes) {
|
|
113
|
+
if (trashed === "exclude")
|
|
114
|
+
query.whereNull(this.deletedAtColumn);
|
|
115
|
+
else if (trashed === "only")
|
|
116
|
+
query.whereNotNull(this.deletedAtColumn);
|
|
117
|
+
}
|
|
118
|
+
return query;
|
|
119
|
+
}
|
|
120
|
+
/** A query builder scoped to this model's table (global scopes applied). */
|
|
55
121
|
static query() {
|
|
56
|
-
return
|
|
122
|
+
return this.baseQuery();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Drop named global scopes from this query.
|
|
126
|
+
*
|
|
127
|
+
* Deliberately explicit and greppable: a query that escapes a tenancy scope is
|
|
128
|
+
* exactly the thing you want to be able to find at audit time, so it has to be
|
|
129
|
+
* *typed out*, not arrived at by forgetting something.
|
|
130
|
+
*/
|
|
131
|
+
static withoutGlobalScope(...names) {
|
|
132
|
+
return this.baseQuery("exclude", new Set(names));
|
|
133
|
+
}
|
|
134
|
+
/** Drop every global scope. Same warning, louder. */
|
|
135
|
+
static withoutGlobalScopes() {
|
|
136
|
+
return this.baseQuery("exclude", new Set(scopesFor(this).keys()));
|
|
137
|
+
}
|
|
138
|
+
/** Include soft-deleted rows (bypasses the soft-delete scope). */
|
|
139
|
+
static withTrashed() {
|
|
140
|
+
return this.baseQuery("with");
|
|
141
|
+
}
|
|
142
|
+
/** Only soft-deleted rows. */
|
|
143
|
+
static onlyTrashed() {
|
|
144
|
+
return this.baseQuery("only");
|
|
145
|
+
}
|
|
146
|
+
/* --------------------- model-aware query (eager, has) ----------------- */
|
|
147
|
+
/** A model-aware query — hydrates to models and supports `with`/`withCount`/`whereHas`. */
|
|
148
|
+
static newQuery() {
|
|
149
|
+
return new ModelQuery(this, this.baseQuery());
|
|
150
|
+
}
|
|
151
|
+
/** Start a query eager-loading the given relations (dotted paths nest). */
|
|
152
|
+
static with(...names) {
|
|
153
|
+
return this.newQuery().with(...names);
|
|
154
|
+
}
|
|
155
|
+
/** Start a query counting the given relations into `<relation>_count`. */
|
|
156
|
+
static withCount(...names) {
|
|
157
|
+
return this.newQuery().withCount(...names);
|
|
158
|
+
}
|
|
159
|
+
/** Start a query constrained to models that have at least one related row. */
|
|
160
|
+
static has(name) {
|
|
161
|
+
return this.newQuery().has(name);
|
|
162
|
+
}
|
|
163
|
+
/** Start a query constrained to models whose related rows match `constrain`. */
|
|
164
|
+
static whereHas(name, constrain) {
|
|
165
|
+
return this.newQuery().whereHas(name, constrain);
|
|
166
|
+
}
|
|
167
|
+
/** Start a query constrained to models with no matching related row. */
|
|
168
|
+
static doesntHave(name, constrain) {
|
|
169
|
+
return this.newQuery().doesntHave(name, constrain);
|
|
170
|
+
}
|
|
171
|
+
/** Construct a model from a row and fire its `retrieved` event. */
|
|
172
|
+
static async hydrate(row) {
|
|
173
|
+
const model = new this(row);
|
|
174
|
+
await fireModelEvent(this, "retrieved", model);
|
|
175
|
+
return model;
|
|
176
|
+
}
|
|
177
|
+
static creating(hook) {
|
|
178
|
+
addModelHook(this, "creating", hook);
|
|
179
|
+
}
|
|
180
|
+
static created(hook) {
|
|
181
|
+
addModelHook(this, "created", hook);
|
|
182
|
+
}
|
|
183
|
+
static updating(hook) {
|
|
184
|
+
addModelHook(this, "updating", hook);
|
|
185
|
+
}
|
|
186
|
+
static updated(hook) {
|
|
187
|
+
addModelHook(this, "updated", hook);
|
|
188
|
+
}
|
|
189
|
+
static saving(hook) {
|
|
190
|
+
addModelHook(this, "saving", hook);
|
|
191
|
+
}
|
|
192
|
+
static saved(hook) {
|
|
193
|
+
addModelHook(this, "saved", hook);
|
|
194
|
+
}
|
|
195
|
+
static deleting(hook) {
|
|
196
|
+
addModelHook(this, "deleting", hook);
|
|
197
|
+
}
|
|
198
|
+
static deleted(hook) {
|
|
199
|
+
addModelHook(this, "deleted", hook);
|
|
200
|
+
}
|
|
201
|
+
static restoring(hook) {
|
|
202
|
+
addModelHook(this, "restoring", hook);
|
|
203
|
+
}
|
|
204
|
+
static restored(hook) {
|
|
205
|
+
addModelHook(this, "restored", hook);
|
|
206
|
+
}
|
|
207
|
+
static retrieved(hook) {
|
|
208
|
+
addModelHook(this, "retrieved", hook);
|
|
209
|
+
}
|
|
210
|
+
/** Register an observer whose methods are named after the events they handle. */
|
|
211
|
+
static observe(observer) {
|
|
212
|
+
addModelObserver(this, observer);
|
|
57
213
|
}
|
|
58
214
|
/** Keep only the attributes mass-assignment allows (fillable / guarded). */
|
|
59
215
|
static filterFillable(attributes) {
|
|
@@ -88,12 +244,12 @@ export class Model {
|
|
|
88
244
|
return out;
|
|
89
245
|
}
|
|
90
246
|
static async all() {
|
|
91
|
-
const rows = await
|
|
92
|
-
return rows.map((row) =>
|
|
247
|
+
const rows = await this.baseQuery().get();
|
|
248
|
+
return Promise.all(rows.map((row) => this.hydrate(row)));
|
|
93
249
|
}
|
|
94
250
|
static async find(id) {
|
|
95
|
-
const row = await
|
|
96
|
-
return row ?
|
|
251
|
+
const row = await this.baseQuery().where(this.primaryKey, id).first();
|
|
252
|
+
return row ? this.hydrate(row) : null;
|
|
97
253
|
}
|
|
98
254
|
static async findOrFail(id) {
|
|
99
255
|
const model = await this.find(id);
|
|
@@ -102,54 +258,49 @@ export class Model {
|
|
|
102
258
|
return model;
|
|
103
259
|
}
|
|
104
260
|
static async first() {
|
|
105
|
-
const row = await
|
|
106
|
-
return row ?
|
|
261
|
+
const row = await this.baseQuery().first();
|
|
262
|
+
return row ? this.hydrate(row) : null;
|
|
107
263
|
}
|
|
108
264
|
/** Fetch models matching a simple equality condition. */
|
|
109
265
|
static async where(column, value) {
|
|
110
|
-
const rows = await
|
|
111
|
-
return rows.map((row) =>
|
|
266
|
+
const rows = await this.baseQuery().where(column, value).get();
|
|
267
|
+
return Promise.all(rows.map((row) => this.hydrate(row)));
|
|
112
268
|
}
|
|
113
269
|
static async create(attributes) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
model[this.createdAtColumn] = write[this.createdAtColumn];
|
|
120
|
-
model[this.updatedAtColumn] = write[this.updatedAtColumn];
|
|
121
|
-
}
|
|
122
|
-
if (id != null)
|
|
123
|
-
model[this.primaryKey] = id;
|
|
270
|
+
// Route through save() so mass-assignment, timestamps, and the saving/creating
|
|
271
|
+
// lifecycle events all apply in one place.
|
|
272
|
+
const model = new this();
|
|
273
|
+
model.fill(attributes);
|
|
274
|
+
await model.save();
|
|
124
275
|
return model;
|
|
125
276
|
}
|
|
126
277
|
/** A page of models plus pagination metadata. */
|
|
127
278
|
static async paginate(page = 1, perPage = 15) {
|
|
128
|
-
const result = await
|
|
129
|
-
return { ...result, data: result.data.map((row) =>
|
|
279
|
+
const result = await this.baseQuery().paginate(page, perPage);
|
|
280
|
+
return { ...result, data: await Promise.all(result.data.map((row) => this.hydrate(row))) };
|
|
130
281
|
}
|
|
131
282
|
/** Find the first row matching `match`, or create one from `{ ...match, ...values }`. */
|
|
132
283
|
static async firstOrCreate(match, values = {}) {
|
|
133
284
|
const row = await this.matching(match).first();
|
|
134
285
|
if (row)
|
|
135
|
-
return
|
|
286
|
+
return this.hydrate(row);
|
|
136
287
|
return this.create({ ...match, ...values });
|
|
137
288
|
}
|
|
138
289
|
/** Update the first row matching `match` with `values`, or create it. */
|
|
139
290
|
static async updateOrCreate(match, values = {}) {
|
|
140
291
|
const row = await this.matching(match).first();
|
|
141
292
|
if (row) {
|
|
142
|
-
const model =
|
|
293
|
+
const model = await this.hydrate(row);
|
|
143
294
|
await model.forceFill(values).save();
|
|
144
295
|
return model;
|
|
145
296
|
}
|
|
146
297
|
return this.create({ ...match, ...values });
|
|
147
298
|
}
|
|
148
|
-
/** A query scoped to every column/value in `match
|
|
299
|
+
/** A query scoped to every column/value in `match` (global scopes applied). */
|
|
149
300
|
static matching(match) {
|
|
150
|
-
|
|
301
|
+
const q = this.baseQuery();
|
|
151
302
|
for (const [column, value] of Object.entries(match))
|
|
152
|
-
q
|
|
303
|
+
q.where(column, value);
|
|
153
304
|
return q;
|
|
154
305
|
}
|
|
155
306
|
/**
|
|
@@ -197,6 +348,18 @@ export class Model {
|
|
|
197
348
|
.join("_"), foreignPivotKey = `${this.constructor.name.toLowerCase()}_${this.ctor().primaryKey}`, relatedPivotKey = `${related.name.toLowerCase()}_${related.primaryKey}`, parentKey = this.ctor().primaryKey, relatedKey = related.primaryKey) {
|
|
198
349
|
return new BelongsToMany(this, related, pivotTable, foreignPivotKey, relatedPivotKey, parentKey, relatedKey);
|
|
199
350
|
}
|
|
351
|
+
/** Polymorphic one-to-many: children carry `<name>_id` + `<name>_type`. */
|
|
352
|
+
morphMany(related, name, localKey = this.ctor().primaryKey) {
|
|
353
|
+
return new MorphMany(this, related, this.constructor.name, `${name}_id`, `${name}_type`, localKey);
|
|
354
|
+
}
|
|
355
|
+
/** Polymorphic one-to-one. */
|
|
356
|
+
morphOne(related, name, localKey = this.ctor().primaryKey) {
|
|
357
|
+
return new MorphOne(this, related, this.constructor.name, `${name}_id`, `${name}_type`, localKey);
|
|
358
|
+
}
|
|
359
|
+
/** The owning side of a polymorphic relation — resolves via `<name>_type` (see `registerMorphType`). */
|
|
360
|
+
morphTo(name, idColumn = `${name}_id`, typeColumn = `${name}_type`) {
|
|
361
|
+
return new MorphTo(this, idColumn, typeColumn);
|
|
362
|
+
}
|
|
200
363
|
/** Store an eager-loaded relation result under `name`. */
|
|
201
364
|
setRelation(name, value) {
|
|
202
365
|
const bag = relationStore.get(this) ?? {};
|
|
@@ -208,12 +371,18 @@ export class Model {
|
|
|
208
371
|
getRelation(name) {
|
|
209
372
|
return relationStore.get(this)?.[name];
|
|
210
373
|
}
|
|
211
|
-
/** Insert (no primary key) or update (has one). */
|
|
374
|
+
/** Insert (no primary key) or update (has one). Fires save/create/update events. */
|
|
212
375
|
async save() {
|
|
213
376
|
const ctor = this.ctor();
|
|
377
|
+
const cls = this.constructor;
|
|
214
378
|
const { table, primaryKey, connection } = ctor;
|
|
215
379
|
const idValue = this[primaryKey];
|
|
216
380
|
const forInsert = idValue == null;
|
|
381
|
+
// `*ing` hooks run before the write and may mutate the model or veto it.
|
|
382
|
+
if (!(await fireModelEvent(cls, "saving", this)))
|
|
383
|
+
return this;
|
|
384
|
+
if (!(await fireModelEvent(cls, forInsert ? "creating" : "updating", this)))
|
|
385
|
+
return this;
|
|
217
386
|
const data = ctor.stampTimestamps(ctor.toDatabase({ ...this }), forInsert);
|
|
218
387
|
// Reflect the stamps back onto the instance.
|
|
219
388
|
if (ctor.timestamps) {
|
|
@@ -230,6 +399,8 @@ export class Model {
|
|
|
230
399
|
if (id != null)
|
|
231
400
|
this[primaryKey] = id;
|
|
232
401
|
}
|
|
402
|
+
await fireModelEvent(cls, forInsert ? "created" : "updated", this);
|
|
403
|
+
await fireModelEvent(cls, "saved", this);
|
|
233
404
|
return this;
|
|
234
405
|
}
|
|
235
406
|
/** Mass-assign then save — `fill` + `save` in one call. */
|
|
@@ -244,9 +415,51 @@ export class Model {
|
|
|
244
415
|
Object.assign(this, applyCasts(row, ctor.casts, castGet));
|
|
245
416
|
return this;
|
|
246
417
|
}
|
|
418
|
+
/** Delete the row — or, with soft deletes on, set `deleted_at`. Fires delete events. */
|
|
247
419
|
async delete() {
|
|
248
|
-
const
|
|
249
|
-
|
|
420
|
+
const ctor = this.ctor();
|
|
421
|
+
const cls = this.constructor;
|
|
422
|
+
const { table, primaryKey, connection } = ctor;
|
|
423
|
+
if (!(await fireModelEvent(cls, "deleting", this)))
|
|
424
|
+
return;
|
|
425
|
+
if (ctor.softDeletes) {
|
|
426
|
+
const now = new Date().toISOString();
|
|
427
|
+
await db(table, connection)
|
|
428
|
+
.where(primaryKey, this[primaryKey])
|
|
429
|
+
.update({ [ctor.deletedAtColumn]: now });
|
|
430
|
+
this[ctor.deletedAtColumn] = now;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
await db(table, connection).where(primaryKey, this[primaryKey]).delete();
|
|
434
|
+
}
|
|
435
|
+
await fireModelEvent(cls, "deleted", this);
|
|
436
|
+
}
|
|
437
|
+
/** Permanently delete a soft-deletable row (bypasses soft deletes). */
|
|
438
|
+
async forceDelete() {
|
|
439
|
+
const ctor = this.ctor();
|
|
440
|
+
const cls = this.constructor;
|
|
441
|
+
if (!(await fireModelEvent(cls, "deleting", this)))
|
|
442
|
+
return;
|
|
443
|
+
await db(ctor.table, ctor.connection).where(ctor.primaryKey, this[ctor.primaryKey]).delete();
|
|
444
|
+
await fireModelEvent(cls, "deleted", this);
|
|
445
|
+
}
|
|
446
|
+
/** Restore a soft-deleted row (clears `deleted_at`). Fires restore events. */
|
|
447
|
+
async restore() {
|
|
448
|
+
const ctor = this.ctor();
|
|
449
|
+
const cls = this.constructor;
|
|
450
|
+
if (!(await fireModelEvent(cls, "restoring", this)))
|
|
451
|
+
return this;
|
|
452
|
+
await db(ctor.table, ctor.connection)
|
|
453
|
+
.where(ctor.primaryKey, this[ctor.primaryKey])
|
|
454
|
+
.update({ [ctor.deletedAtColumn]: null });
|
|
455
|
+
this[ctor.deletedAtColumn] = null;
|
|
456
|
+
await fireModelEvent(cls, "restored", this);
|
|
457
|
+
return this;
|
|
458
|
+
}
|
|
459
|
+
/** Whether this soft-deletable model is currently trashed. */
|
|
460
|
+
trashed() {
|
|
461
|
+
const ctor = this.ctor();
|
|
462
|
+
return ctor.softDeletes && this[ctor.deletedAtColumn] != null;
|
|
250
463
|
}
|
|
251
464
|
/** Merge mass-assignable attributes into the model (cast, not saved). */
|
|
252
465
|
fill(attributes) {
|
|
@@ -261,7 +474,25 @@ export class Model {
|
|
|
261
474
|
return this;
|
|
262
475
|
}
|
|
263
476
|
toJSON() {
|
|
264
|
-
const
|
|
477
|
+
const ctor = this.ctor();
|
|
478
|
+
let data = applyCasts({ ...this }, ctor.casts, castGet);
|
|
479
|
+
// Appended computed attributes: a getter (value) or a zero-arg method.
|
|
480
|
+
for (const name of ctor.appends) {
|
|
481
|
+
const value = this[name];
|
|
482
|
+
data[name] = typeof value === "function" ? value.call(this) : value;
|
|
483
|
+
}
|
|
484
|
+
// `visible` is an allowlist and wins; otherwise `hidden` is a denylist.
|
|
485
|
+
if (ctor.visible.length) {
|
|
486
|
+
const kept = {};
|
|
487
|
+
for (const key of ctor.visible)
|
|
488
|
+
if (key in data)
|
|
489
|
+
kept[key] = data[key];
|
|
490
|
+
data = kept;
|
|
491
|
+
}
|
|
492
|
+
else if (ctor.hidden.length) {
|
|
493
|
+
for (const key of ctor.hidden)
|
|
494
|
+
delete data[key];
|
|
495
|
+
}
|
|
265
496
|
const relations = relationStore.get(this);
|
|
266
497
|
if (relations) {
|
|
267
498
|
for (const [name, value] of Object.entries(relations))
|
package/dist/core/relations.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export declare class HasMany<T extends Model> extends Relation<T, T[]> {
|
|
|
49
49
|
query(): QueryBuilder;
|
|
50
50
|
get(): Promise<T[]>;
|
|
51
51
|
eager(models: Model[], name: string): Promise<void>;
|
|
52
|
+
parentColumn(): string;
|
|
53
|
+
matchingParentKeys(constrain?: (q: QueryBuilder) => void): Promise<unknown[]>;
|
|
54
|
+
countsByParent(parentKeys: unknown[]): Promise<Map<unknown, number>>;
|
|
52
55
|
}
|
|
53
56
|
export declare class HasOne<T extends Model> extends Relation<T, T | null> {
|
|
54
57
|
private foreignKey;
|
|
@@ -58,6 +61,9 @@ export declare class HasOne<T extends Model> extends Relation<T, T | null> {
|
|
|
58
61
|
query(): QueryBuilder;
|
|
59
62
|
get(): Promise<T | null>;
|
|
60
63
|
eager(models: Model[], name: string): Promise<void>;
|
|
64
|
+
parentColumn(): string;
|
|
65
|
+
matchingParentKeys(constrain?: (q: QueryBuilder) => void): Promise<unknown[]>;
|
|
66
|
+
countsByParent(parentKeys: unknown[]): Promise<Map<unknown, number>>;
|
|
61
67
|
}
|
|
62
68
|
export declare class BelongsTo<T extends Model> extends Relation<T, T | null> {
|
|
63
69
|
private foreignKey;
|
|
@@ -67,6 +73,9 @@ export declare class BelongsTo<T extends Model> extends Relation<T, T | null> {
|
|
|
67
73
|
query(): QueryBuilder;
|
|
68
74
|
get(): Promise<T | null>;
|
|
69
75
|
eager(models: Model[], name: string): Promise<void>;
|
|
76
|
+
parentColumn(): string;
|
|
77
|
+
matchingParentKeys(constrain?: (q: QueryBuilder) => void): Promise<unknown[]>;
|
|
78
|
+
countsByParent(parentKeys: unknown[]): Promise<Map<unknown, number>>;
|
|
70
79
|
}
|
|
71
80
|
export declare class BelongsToMany<T extends Model> extends Relation<T, T[]> {
|
|
72
81
|
private pivotTable;
|
|
@@ -86,5 +95,49 @@ export declare class BelongsToMany<T extends Model> extends Relation<T, T[]> {
|
|
|
86
95
|
detach(id?: unknown): Promise<void>;
|
|
87
96
|
/** Make the pivot contain exactly `ids` (detach the rest, attach the new). */
|
|
88
97
|
sync(ids: unknown[]): Promise<void>;
|
|
98
|
+
parentColumn(): string;
|
|
99
|
+
matchingParentKeys(constrain?: (q: QueryBuilder) => void): Promise<unknown[]>;
|
|
100
|
+
countsByParent(parentKeys: unknown[]): Promise<Map<unknown, number>>;
|
|
101
|
+
}
|
|
102
|
+
/** Register a model under a morph type string (usually its class name). */
|
|
103
|
+
export declare function registerMorphType(type: string, related: ModelClass<Model>): void;
|
|
104
|
+
/** The parent side of a polymorphic one-to-many (`Post.comments()` over `commentable`). */
|
|
105
|
+
export declare class MorphMany<T extends Model> extends Relation<T, T[]> {
|
|
106
|
+
private morphType;
|
|
107
|
+
private idColumn;
|
|
108
|
+
private typeColumn;
|
|
109
|
+
private localKey;
|
|
110
|
+
constructor(parent: Model, related: ModelClass<T>, morphType: string, idColumn: string, typeColumn: string, localKey: string);
|
|
111
|
+
private localValue;
|
|
112
|
+
query(): QueryBuilder;
|
|
113
|
+
get(): Promise<T[]>;
|
|
114
|
+
eager(models: Model[], name: string): Promise<void>;
|
|
115
|
+
parentColumn(): string;
|
|
116
|
+
matchingParentKeys(constrain?: (q: QueryBuilder) => void): Promise<unknown[]>;
|
|
117
|
+
countsByParent(parentKeys: unknown[]): Promise<Map<unknown, number>>;
|
|
118
|
+
/** Create a related row with the morph keys (`*_id` / `*_type`) filled in. */
|
|
119
|
+
create(attributes: Row): Promise<T>;
|
|
120
|
+
}
|
|
121
|
+
/** The parent side of a polymorphic one-to-one. */
|
|
122
|
+
export declare class MorphOne<T extends Model> extends Relation<T, T | null> {
|
|
123
|
+
private morphType;
|
|
124
|
+
private idColumn;
|
|
125
|
+
private typeColumn;
|
|
126
|
+
private localKey;
|
|
127
|
+
constructor(parent: Model, related: ModelClass<T>, morphType: string, idColumn: string, typeColumn: string, localKey: string);
|
|
128
|
+
query(): QueryBuilder;
|
|
129
|
+
get(): Promise<T | null>;
|
|
130
|
+
eager(models: Model[], name: string): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
/** The owning side of a polymorphic relation — resolves its parent by stored type + id. */
|
|
133
|
+
export declare class MorphTo implements PromiseLike<Model | null> {
|
|
134
|
+
private parent;
|
|
135
|
+
private idColumn;
|
|
136
|
+
private typeColumn;
|
|
137
|
+
constructor(parent: Model, idColumn: string, typeColumn: string);
|
|
138
|
+
private relatedClass;
|
|
139
|
+
get(): Promise<Model | null>;
|
|
140
|
+
eager(models: Model[], name: string): Promise<void>;
|
|
141
|
+
then<R1 = Model | null, R2 = never>(onFulfilled?: ((value: Model | null) => R1 | PromiseLike<R1>) | null, onRejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): PromiseLike<R1 | R2>;
|
|
89
142
|
}
|
|
90
143
|
export {};
|