@zerotal/orm 1.3.0 → 1.5.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.
@@ -36,9 +36,17 @@ import { installReactiveAccessors, type ColumnOptions } from "./decorators/colum
36
36
  import { _compose } from "./mixins.ts";
37
37
  import type { Compose } from "./mixins.ts";
38
38
  import { columnsFor, relationsFor } from "./decorators/_metadata.ts";
39
+ import {
40
+ collectEncryptable,
41
+ decryptColumn,
42
+ encryptColumn,
43
+ isEncryptedCast,
44
+ type EncryptedCastName,
45
+ } from "../casts/encrypted.ts";
39
46
  import { TransactionContext } from "../db/TransactionContext.ts";
40
47
  import type { InsertPayload, UpdatePayload, FillablePayload } from "./payload.ts";
41
48
  import type { WhereOperator, OrderDirection } from "../db/types.ts";
49
+ import type { ClassRef } from "../support/classRef.ts";
42
50
 
43
51
  let _dialect: "sqlite" | "postgres" | "mysql" = "sqlite";
44
52
 
@@ -222,18 +230,26 @@ type StringCast =
222
230
  | "float"
223
231
  | "enum"
224
232
  | "immutable_datetime"
233
+ | EncryptedCastName
225
234
  | `decimal:${number}`;
226
235
  type CastOption = ColumnOptions["cast"];
227
236
 
