@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.
Files changed (65) hide show
  1. package/dist/accounts/accounts.config.stub +50 -0
  2. package/dist/accounts/config.d.ts +46 -0
  3. package/dist/accounts/config.js +39 -0
  4. package/dist/accounts/flows.d.ts +50 -0
  5. package/dist/accounts/flows.js +133 -0
  6. package/dist/accounts/index.d.ts +28 -0
  7. package/dist/accounts/index.js +23 -0
  8. package/dist/accounts/migration.d.ts +14 -0
  9. package/dist/accounts/migration.js +39 -0
  10. package/dist/accounts/provider.d.ts +18 -0
  11. package/dist/accounts/provider.js +37 -0
  12. package/dist/accounts/routes.d.ts +15 -0
  13. package/dist/accounts/routes.js +116 -0
  14. package/dist/accounts/store.d.ts +33 -0
  15. package/dist/accounts/store.js +37 -0
  16. package/dist/accounts/tokens.d.ts +60 -0
  17. package/dist/accounts/tokens.js +116 -0
  18. package/dist/accounts/totp.d.ts +58 -0
  19. package/dist/accounts/totp.js +134 -0
  20. package/dist/accounts/two-factor.d.ts +56 -0
  21. package/dist/accounts/two-factor.js +146 -0
  22. package/dist/core/database.d.ts +36 -0
  23. package/dist/core/database.js +141 -4
  24. package/dist/core/index.d.ts +5 -2
  25. package/dist/core/index.js +3 -2
  26. package/dist/core/migrations.d.ts +52 -2
  27. package/dist/core/migrations.js +134 -3
  28. package/dist/core/model-events.d.ts +34 -0
  29. package/dist/core/model-events.js +89 -0
  30. package/dist/core/model-query.d.ts +68 -0
  31. package/dist/core/model-query.js +234 -0
  32. package/dist/core/model.d.ts +109 -4
  33. package/dist/core/model.js +263 -32
  34. package/dist/core/relations.d.ts +53 -0
  35. package/dist/core/relations.js +242 -0
  36. package/dist/teams/config.d.ts +27 -0
  37. package/dist/teams/config.js +23 -0
  38. package/dist/teams/context.d.ts +54 -0
  39. package/dist/teams/context.js +73 -0
  40. package/dist/teams/index.d.ts +25 -0
  41. package/dist/teams/index.js +20 -0
  42. package/dist/teams/invitations.d.ts +38 -0
  43. package/dist/teams/invitations.js +123 -0
  44. package/dist/teams/middleware.d.ts +30 -0
  45. package/dist/teams/middleware.js +92 -0
  46. package/dist/teams/migration.d.ts +9 -0
  47. package/dist/teams/migration.js +52 -0
  48. package/dist/teams/models.d.ts +54 -0
  49. package/dist/teams/models.js +85 -0
  50. package/dist/teams/provider.d.ts +17 -0
  51. package/dist/teams/provider.js +27 -0
  52. package/dist/teams/teams.config.stub +24 -0
  53. package/dist/teams/tenant.d.ts +25 -0
  54. package/dist/teams/tenant.js +45 -0
  55. package/docs/accounts.md +214 -0
  56. package/docs/ai-manifest.json +70 -1
  57. package/docs/database.md +80 -0
  58. package/docs/examples/accounts.ts +150 -0
  59. package/docs/examples/teams.ts +101 -0
  60. package/docs/migrations.md +86 -6
  61. package/docs/models.md +279 -6
  62. package/docs/teams.md +176 -0
  63. package/llms-full.txt +849 -12
  64. package/llms.txt +4 -0
  65. package/package.json +10 -2
@@ -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
- /** A raw query builder scoped to this model's table. */
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 db(this.table, this.connection);
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 db(this.table, this.connection).get();
92
- return rows.map((row) => new this(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 db(this.table, this.connection).where(this.primaryKey, id).first();
96
- return row ? new this(row) : null;
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 db(this.table, this.connection).first();
106
- return row ? new this(row) : null;
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 db(this.table, this.connection).where(column, value).get();
111
- return rows.map((row) => new this(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
- const filtered = this.filterFillable(attributes);
115
- const write = this.stampTimestamps(this.toDatabase(filtered), true);
116
- const id = await db(this.table, this.connection).insertGetId(write);
117
- const model = new this(filtered);
118
- if (this.timestamps) {
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 db(this.table, this.connection).paginate(page, perPage);
129
- return { ...result, data: result.data.map((row) => new this(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 new this(row);
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 = new this(row);
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
- let q = db(this.table, this.connection);
301
+ const q = this.baseQuery();
151
302
  for (const [column, value] of Object.entries(match))
152
- q = q.where(column, value);
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 { table, primaryKey, connection } = this.ctor();
249
- await db(table, connection).where(primaryKey, this[primaryKey]).delete();
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 data = applyCasts({ ...this }, this.ctor().casts, castGet);
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))
@@ -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 {};