228
- function getCasts(ctor: Function): Record<string, CastOption> {
237
+ function getCasts(ctor: ClassRef): Record<string, CastOption> {
229
238
  const merged: Record<string, CastOption> = {};
230
- const chain: Function[] = [];
231
- let current: Function | null = ctor;
239
+ const chain: ClassRef[] = [];
240
+ let current: ClassRef | null = ctor;
232
241
  while (current && current !== Function.prototype) {
233
242
  chain.push(current);
234
- current = Object.getPrototypeOf(current) as Function | null;
243
+ current = Object.getPrototypeOf(current) as ClassRef | null;
235
244
  }
236
245
  chain.reverse();
246
+ // `static encryptable` first, so an explicit cast on the same column still wins —
247
+ // spelling one out is the more specific statement of intent.
248
+ const colReg = columnsFor(ctor);
249
+ Object.assign(
250
+ merged,
251
+ collectEncryptable(chain, (key) => colReg?.get(key)?.type),
252
+ );
237
253
  for (const entry of chain) {
238
254
  const casts = (entry as { casts?: Record<string, CastOption> }).casts;
239
255
  if (casts) Object.assign(merged, casts);
@@ -241,8 +257,9 @@ function getCasts(ctor: Function): Record<string, CastOption> {
241
257
  return merged;
242
258
  }
243
259
 
244
- function applyCastGet(value: unknown, cast: StringCast): unknown {
260
+ function applyCastGet(value: unknown, cast: StringCast, label: string): unknown {
245
261
  if (value === null || value === undefined) return value;
262
+ if (isEncryptedCast(cast)) return decryptColumn(value, cast, label);
246
263
  const cstr = cast as unknown as string;
247
264
  if (cstr.startsWith("decimal:")) {
248
265
  const n = parseInt(cstr.slice(8), 10) || 0;
@@ -294,8 +311,9 @@ function tryParseJson(s: string): unknown {
294
311
  }
295
312
  }
296
313
 
297
- function applyCastSet(value: unknown, cast: StringCast): unknown {
314
+ function applyCastSet(value: unknown, cast: StringCast, label: string): unknown {
298
315
  if (value === null || value === undefined) return value;
316
+ if (isEncryptedCast(cast)) return encryptColumn(value, cast, label);
299
317
  const cstr = cast as unknown as string;
300
318
  if (cstr.startsWith("decimal:")) {
301
319
  const n = parseInt(cstr.slice(8), 10) || 0;
@@ -362,6 +380,7 @@ function _serializeForWrite(
362
380
  val: unknown,
363
381
  casts: Record<string, CastOption>,
364
382
  colReg: Map<string, ColumnOptions> | null,
383
+ model?: string,
365
384
  ): unknown {
366
385
  const colMeta = colReg?.get(key);
367
386
  const castOpt = casts[key] ?? colMeta?.cast;
@@ -370,7 +389,7 @@ function _serializeForWrite(
370
389
  if (castOpt && typeof castOpt === "object" && castOpt.set) {
371
390
  serializedVal = castOpt.set(val);
372
391
  } else if (typeof castOpt === "string") {
373
- serializedVal = applyCastSet(val, castOpt);
392
+ serializedVal = applyCastSet(val, castOpt, model ? `${model}.${key}` : key);
374
393
  } else if (colType === "boolean" && val !== null && val !== undefined) {
375
394
  serializedVal = val ? 1 : 0;
376
395
  } else if (colType === "json" && val !== null) {
@@ -408,7 +427,7 @@ type ModelCtor<T extends BaseModel> = typeof BaseModel & { new (): T };
408
427
  * (`Roles(Permissions(Base))`), where each relation lives on a different
409
428
  * class in the chain.
410
429
  */
411
- function relNames(ctor: Function): Set<string> {
430
+ function relNames(ctor: ClassRef): Set<string> {
412
431
  return new Set(relationsFor(ctor).keys());
413
432
  }
414
433
 
@@ -445,7 +464,7 @@ function* ownDataEntries(
445
464
  * (rare edge case: a model with zero @column decorators) so callers can fall back
446
465
  * to the old unrestricted behaviour.
447
466
  */
448
- function _allColumnKeys(cls: Function): Set<string> | null {
467
+ function _allColumnKeys(cls: ClassRef): Set<string> | null {
449
468
  const cols = columnsFor(cls);
450
469
  return cols ? new Set(cols.keys()) : null;
451
470
  }
@@ -833,6 +852,42 @@ export class BaseModel {
833
852
  */
834
853
  static hashable?: string[];
835
854
 
855
+ /**
856
+ * Columns encrypted at rest with AES-256-GCM under `APP_KEY`, and decrypted
857
+ * transparently on read. Shorthand for putting `cast: "encrypted"` on each one.
858
+ *
859
+ * Unlike {@link hashable} this is reversible and non-destructive: the model
860
+ * property still holds the value you assigned after a `save()`, because the
861
+ * encryption happens on the way to the database rather than to the instance.
862
+ * `$dirty` therefore compares plaintext, and an unchanged column is not
863
+ * rewritten with a new IV on every save.
864
+ *
865
+ * A `json` column in this list encrypts as `encrypted:json`, so it round-trips
866
+ * as the structure it was rather than as `"[object Object]"`.
867
+ *
868
+ * @example
869
+ * ```ts
870
+ * class Client extends BaseModel {
871
+ * static encryptable = ["idNumber", "passportNumber"];
872
+ *
873
+ * // TEXT, not VARCHAR — a payload outgrows its plaintext.
874
+ * @column({ type: "text", nullable: true }) idNumber?: string;
875
+ * @column({ type: "text", nullable: true }) passportNumber?: string;
876
+ * }
877
+ * ```
878
+ *
879
+ * @remarks
880
+ * Encrypted columns cannot be filtered, grouped or usefully sorted — every
881
+ * write draws a fresh IV, so the ciphertext for a given value never repeats.
882
+ * `where()` on one throws rather than quietly matching nothing. For lookup,
883
+ * keep a hashed blind-index column beside it. Add these to {@link hidden} too
884
+ * if the model is serialized to a client: decryption puts the real value back
885
+ * on the instance, and `toJSON()` will happily include it.
886
+ *
887
+ * @category Persistence
888
+ */
889
+ static encryptable?: string[];
890
+
836
891
  /**
837
892
  * Register an observer class for this model.
838
893
  * The observer's lifecycle methods (creating, created, updating, …) are
@@ -1470,8 +1525,8 @@ export class BaseModel {
1470
1525
  const ModelClass = this as unknown as typeof BaseModel;
1471
1526
  const conn = _resolveConn(ModelClass);
1472
1527
  const dialect = dialectFor(conn as unknown as object);
1473
- const casts = getCasts(ModelClass as unknown as Function);
1474
- const colReg = columnsFor(ModelClass as unknown as Function);
1528
+ const casts = getCasts(ModelClass as unknown as ClassRef);
1529
+ const colReg = columnsFor(ModelClass as unknown as ClassRef);
1475
1530
  const useTs = ModelClass.timestamps;
1476
1531
 
1477
1532
  const rows: Record<string, unknown>[] = _writeDialect.run(dialect, () => {
@@ -1480,7 +1535,7 @@ export class BaseModel {
1480
1535
  const row: Record<string, unknown> = {};
1481
1536
  for (const [key, val] of Object.entries(rec as Record<string, unknown>)) {
1482
1537
  if (key.startsWith("_")) continue;
1483
- row[toSnake(key)] = _serializeForWrite(key, val, casts, colReg);
1538
+ row[toSnake(key)] = _serializeForWrite(key, val, casts, colReg, ModelClass.name);
1484
1539
  }
1485
1540
  if (useTs) {
1486
1541
  row["created_at"] = now;
@@ -1556,13 +1611,13 @@ export class BaseModel {
1556
1611
  ): Promise<void> {
1557
1612
  const conn = _resolveConn(this);
1558
1613
  const dialect = dialectFor(conn as unknown as object);
1559
- const casts = getCasts(this as unknown as Function);
1560
- const colReg = columnsFor(this as unknown as Function);
1614
+ const casts = getCasts(this as unknown as ClassRef);
1615
+ const colReg = columnsFor(this as unknown as ClassRef);
1561
1616
 
1562
1617
  const row: Record<string, unknown> = _writeDialect.run(dialect, () => {
1563
1618
  const r: Record<string, unknown> = {};
1564
1619
  for (const [key, val] of Object.entries(data as Record<string, unknown>)) {
1565
- r[toSnake(key)] = _serializeForWrite(key, val, casts, colReg);
1620
+ r[toSnake(key)] = _serializeForWrite(key, val, casts, colReg, this.name);
1566
1621
  }
1567
1622
  return r;
1568
1623
  });
@@ -1643,8 +1698,8 @@ export class BaseModel {
1643
1698
  const ModelClass = this.constructor as typeof BaseModel;
1644
1699
  const conn = _resolveConn(ModelClass);
1645
1700
  const dialect = dialectFor(conn as unknown as object);
1646
- const rels = relNames(ModelClass as unknown as Function);
1647
- const casts = getCasts(ModelClass as unknown as Function);
1701
+ const rels = relNames(ModelClass as unknown as ClassRef);
1702
+ const casts = getCasts(ModelClass as unknown as ClassRef);
1648
1703
 
1649
1704
  await HookRegistry.run(ModelClass, "beforeSave", this);
1650
1705
 
@@ -1676,8 +1731,8 @@ export class BaseModel {
1676
1731
  }
1677
1732
  }
1678
1733
 
1679
- const colReg = columnsFor(ModelClass as unknown as Function);
1680
- const colKeys = _allColumnKeys(ModelClass as unknown as Function);
1734
+ const colReg = columnsFor(ModelClass as unknown as ClassRef);
1735
+ const colKeys = _allColumnKeys(ModelClass as unknown as ClassRef);
1681
1736
 
1682
1737
  if (!this._exists) {
1683
1738
  // ── INSERT ──
@@ -1703,7 +1758,7 @@ export class BaseModel {
1703
1758
  // value is readable straight after save() without a reload.
1704
1759
  self[key] = effective;
1705
1760
  }
1706
- r[toSnake(key)] = _serializeForWrite(key, effective, casts, colReg);
1761
+ r[toSnake(key)] = _serializeForWrite(key, effective, casts, colReg, ModelClass.name);
1707
1762
  }
1708
1763
  if (ModelClass.timestamps) {
1709
1764
  const now = _serializeDate(new Date());
@@ -1785,7 +1840,11 @@ export class BaseModel {
1785
1840
  if (Object.keys(dirty).length > 0) {
1786
1841
  const entries = _writeDialect.run(dialect, () =>
1787
1842
  Object.entries(dirty).map(
1788
- ([k, v]) => [toSnake(k), _serializeForWrite(k, v, casts, colReg)] as [string, unknown],
1843
+ ([k, v]) =>
1844
+ [toSnake(k), _serializeForWrite(k, v, casts, colReg, ModelClass.name)] as [
1845
+ string,
1846
+ unknown,
1847
+ ],
1789
1848
  ),
1790
1849
  );
1791
1850
  const segs: Seg[] = [`UPDATE ${ModelClass.table} SET `];
@@ -1937,8 +1996,8 @@ export class BaseModel {
1937
1996
  */
1938
1997
  replicate(except?: string[]): this {
1939
1998
  const ModelClass = this.constructor as typeof BaseModel;
1940
- const rels = relNames(ModelClass as unknown as Function);
1941
- const colKeys = _allColumnKeys(ModelClass as unknown as Function);
1999
+ const rels = relNames(ModelClass as unknown as ClassRef);
2000
+ const colKeys = _allColumnKeys(ModelClass as unknown as ClassRef);
1942
2001
  const skip = new Set<string>([...SYSTEM_KEYS, ...(except ?? [])]);
1943
2002
  const inst = new (this.constructor as new () => this)();
1944
2003
  for (const [k, v] of ownDataEntries(this, skip, rels, colKeys)) {
@@ -2182,7 +2241,7 @@ export class BaseModel {
2182
2241
  * @category Relationships
2183
2242
  */
2184
2243
  associate(relation: string, model: BaseModel): this {
2185
- const meta = relationsFor(this.constructor).get(relation);
2244
+ const meta = relationsFor(this.constructor as ClassRef).get(relation);
2186
2245
  if (!meta || meta.type !== "belongsTo") {
2187
2246
  throw new Error(
2188
2247
  `associate(): "${relation}" is not a belongsTo relation on ${this.constructor.name}`,
@@ -2209,7 +2268,7 @@ export class BaseModel {
2209
2268
  * @category Relationships
2210
2269
  */
2211
2270
  dissociate(relation: string): this {
2212
- const meta = relationsFor(this.constructor).get(relation);
2271
+ const meta = relationsFor(this.constructor as ClassRef).get(relation);
2213
2272
  if (!meta || meta.type !== "belongsTo") {
2214
2273
  throw new Error(
2215
2274
  `dissociate(): "${relation}" is not a belongsTo relation on ${this.constructor.name}`,
@@ -2249,8 +2308,8 @@ export class BaseModel {
2249
2308
  */
2250
2309
  $dirty(): Record<string, unknown> {
2251
2310
  const ModelClass = this.constructor as typeof BaseModel;
2252
- const rels = relNames(ModelClass as unknown as Function);
2253
- const colKeys = _allColumnKeys(ModelClass as unknown as Function);
2311
+ const rels = relNames(ModelClass as unknown as ClassRef);
2312
+ const colKeys = _allColumnKeys(ModelClass as unknown as ClassRef);
2254
2313
  const out: Record<string, unknown> = {};
2255
2314
  for (const [key, val] of ownDataEntries(this, SYSTEM_KEYS, rels, colKeys)) {
2256
2315
  if (val !== this._original[key] || this._forcedDirty.has(key)) {
@@ -2441,8 +2500,8 @@ function _createLazyPivotProxy(
2441
2500
  function _applyRow(inst: BaseModel, row: Record<string, unknown>): void {
2442
2501
  const self = inst as unknown as Record<string, unknown>;
2443
2502
  const orig: Record<string, unknown> = {};
2444
- const ctor = inst.constructor;
2445
- const ModelClass = ctor as typeof BaseModel;
2503
+ const ModelClass = inst.constructor as typeof BaseModel;
2504
+ const ctor: ClassRef = ModelClass;
2446
2505
  const colReg = columnsFor(ctor);
2447
2506
  installReactiveAccessors(inst); // json/array reactiveCasts accessors (registered at decoration)
2448
2507
  const casts = getCasts(ctor);
@@ -2467,7 +2526,7 @@ function _applyRow(inst: BaseModel, row: Record<string, unknown>): void {
2467
2526
  finalVal = castObj.get(rawVal);
2468
2527
  } else if (typeof cast === "string") {
2469
2528
  // Explicit shorthand cast ('boolean', 'json', 'date', etc.)
2470
- finalVal = applyCastGet(rawVal, cast);
2529
+ finalVal = applyCastGet(rawVal, cast, `${ModelClass.name}.${propKey}`);
2471
2530
  } else if (colType === "boolean" && rawVal !== null && rawVal !== undefined) {
2472
2531
  // Auto-cast based on @column({ type: 'boolean' }) — SQLite stores 0/1
2473
2532
  finalVal = rawVal === 1 || rawVal === "1" || rawVal === true;
@@ -6,6 +6,7 @@ import {
6
6
  toSnakeColumn as _toSnakeColumn,
7
7
  ctorChain,
8
8
  } from "../support/identifiers.ts";
9
+ import { collectEncryptable, encryptedQueryError, isEncryptedCast } from "../casts/encrypted.ts";
9
10
  import type {
10
11
  PaginateResult,
11
12
  SimplePaginateResult,
@@ -21,12 +22,21 @@ import type { BaseModel } from "./BaseModel.ts";
21
22
  import { type ColumnOptions } from "./decorators/column.ts";
22
23
  import { columnsFor, relationsFor } from "./decorators/_metadata.ts";
23
24
  import { currentOrmContext } from "./OrmContext.ts";
25
+ import type { ClassRef } from "../support/classRef.ts";
24
26
 
25
27
  type StringCast = "datetime" | "array" | "json" | "date" | "boolean" | "integer" | "float";
26
28
  type CastOption = ColumnOptions["cast"];
27
29
 
28
- function _getCasts(ctor: Function): Record<string, CastOption> {
30
+ function _getCasts(ctor: ClassRef): Record<string, CastOption> {
29
31
  const merged: Record<string, CastOption> = {};
32
+ const colReg = columnsFor(ctor);
33
+ // Mirrors getCasts() in BaseModel: `static encryptable` resolves to casts, and an
34
+ // explicit cast on the same column wins. Without this the guard below cannot see
35
+ // a column declared encrypted through the list form.
36
+ Object.assign(
37
+ merged,
38
+ collectEncryptable(ctorChain(ctor), (key) => colReg?.get(key)?.type),
39
+ );
30
40
  for (const entry of ctorChain(ctor)) {
31
41
  const casts = (entry as { casts?: Record<string, CastOption> }).casts;
32
42
  if (casts) Object.assign(merged, casts);
@@ -265,9 +275,9 @@ function _createPivotCollection<T extends BaseModel>(
265
275
  // BaseModel as a type only — no cycle.
266
276
 
267
277
  export type GlobalScopeCallback = (qb: ModelQueryBuilder<BaseModel>) => void;
268
- export function _globalScopeRegistry(): Map<Function, Map<string, GlobalScopeCallback>> {
278
+ export function _globalScopeRegistry(): Map<ClassRef, Map<string, GlobalScopeCallback>> {
269
279
  return currentOrmContext().globalScopes as unknown as Map<
270
- Function,
280
+ ClassRef,
271
281
  Map<string, GlobalScopeCallback>
272
282
  >;
273
283
  }
@@ -1133,15 +1143,14 @@ export class ModelQueryBuilder<M extends BaseModel> extends QueryBuilder {
1133
1143
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1134
1144
  withScopes(callback: (scopes: any) => void): this {
1135
1145
  const ModelClass = this._ModelClass;
1136
- const self = this;
1137
1146
  const proxy = new Proxy({} as Record<string, (...args: unknown[]) => void>, {
1138
- get(_target, prop: string | symbol) {
1147
+ get: (_target, prop: string | symbol) => {
1139
1148
  return (...args: unknown[]) => {
1140
1149
  const fn = (ModelClass as unknown as Record<string | symbol, unknown>)[prop];
1141
1150
  if (typeof fn !== "function") return;
1142
1151
  const result = fn(...args) as { apply?: (q: unknown) => void } | null | undefined;
1143
1152
  if (result != null && typeof result.apply === "function") {
1144
- result.apply(self);
1153
+ result.apply(this);
1145
1154
  }
1146
1155
  };
1147
1156
  },
@@ -1240,11 +1249,20 @@ export class ModelQueryBuilder<M extends BaseModel> extends QueryBuilder {
1240
1249
 
1241
1250
  const rawKey = column.split(".").pop() ?? column;
1242
1251
  const camelKey = rawKey.includes("_") ? _toCamel(rawKey) : rawKey;
1243
- const casts = _getCasts(this._ModelClass as unknown as Function);
1244
- const colMeta = columnsFor(this._ModelClass as unknown as Function)?.get(camelKey);
1252
+ const casts = _getCasts(this._ModelClass as unknown as ClassRef);
1253
+ const colMeta = columnsFor(this._ModelClass as unknown as ClassRef)?.get(camelKey);
1245
1254
  const castOpt = casts[rawKey] ?? casts[camelKey] ?? colMeta?.cast;
1246
1255
  const colType = colMeta?.type;
1247
1256
 
1257
+ // Before anything binds: an encrypted column cannot be compared. Running the
1258
+ // cast's set() here would encrypt the search term under a fresh IV, producing
1259
+ // ciphertext that cannot equal what is stored — a query that always returns
1260
+ // nothing and never says why. Same failure the created_at note below describes,
1261
+ // and permanent rather than occasional, so it is refused outright.
1262
+ if (isEncryptedCast(castOpt)) {
1263
+ throw encryptedQueryError(`${this._ModelClass.name}.${camelKey}`);
1264
+ }
1265
+
1248
1266
  if (operator === "in" || operator === "not in") {
1249
1267
  if (Array.isArray(value)) {
1250
1268
  return value.map((v) => this._coerceWhereValue(column, v));
@@ -1306,9 +1324,9 @@ export class ModelQueryBuilder<M extends BaseModel> extends QueryBuilder {
1306
1324
  if (spec.children.length > 0 && related.length > 0) {
1307
1325
  if (meta.type === "morphTo") {
1308
1326
  // Mixed related classes — group by constructor and recurse per group.
1309
- const groups = new Map<Function, BaseModel[]>();
1327
+ const groups = new Map<ClassRef, BaseModel[]>();
1310
1328
  for (const r of related) {
1311
- const ctor = r.constructor as Function;
1329
+ const ctor = r.constructor as ClassRef;
1312
1330
  if (!groups.has(ctor)) groups.set(ctor, []);
1313
1331
  groups.get(ctor)!.push(r);
1314
1332
  }
@@ -1,4 +1,5 @@
1
1
  import { HookRegistry, type HookName } from "./hooks/HookRegistry.ts";
2
+ import type { ClassRef } from "../support/classRef.ts";
2
3
 
3
4
  /**
4
5
  * Observer interface — implement any subset of lifecycle methods to react to a
@@ -61,7 +62,7 @@ const _methodToHook: Record<keyof ModelObserver, HookName> = {
61
62
  * @param ObserverClass - An observer class (zero-arg constructor) implementing any subset of {@link ModelObserver}.
62
63
  * @internal
63
64
  */
64
- export function registerObserver<T>(ModelClass: Function, ObserverClass: ObserverClass<T>): void {
65
+ export function registerObserver<T>(ModelClass: ClassRef, ObserverClass: ObserverClass<T>): void {
65
66
  const instance = new ObserverClass();
66
67
 
67
68
  for (const [method, hook] of Object.entries(_methodToHook) as [keyof ModelObserver, HookName][]) {
@@ -25,11 +25,11 @@ export class OrmContext {
25
25
  /** Connections registered by name, selectable via `static connection`. */
26
26
  namedConnections = new Map<string, SQLInstance>();
27
27
  /** Per-model `onTransition` callbacks, keyed by target state (see the `State` mixin). */
28
- transitionCallbacks = new Map<Function, Map<string, unknown[]>>();
28
+ transitionCallbacks = new Map<ClassRef, Map<string, unknown[]>>();
29
29
  /** Per-model registered global query scopes. */
30
- globalScopes = new Map<Function, Map<string, unknown>>();
30
+ globalScopes = new Map<ClassRef, Map<string, unknown>>();
31
31
  /** Per-model lifecycle hooks. */
32
- hooks = new Map<Function, Map<string, unknown[]>>();
32
+ hooks = new Map<ClassRef, Map<string, unknown[]>>();
33
33
  }
34
34
 
35
35
  let _ctx = new OrmContext();
@@ -58,6 +58,7 @@ export function resetOrmContext(): void {
58
58
  }
59
59
 
60
60
  import { registerAppScope } from "@zerotal/core";
61
+ import type { ClassRef } from "../support/classRef.ts";
61
62
 
62
63
  let _appScopeRegistered = false;
63
64
  if (!_appScopeRegistered) {
@@ -22,6 +22,7 @@
22
22
  import { StateError } from "../errors/index.ts";
23
23
  import { currentOrmContext } from "./OrmContext.ts";
24
24
  import type { Constructor } from "./mixins.ts";
25
+ import type { ClassRef } from "../support/classRef.ts";
25
26
 
26
27
  // ── Types ─────────────────────────────────────────────────────────────────────
27
28
 
@@ -87,7 +88,7 @@ export type TransitionCallback<T> = (
87
88
  // ── Callback registry (execution-scoped on the OrmContext) ────────────────────
88
89
 
89
90
  /** @internal Register or retrieve transition callbacks for a model class. */
90
- function _getCallbacks(ModelClass: Function, state: string): TransitionCallback<unknown>[] {
91
+ function _getCallbacks(ModelClass: ClassRef, state: string): TransitionCallback<unknown>[] {
91
92
  const reg = currentOrmContext().transitionCallbacks;
92
93
  if (!reg.has(ModelClass)) reg.set(ModelClass, new Map());
93
94
  const map = reg.get(ModelClass)!;
@@ -169,7 +170,7 @@ export function State<TBase extends Constructor>(Base: TBase) {
169
170
  toState: string,
170
171
  callback: TransitionCallback<T>,
171
172
  ): void {
172
- _getCallbacks(this as unknown as Function, toState).push(
173
+ _getCallbacks(this as unknown as ClassRef, toState).push(
173
174
  callback as TransitionCallback<unknown>,
174
175
  );
175
176
  }
@@ -260,7 +261,7 @@ export function State<TBase extends Constructor>(Base: TBase) {
260
261
  await (this as unknown as { save(): Promise<unknown> }).save();
261
262
 
262
263
  // Fire registered transition callbacks.
263
- const map = currentOrmContext().transitionCallbacks.get(this.constructor as Function);
264
+ const map = currentOrmContext().transitionCallbacks.get(this.constructor as ClassRef);
264
265
  const meta = { from: currentState, to: newState };
265
266
  const toFns = (map?.get(newState) ?? []) as TransitionCallback<unknown>[];
266
267
  const anyFns = (map?.get("*") ?? []) as TransitionCallback<unknown>[];
@@ -18,19 +18,20 @@
18
18
  import { relationRegistry } from "../relations/RelationRegistry.ts";
19
19
  import type { RelationMetadata } from "../relations/RelationRegistry.ts";
20
20
  import type { ColumnOptions } from "./column.ts";
21
+ import type { ClassRef } from "../../support/classRef.ts";
21
22
 
22
23
  /**
23
24
  * Per-class OWN column definitions. Readers walk the prototype chain to merge inherited.
24
25
  * @internal
25
26
  */
26
- export const columnRegistry = new Map<Function, Map<string, ColumnOptions>>();
27
+ export const columnRegistry = new Map<ClassRef, Map<string, ColumnOptions>>();
27
28
 
28
29
  // ── Definition-time queue ─────────────────────────────────────────────────────
29
30
 
30
31
  interface PendingMember {
31
32
  /** Field name (captured correctly in the decorator body). */
32
33
  name: string;
33
- apply: (ctor: Function) => void;
34
+ apply: (ctor: ClassRef) => void;
34
35
  }
35
36
  let _pending: PendingMember[] = [];
36
37
 
@@ -38,7 +39,7 @@ let _pending: PendingMember[] = [];
38
39
  * Enqueue a member registration from a decorator body (name captured correctly there).
39
40
  * @internal
40
41
  */
41
- export function enqueueMember(name: string, apply: (ctor: Function) => void): void {
42
+ export function enqueueMember(name: string, apply: (ctor: ClassRef) => void): void {
42
43
  _pending.push({ name, apply });
43
44
  }
44
45
 
@@ -48,7 +49,7 @@ export function enqueueMember(name: string, apply: (ctor: Function) => void): vo
48
49
  * contains exactly that class's members and nothing else.
49
50
  * @internal
50
51
  */
51
- export function drainPendingMembers(ctor: Function): void {
52
+ export function drainPendingMembers(ctor: ClassRef): void {
52
53
  if (_pending.length === 0) return;
53
54
  const batch = _pending;
54
55
  _pending = [];
@@ -63,7 +64,7 @@ export function drainPendingMembers(ctor: Function): void {
63
64
  * `static casts` map (seeded from the parent so a subclass extends rather than mutates it).
64
65
  * @internal
65
66
  */
66
- export function registerColumn(ctor: Function, name: string, options: ColumnOptions): void {
67
+ export function registerColumn(ctor: ClassRef, name: string, options: ColumnOptions): void {
67
68
  let m = columnRegistry.get(ctor);
68
69
  if (!m) {
69
70
  m = new Map();
@@ -89,7 +90,7 @@ export function registerColumn(ctor: Function, name: string, options: ColumnOpti
89
90
  * Record relation metadata for `ctor`. Invoked from a drained decorator closure.
90
91
  * @internal
91
92
  */
92
- export function registerRelation(ctor: Function, name: string, meta: RelationMetadata): void {
93
+ export function registerRelation(ctor: ClassRef, name: string, meta: RelationMetadata): void {
93
94
  let m = relationRegistry.get(ctor);
94
95
  if (!m) {
95
96
  m = new Map();
@@ -100,23 +101,23 @@ export function registerRelation(ctor: Function, name: string, meta: RelationMet
100
101
 
101
102
  // ── Convention registration (used by the auto-discovery loader) ───────────────
102
103
 
103
- const _registeredModels = new WeakSet<Function>();
104
+ const _registeredModels = new WeakSet<ClassRef>();
104
105
  /**
105
106
  * class name → model class, for observer/policy association by name.
106
107
  * @internal
107
108
  */
108
- export const modelsByName = new Map<string, Function>();
109
+ export const modelsByName = new Map<string, ClassRef>();
109
110
 
110
111
  /**
111
112
  * Look up a model class by its (unqualified) class name.
112
113
  * @internal
113
114
  */
114
- export function modelByName(name: string): Function | undefined {
115
+ export function modelByName(name: string): ClassRef | undefined {
115
116
  return modelsByName.get(name);
116
117
  }
117
118
 
118
119
  /** Index a model class under its name. Called by @table's drain and by registerModel(). */
119
- function registerModelName(ctor: Function): void {
120
+ function registerModelName(ctor: ClassRef): void {
120
121
  const name = (ctor as { name?: string }).name;
121
122
  if (name) modelsByName.set(name, ctor);
122
123
  }
@@ -136,7 +137,7 @@ function registerModelName(ctor: Function): void {
136
137
  * safe no-op on already-`@table`'d models.
137
138
  * @internal
138
139
  */
139
- export function registerModel(ctor: Function): void {
140
+ export function registerModel(ctor: ClassRef): void {
140
141
  if (_registeredModels.has(ctor)) return;
141
142
  _registeredModels.add(ctor);
142
143
 
@@ -170,13 +171,13 @@ export function registerModel(ctor: Function): void {
170
171
  * Merged column definitions (own + inherited) for a class, or null if none.
171
172
  * @internal
172
173
  */
173
- export function columnsFor(ctor: Function): Map<string, ColumnOptions> | null {
174
+ export function columnsFor(ctor: ClassRef): Map<string, ColumnOptions> | null {
174
175
  const merged = new Map<string, ColumnOptions>();
175
- let cls: Function | null = ctor;
176
+ let cls: ClassRef | null = ctor;
176
177
  while (cls && cls !== Function.prototype) {
177
178
  const c = columnRegistry.get(cls);
178
179
  if (c) for (const [k, v] of c) if (!merged.has(k)) merged.set(k, v);
179
- cls = Object.getPrototypeOf(cls) as Function | null;
180
+ cls = Object.getPrototypeOf(cls) as ClassRef | null;
180
181
  }
181
182
  return merged.size ? merged : null;
182
183
  }
@@ -185,13 +186,13 @@ export function columnsFor(ctor: Function): Map<string, ColumnOptions> | null {
185
186
  * Merged relation metadata (imperative mixins + @decorators, own + inherited).
186
187
  * @internal
187
188
  */
188
- export function relationsFor(ctor: Function): Map<string, RelationMetadata> {
189
+ export function relationsFor(ctor: ClassRef): Map<string, RelationMetadata> {
189
190
  const merged = new Map<string, RelationMetadata>();
190
- let cls: Function | null = ctor;
191
+ let cls: ClassRef | null = ctor;
191
192
  while (cls && cls !== Function.prototype) {
192
193
  const r = relationRegistry.get(cls);
193
194
  if (r) for (const [k, v] of r) if (!merged.has(k)) merged.set(k, v);
194
- cls = Object.getPrototypeOf(cls) as Function | null;
195
+ cls = Object.getPrototypeOf(cls) as ClassRef | null;
195
196
  }
196
197
  return merged;
197
198
  }
@@ -200,7 +201,7 @@ export function relationsFor(ctor: Function): Map<string, RelationMetadata> {
200
201
  * Names of reactive (json/array cast) columns for a class (own + inherited).
201
202
  * @internal
202
203
  */
203
- export function reactiveColumnsFor(ctor: Function): string[] {
204
+ export function reactiveColumnsFor(ctor: ClassRef): string[] {
204
205
  const cols = columnsFor(ctor);
205
206
  if (!cols) return [];
206
207
  const out: string[] = [];
@@ -1,5 +1,6 @@
1
1
  import type { RelationMetadata } from "../relations/RelationRegistry.ts";
2
2
  import { enqueueMember, registerRelation } from "./_metadata.ts";
3
+ import type { ClassRef } from "../../support/classRef.ts";
3
4
 
4
5
  /**
5
6
  * The single plumbing for every relation field decorator (standard TC39 decorators).
@@ -11,7 +12,7 @@ import { enqueueMember, registerRelation } from "./_metadata.ts";
11
12
  * with that class so morph/through values depending on the class name resolve correctly.
12
13
  */
13
14
  export function makeRelationDecorator(
14
- metaFor: (ctor: Function, field: string) => RelationMetadata,
15
+ metaFor: (ctor: ClassRef, field: string) => RelationMetadata,
15
16
  ) {
16
17
  return function (_value: unknown, context: ClassFieldDecoratorContext): void {
17
18
  const name = String(context.name);