litedbmodel 2.2.6 → 2.2.8

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 (142) hide show
  1. package/README.md +146 -86
  2. package/dist/Column.d.ts +30 -6
  3. package/dist/Column.d.ts.map +1 -1
  4. package/dist/Column.js +30 -10
  5. package/dist/Column.js.map +1 -1
  6. package/dist/DBModel.d.ts +28 -16
  7. package/dist/DBModel.d.ts.map +1 -1
  8. package/dist/DBModel.js +60 -46
  9. package/dist/DBModel.js.map +1 -1
  10. package/dist/LazyRelation.d.ts +2 -2
  11. package/dist/LazyRelation.js +2 -2
  12. package/dist/TypeCast.d.ts +2 -2
  13. package/dist/TypeCast.d.ts.map +1 -1
  14. package/dist/TypeCast.js +7 -9
  15. package/dist/TypeCast.js.map +1 -1
  16. package/dist/decorators.d.ts +128 -60
  17. package/dist/decorators.d.ts.map +1 -1
  18. package/dist/decorators.js +393 -251
  19. package/dist/decorators.js.map +1 -1
  20. package/dist/drivers/MysqlHelper.d.ts +3 -3
  21. package/dist/drivers/MysqlHelper.d.ts.map +1 -1
  22. package/dist/drivers/MysqlHelper.js +3 -17
  23. package/dist/drivers/MysqlHelper.js.map +1 -1
  24. package/dist/drivers/MysqlSqlBuilder.d.ts.map +1 -1
  25. package/dist/drivers/MysqlSqlBuilder.js +2 -13
  26. package/dist/drivers/MysqlSqlBuilder.js.map +1 -1
  27. package/dist/drivers/PostgresHelper.d.ts +3 -2
  28. package/dist/drivers/PostgresHelper.d.ts.map +1 -1
  29. package/dist/drivers/PostgresHelper.js +3 -16
  30. package/dist/drivers/PostgresHelper.js.map +1 -1
  31. package/dist/drivers/PostgresSqlBuilder.d.ts.map +1 -1
  32. package/dist/drivers/PostgresSqlBuilder.js +2 -14
  33. package/dist/drivers/PostgresSqlBuilder.js.map +1 -1
  34. package/dist/drivers/SqliteHelper.d.ts +4 -2
  35. package/dist/drivers/SqliteHelper.d.ts.map +1 -1
  36. package/dist/drivers/SqliteHelper.js +4 -2
  37. package/dist/drivers/SqliteHelper.js.map +1 -1
  38. package/dist/drivers/SqliteSqlBuilder.d.ts.map +1 -1
  39. package/dist/drivers/SqliteSqlBuilder.js +2 -13
  40. package/dist/drivers/SqliteSqlBuilder.js.map +1 -1
  41. package/dist/index.cjs +861 -816
  42. package/dist/index.d.ts +2 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/scp/coltype.d.ts +14 -5
  46. package/dist/scp/coltype.d.ts.map +1 -1
  47. package/dist/scp/coltype.js +35 -36
  48. package/dist/scp/coltype.js.map +1 -1
  49. package/dist/scp/decorator-adapter.d.ts +12 -15
  50. package/dist/scp/decorator-adapter.d.ts.map +1 -1
  51. package/dist/scp/decorator-adapter.js +13 -15
  52. package/dist/scp/decorator-adapter.js.map +1 -1
  53. package/dist/scp/index.cjs +237 -233
  54. package/dist/scp/index.mjs +237 -233
  55. package/dist/scp/makesql/json-array.d.ts +6 -0
  56. package/dist/scp/makesql/json-array.d.ts.map +1 -1
  57. package/dist/scp/makesql/json-array.js +17 -2
  58. package/dist/scp/makesql/json-array.js.map +1 -1
  59. package/dist/scp/makesql/tx.d.ts.map +1 -1
  60. package/dist/scp/makesql/tx.js +1 -3
  61. package/dist/scp/makesql/tx.js.map +1 -1
  62. package/dist/scp/relation.d.ts.map +1 -1
  63. package/dist/scp/relation.js +8 -10
  64. package/dist/scp/relation.js.map +1 -1
  65. package/docs/BENCHMARK.md +3 -3
  66. package/docs/COMPARISON.md +7 -7
  67. package/docs/api/README.md +18 -18
  68. package/docs/api/classes/Conditions.md +10 -10
  69. package/docs/api/classes/DBHandler.md +1 -1
  70. package/docs/api/classes/DBModel.md +84 -84
  71. package/docs/api/classes/LimitExceededError.md +1 -1
  72. package/docs/api/classes/Middleware.md +1 -1
  73. package/docs/api/classes/SqlRaw.md +1 -1
  74. package/docs/api/classes/SqlRef.md +1 -1
  75. package/docs/api/classes/Values.md +6 -6
  76. package/docs/api/classes/WriteInReadOnlyContextError.md +1 -1
  77. package/docs/api/classes/WriteOutsideTransactionError.md +1 -1
  78. package/docs/api/functions/belongsTo.md +10 -4
  79. package/docs/api/functions/closeAllPools.md +1 -1
  80. package/docs/api/functions/condsToRecord.md +4 -3
  81. package/docs/api/functions/createMiddleware.md +1 -1
  82. package/docs/api/functions/createPostgresDriver.md +1 -1
  83. package/docs/api/functions/createSqliteDriver.md +1 -1
  84. package/docs/api/functions/dbCast.md +1 -1
  85. package/docs/api/functions/dbCastIn.md +1 -1
  86. package/docs/api/functions/dbDynamic.md +1 -1
  87. package/docs/api/functions/dbFalse.md +1 -1
  88. package/docs/api/functions/dbImmediate.md +1 -1
  89. package/docs/api/functions/dbIn.md +1 -1
  90. package/docs/api/functions/dbNotNull.md +1 -1
  91. package/docs/api/functions/dbNow.md +1 -1
  92. package/docs/api/functions/dbNull.md +1 -1
  93. package/docs/api/functions/dbRaw.md +1 -1
  94. package/docs/api/functions/dbTrue.md +1 -1
  95. package/docs/api/functions/dbUuid.md +1 -1
  96. package/docs/api/functions/dbUuidIn.md +1 -1
  97. package/docs/api/functions/formatLocalDate.md +2 -2
  98. package/docs/api/functions/formatUTCDate.md +2 -2
  99. package/docs/api/functions/getDBConfig.md +1 -1
  100. package/docs/api/functions/getDBHandler.md +1 -1
  101. package/docs/api/functions/getTransactionConnection.md +2 -2
  102. package/docs/api/functions/getTransactionContext.md +2 -2
  103. package/docs/api/functions/hasMany.md +11 -5
  104. package/docs/api/functions/hasOne.md +10 -4
  105. package/docs/api/functions/initDBHandler.md +1 -1
  106. package/docs/api/functions/isAnySqlFragment.md +1 -1
  107. package/docs/api/functions/isConnectionError.md +1 -1
  108. package/docs/api/functions/isSqlCondition.md +1 -1
  109. package/docs/api/functions/isSqlFragment.md +1 -1
  110. package/docs/api/functions/isSqlRaw.md +1 -1
  111. package/docs/api/functions/isSqlRef.md +1 -1
  112. package/docs/api/functions/isSqlTypedFragment.md +1 -1
  113. package/docs/api/functions/model.md +8 -155
  114. package/docs/api/functions/parentRef.md +1 -1
  115. package/docs/api/functions/sql.md +1 -1
  116. package/docs/api/globals.md +99 -58
  117. package/docs/api/interfaces/Column.md +32 -31
  118. package/docs/api/interfaces/ColumnDecorator.md +81 -0
  119. package/docs/api/interfaces/ColumnOptions.md +6 -10
  120. package/docs/api/interfaces/CreatedMiddlewareClass.md +1 -1
  121. package/docs/api/interfaces/DBConfig.md +1 -1
  122. package/docs/api/interfaces/DBConfigOptions.md +1 -1
  123. package/docs/api/interfaces/DeleteOptions.md +1 -1
  124. package/docs/api/interfaces/InsertOptions.md +1 -1
  125. package/docs/api/interfaces/LimitConfig.md +1 -1
  126. package/docs/api/interfaces/MiddlewareConfig.md +1 -1
  127. package/docs/api/interfaces/ModelClassDecorator.md +64 -0
  128. package/docs/api/interfaces/ModelOptions.md +1 -1
  129. package/docs/api/interfaces/PkeyResult.md +1 -1
  130. package/docs/api/interfaces/RelationDecorator.md +72 -0
  131. package/docs/api/interfaces/SelectOptions.md +1 -1
  132. package/docs/api/interfaces/SqlCondition.md +1 -1
  133. package/docs/api/interfaces/SqlFragment.md +1 -1
  134. package/docs/api/interfaces/SqlTypedFragment.md +1 -1
  135. package/docs/api/interfaces/TransactionOptions.md +1 -1
  136. package/docs/api/interfaces/UpdateManyOptions.md +1 -1
  137. package/docs/api/interfaces/UpdateOptions.md +1 -1
  138. package/docs/api-guide.md +17 -17
  139. package/docs/architecture.md +14 -10
  140. package/docs/sql-tagged-template.md +5 -5
  141. package/eslint-plugin/rules/require-declare-for-relations.js +59 -4
  142. package/package.json +8 -7
@@ -27,16 +27,15 @@ const TypeCast_1 = require("./TypeCast");
27
27
  const coltype_1 = require("./scp/coltype");
28
28
  // ── v1 read type contract (issue #9), aligned to the v2 SCP read de-box ────────
29
29
  // The legacy v1 `DBModel` decorator read path historically materialized `@column.bigint()` to a JS
30
- // `bigint` (not JSON-safe) and `@column.datetime()` / an auto-inferred `Date` to a JS `Date`
31
- // (TZ-shifted). Both carry the SAME i64-rounding / date-corruption hazards the v2 SCP read path
30
+ // `bigint` (not JSON-safe) and `@column.datetime()` to a JS `Date` (TZ-shifted). Both carry the SAME i64-rounding / date-corruption hazards the v2 SCP read path
32
31
  // already closed. Per the owner (2026-07-15), v1 is realigned to the v2 read TYPE CONTRACT — and it
33
32
  // does so by REUSING the v2 materializer (`materializeCell`, `src/scp/coltype.ts`), NOT a divergent
34
33
  // v1 coercion. The v1 decorator IS the static type source (unlike v2, whose SoT is the SQL DDL), so
35
34
  // each read-affected decorator variant pins its v2 `MaterializeClass` and routes the raw driver cell
36
35
  // through `materializeCell`:
37
- // - `@column.bigint()` / auto `BigInt` → `int64` → EXACT decimal STRING (no i64 rounding, JSON-safe)
38
- // - `@column.datetime()` / auto `Date` → `date` → TZ-attached STRING (NOT a TZ-shifted JS Date)
39
- // - `@column.boolean()` / auto `Boolean` → `bool` → JS boolean
36
+ // - `@column.bigint()` → `int` → JS bigint (bc's `int` value model, checked i64)
37
+ // - `@column.datetime()` → `date` → TZ-attached STRING (NOT a TZ-shifted JS Date)
38
+ // - `@column.boolean()` → `bool` → JS boolean
40
39
  // `null`/`undefined` pass through (nullable columns) exactly as `materializeCell` and the prior v1
41
40
  // casts did. Fail-closed like v2: a driver cell that cannot be coerced to the declared class throws
42
41
  // (a declared BIGINT column returning a non-integer string is a driver-contract violation, not a
@@ -48,183 +47,211 @@ const coltype_1 = require("./scp/coltype");
48
47
  // ============================================
49
48
  const COLUMNS_KEY = Symbol('litedbmodel:columns');
50
49
  const RELATIONS_KEY = Symbol('litedbmodel:relations');
50
+ // ============================================
51
+ // Decorator Protocol Adapter (legacy + TC39 standard)
52
+ // ============================================
51
53
  /**
52
- * Infer type cast function from design:type metadata
53
- * Returns undefined if type cannot be inferred (Array, Object, etc.)
54
- * Note: All type casts preserve null (DB NULL -> JS null)
54
+ * TC39 (standard) decorators carry NO `design:type`: `emitDecoratorMetadata` is a legacy-decorator
55
+ * feature, and esbuild — which powers `tsx`, `vite` and `vitest` — does not implement it even for
56
+ * legacy decorators. A column's type is therefore declared by its `@column.*` FAMILY, which is the
57
+ * single source of truth on every toolchain (`.text()` / `.number()` / `.datetime()` / …). The bare
58
+ * `@column()` carries no type and is rejected at decoration time — see {@link requireColumnFamily}.
59
+ *
60
+ * Standard decorators reach the class through `context.metadata`: the SAME object is handed to every
61
+ * member decorator of a class and then to the class decorator, in that order. TypeScript only creates
62
+ * it when `Symbol.metadata` exists (esbuild always creates it), so the polyfill below — the one the
63
+ * TC39 proposal prescribes — makes the two emits behave identically.
55
64
  */
56
- function inferTypeCastFromDesignType(target, propertyKey) {
57
- const designType = Reflect.getMetadata('design:type', target, propertyKey);
58
- if (!designType)
59
- return undefined;
60
- switch (designType) {
61
- case Boolean:
62
- return {
63
- // v2 read contract: BOOLEAN → boolean, via the shared materializer.
64
- typeCast: (v) => {
65
- if (v === undefined)
66
- return undefined;
67
- return (0, coltype_1.materializeCell)(v, 'bool');
68
- },
69
- sqlCast: 'boolean',
70
- };
71
- case Date:
72
- // Auto-inferred Date: same behavior as @column.datetime() — v2 read contract materializes the
73
- // date family to a TZ-attached STRING (NOT a TZ-shifted JS Date), via the shared materializer.
74
- return {
75
- typeCast: (v) => {
76
- if (v === undefined)
77
- return undefined;
78
- return (0, coltype_1.materializeCell)(v, 'date');
79
- },
80
- // Delegate to driver-specific serializeDatetime
81
- serialize: (val, typeCast) => {
82
- if (val === null || val === undefined)
83
- return null;
84
- if (val instanceof Date) {
85
- return typeCast?.serializeDatetime(val) ?? val;
86
- }
87
- return val;
88
- },
89
- sqlCast: 'timestamp',
90
- };
91
- case Number:
92
- return {
93
- typeCast: (v) => {
94
- if (v === undefined)
95
- return undefined;
96
- if (v === null)
97
- return null;
98
- const n = Number(v);
99
- return isNaN(n) ? null : n;
100
- },
101
- };
102
- case BigInt:
103
- return {
104
- // v2 read contract: BIGINT/INT8 → EXACT decimal STRING (no i64 rounding, JSON-safe), via the
105
- // shared materializer. NOTE: the TS field type stays `bigint` in existing models for source
106
- // compat, but the VALUE is now the exact string — the whole point of the #9 realignment.
107
- typeCast: (v) => {
108
- if (v === undefined)
109
- return undefined;
110
- return (0, coltype_1.materializeCell)(v, 'int64');
111
- },
112
- sqlCast: 'bigint',
113
- };
114
- // Array, Object, String, and other types require explicit specification
115
- default:
116
- return undefined;
117
- }
65
+ if (typeof Symbol === 'function' && !Symbol.metadata) {
66
+ // `configurable: true`, as the TC39 proposal's own `Symbol.metadata ??= Symbol('Symbol.metadata')`
67
+ // leaves it: a library has no business locking a global well-known symbol. Locking it made a
68
+ // polyfill loaded LATER throw `Cannot redefine property: metadata`. `Symbol.for` keeps the value
69
+ // identical across copies of this package, so a later polyfill that reuses it is a no-op.
70
+ Object.defineProperty(Symbol, 'metadata', {
71
+ value: Symbol.for('Symbol.metadata'),
72
+ writable: false,
73
+ enumerable: false,
74
+ configurable: true,
75
+ });
118
76
  }
119
77
  /**
120
- * Derive the §4.1 SQL-type token from a field's TS `design:type` (Phase F-2 / #105 option B), for a
121
- * column with NO explicit `sqlCast` family. This types the SCP typed-read de-box for a bare `@column()`
122
- * (the README shape) — extending the SAME `design:type` source `inferTypeCastFromDesignType` reads for
123
- * the read cast, so it stays consistent with v1's read contract (String→TEXT→string, Number→INTEGER→
124
- * number, Boolean→BOOLEAN, Date→TIMESTAMP, BigInt→BIGINT). Returns `undefined` when `design:type` is
125
- * absent (no `emitDecoratorMetadata`) or is a family with no unambiguous SQL type (Array/Object) — the
126
- * adapter then falls back to its documented default / the `columnTypes` escape hatch (REAL/DECIMAL/etc.).
78
+ * Which decorator protocol a decorator was invoked under. Legacy passes `(prototype, propertyKey)`;
79
+ * standard passes `(value, context)` where `context.kind` names the member being decorated. The
80
+ * `kind` probe is the discriminator both emits guarantee.
81
+ * @internal
127
82
  */
128
- function inferBaseSqlTypeFromDesignType(target, propertyKey) {
129
- const designType = Reflect.getMetadata('design:type', target, propertyKey);
130
- if (!designType)
131
- return undefined;
132
- switch (designType) {
133
- case String:
134
- return 'TEXT';
135
- case Number:
136
- return 'INTEGER';
137
- case Boolean:
138
- return 'BOOLEAN';
139
- case Date:
140
- return 'TIMESTAMP';
141
- case BigInt:
142
- return 'BIGINT';
143
- default:
144
- // Array / Object / unknown — no unambiguous scalar SQL type. The adapter's escape hatch pins it.
145
- return undefined;
83
+ function isStandardContext(second) {
84
+ return typeof second === 'object' && second !== null && typeof second.kind === 'string';
85
+ }
86
+ /**
87
+ * The TC39 metadata bag of the class currently being defined. Fail-closed: a toolchain that hands a
88
+ * decorator no bag cannot register a column at all, and silently dropping it is exactly the class of
89
+ * defect this file is being repaired for (issue #287) — so it throws, naming the cause.
90
+ */
91
+ function classMetadataBag(context, propertyKey) {
92
+ const bag = context.metadata;
93
+ if (bag === undefined) {
94
+ throw new Error(`litedbmodel: the standard-decorator context for '${propertyKey}' carries no \`context.metadata\`. ` +
95
+ `litedbmodel defines \`Symbol.metadata\` on import, which every current emit needs to create the ` +
96
+ `bag — import litedbmodel (or 'litedbmodel/decorators') BEFORE the module that defines the model.`);
146
97
  }
98
+ return bag;
147
99
  }
148
100
  /**
149
- * Register column metadata on the model class
101
+ * The COLUMN map a decorator registers into, per protocol.
102
+ *
103
+ * A subclass must NOT write into the map it INHERITS: `Reflect.getMetadata` walks the prototype chain
104
+ * (and a TC39 metadata bag prototype-chains by construction), so mutating the value it returns leaks a
105
+ * subclass's columns into its base — and from there into every SIBLING subclass that extends the same
106
+ * decorated base. Both resolvers therefore COPY the inherited map on first write and own the copy.
150
107
  */
151
- function registerColumn(target, propertyKey, options) {
152
- const constructor = target.constructor;
153
- // Get or create columns map
154
- const columns = Reflect.getMetadata(COLUMNS_KEY, constructor) || new Map();
155
- // Auto-infer type info if not explicitly provided
156
- let finalTypeCast = options.typeCast;
157
- let finalSerialize = options.serialize;
158
- let finalSqlCast = options.sqlCast;
159
- if (!options.skipAutoInfer) {
160
- const inferred = inferTypeCastFromDesignType(target, propertyKey);
161
- if (inferred) {
162
- // Use inferred values only if not explicitly provided
163
- if (!finalTypeCast)
164
- finalTypeCast = inferred.typeCast;
165
- if (!finalSerialize)
166
- finalSerialize = inferred.serialize;
167
- if (!finalSqlCast)
168
- finalSqlCast = inferred.sqlCast;
169
- }
108
+ function legacyColumnMap(constructor) {
109
+ const own = Reflect.getOwnMetadata(COLUMNS_KEY, constructor);
110
+ if (own !== undefined)
111
+ return own;
112
+ const inherited = Reflect.getMetadata(COLUMNS_KEY, constructor);
113
+ const map = new Map(inherited);
114
+ Reflect.defineMetadata(COLUMNS_KEY, map, constructor);
115
+ return map;
116
+ }
117
+ /** @see legacyColumnMap */
118
+ function standardColumnMap(bag) {
119
+ if (!Object.prototype.hasOwnProperty.call(bag, COLUMNS_KEY)) {
120
+ bag[COLUMNS_KEY] = new Map(bag[COLUMNS_KEY]);
170
121
  }
171
- // Phase F-2 (#105 option B): record the base SQL-type token from the field's TS `design:type` so the
172
- // SCP typed-read de-box can type a bare `@column()` (no explicit sqlCast family) correctly — the fix
173
- // for F1's blanket-INTEGER default (which threw `materialize int32` on a live string column). Recorded
174
- // always (independent of the auto-infer skip / explicit sqlCast); the adapter consults it only when
175
- // there is no explicit `sqlCast` family and no `columnTypes` override.
176
- const baseSqlType = inferBaseSqlTypeFromDesignType(target, propertyKey);
122
+ return bag[COLUMNS_KEY];
123
+ }
124
+ /** The RELATION list a decorator registers into. Copy-on-inherit, exactly as the column map. */
125
+ function legacyRelationList(constructor) {
126
+ const own = Reflect.getOwnMetadata(RELATIONS_KEY, constructor);
127
+ if (own !== undefined)
128
+ return own;
129
+ const inherited = Reflect.getMetadata(RELATIONS_KEY, constructor);
130
+ const list = inherited !== undefined ? [...inherited] : [];
131
+ Reflect.defineMetadata(RELATIONS_KEY, list, constructor);
132
+ return list;
133
+ }
134
+ /** @see legacyRelationList */
135
+ function standardRelationList(bag) {
136
+ if (!Object.prototype.hasOwnProperty.call(bag, RELATIONS_KEY)) {
137
+ const inherited = bag[RELATIONS_KEY];
138
+ bag[RELATIONS_KEY] = inherited !== undefined ? [...inherited] : [];
139
+ }
140
+ return bag[RELATIONS_KEY];
141
+ }
142
+ /**
143
+ * Record one column on the class being defined. The ONE place a `@column.*` family lands, for both
144
+ * decorator protocols — the protocol only decides WHICH map is handed in ({@link legacyColumnMap} /
145
+ * {@link standardColumnMap}).
146
+ */
147
+ function registerColumn(columns, propertyKey, options) {
177
148
  columns.set(propertyKey, {
178
149
  columnName: options.columnName,
179
- typeCast: finalTypeCast,
180
- serialize: finalSerialize,
150
+ typeCast: options.typeCast,
151
+ serialize: options.serialize,
181
152
  primaryKey: options.primaryKey,
182
153
  autoIncrement: options.autoIncrement,
183
- sqlCast: finalSqlCast,
184
- ...(baseSqlType !== undefined ? { baseSqlType } : {}),
154
+ sqlCast: options.sqlCast,
155
+ ...(options.baseSqlType !== undefined ? { baseSqlType: options.baseSqlType } : {}),
185
156
  });
186
- Reflect.defineMetadata(COLUMNS_KEY, columns, constructor);
157
+ }
158
+ /** Parse the `columnName | ColumnOptions` argument every family accepts. */
159
+ function parseColumnOptions(columnNameOrOptions, propKey) {
160
+ if (typeof columnNameOrOptions === 'string')
161
+ return { columnName: columnNameOrOptions };
162
+ if (columnNameOrOptions) {
163
+ return {
164
+ columnName: columnNameOrOptions.columnName || propKey,
165
+ primaryKey: columnNameOrOptions.primaryKey,
166
+ autoIncrement: columnNameOrOptions.autoIncrement,
167
+ };
168
+ }
169
+ return { columnName: propKey };
187
170
  }
188
171
  /**
189
- * Create a column decorator with optional type cast and serialize
172
+ * Reject a write a TEXT-shaped family cannot carry.
173
+ *
174
+ * {@link WriteValue} widens every string-reading column to also accept the `Date` / `bigint` that a
175
+ * datetime or bigint column serializes, because `Column<V>` carries only the READ type — the type
176
+ * system cannot tell `@column.text()` from `@column.datetime()` (both read `string`). The FAMILY can,
177
+ * so it says so here: a `Date` handed to a text column reaches the driver unserialized and PostgreSQL
178
+ * stores `{}` — silent corruption, measured. Fail closed, naming the family that would have taken it.
179
+ */
180
+ function rejectNonText(family, val) {
181
+ if (val === null || val === undefined)
182
+ return null;
183
+ if (val instanceof Date) {
184
+ throw new Error(`litedbmodel: @column.${family}() received a Date. A text column cannot serialize one — the ` +
185
+ 'driver would store an object literal. Use @column.datetime() / @column.date() for a date ' +
186
+ 'column, or format the value yourself.');
187
+ }
188
+ if (typeof val === 'object') {
189
+ throw new Error(`litedbmodel: @column.${family}() received an object. Use @column.json<T>() for a JSON column, ` +
190
+ 'or @column.custom() with a serializer.');
191
+ }
192
+ return val;
193
+ }
194
+ /**
195
+ * Read a column whose family declares `string`. Most drivers already hand back a string, but SQLite is
196
+ * dynamically typed: it returns a JS number for a `NUMERIC`/`DECIMAL` column whatever the declaration
197
+ * says, and the declared type would be a lie on that dialect alone.
198
+ */
199
+ function materializeString(family, v) {
200
+ if (v === undefined || v === null)
201
+ return v;
202
+ if (typeof v === 'string')
203
+ return v;
204
+ if (typeof v === 'number' || typeof v === 'bigint' || typeof v === 'boolean')
205
+ return String(v);
206
+ throw new Error(`materialize ${family}: driver returned ${typeof v} for a ${family} column. Use @column.passthrough() ` +
207
+ 'for a BYTEA/BLOB column, or @column.json<T>() for JSON.');
208
+ }
209
+ /**
210
+ * Build one `@column.*` family decorator. The returned decorator accepts BOTH protocols: legacy hands
211
+ * it `(prototype, propertyKey)`, TC39 standard hands it `(undefined, context)`. Everything that
212
+ * describes the COLUMN (cast, serializer, SQL family) is fixed here by the family itself — nothing is
213
+ * read back from the compiler, so every toolchain produces the same model.
214
+ *
190
215
  * @param typeCast - Function to convert DB value to JS value (read)
191
216
  * @param serialize - Function to convert JS value to DB value (write)
192
- * @param skipAutoInfer - If true, skip auto-inference even if typeCast is undefined
193
- * @param sqlCast - SQL type for automatic casting in conditions (e.g., 'uuid')
217
+ * @param sqlCast - SQL type family for casting in conditions / typed reads (e.g. `'uuid'`)
218
+ * @param baseSqlType - §4.1 SQL-type token for a family that applies no cast (`@column.text()`)
194
219
  */
195
- function createColumnDecorator(typeCast, serialize, skipAutoInfer = false, sqlCast) {
220
+ function createColumnDecorator(typeCast, serialize, sqlCast, baseSqlType) {
196
221
  return function (columnNameOrOptions) {
197
- return function (target, propertyKey) {
198
- const propKey = String(propertyKey);
199
- // If typeCast is explicitly provided, skip auto-inference
200
- const shouldSkipAutoInfer = skipAutoInfer || typeCast !== undefined;
201
- // Parse options
202
- let columnName;
203
- let primaryKey;
204
- let autoIncrement;
205
- if (typeof columnNameOrOptions === 'string') {
206
- columnName = columnNameOrOptions;
207
- }
208
- else if (columnNameOrOptions) {
209
- columnName = columnNameOrOptions.columnName || propKey;
210
- primaryKey = columnNameOrOptions.primaryKey;
211
- autoIncrement = columnNameOrOptions.autoIncrement;
212
- }
213
- else {
214
- columnName = propKey;
222
+ return function (targetOrValue, keyOrContext) {
223
+ if (isStandardContext(keyOrContext)) {
224
+ const propKey = String(keyOrContext.name);
225
+ const parsed = parseColumnOptions(columnNameOrOptions, propKey);
226
+ registerColumn(standardColumnMap(classMetadataBag(keyOrContext, propKey)), propKey, {
227
+ ...parsed, typeCast, serialize, sqlCast, baseSqlType,
228
+ });
229
+ return;
215
230
  }
216
- registerColumn(target, propKey, {
217
- columnName,
218
- typeCast,
219
- serialize,
220
- skipAutoInfer: shouldSkipAutoInfer,
221
- primaryKey,
222
- autoIncrement,
223
- sqlCast,
231
+ const propKey = String(keyOrContext);
232
+ const parsed = parseColumnOptions(columnNameOrOptions, propKey);
233
+ registerColumn(legacyColumnMap(targetOrValue.constructor), propKey, {
234
+ ...parsed, typeCast, serialize, sqlCast, baseSqlType,
224
235
  });
225
236
  };
226
237
  };
227
238
  }
239
+ /**
240
+ * The bare `@column()` — REMOVED. It declared no type: it relied on `design:type`, which only exists
241
+ * under legacy decorators AND `emitDecoratorMetadata`, which esbuild (tsx / vite / vitest) does not
242
+ * implement and TC39 standard decorators do not have. Where that metadata was absent the column
243
+ * silently got NO cast and the raw driver value reached the caller — an `integer` read back as a
244
+ * `bigint`, a `numeric` as a `string` (issue #286). A column states its own type instead.
245
+ */
246
+ function requireColumnFamily() {
247
+ throw new Error('litedbmodel: `@column()` no longer declares a column — a column must state its type with a ' +
248
+ 'family: @column.text() / .decimal() / .number() / .boolean() / .bigint() / .datetime() / .date() / .uuid() / ' +
249
+ '.json() / .stringArray() / .intArray() / .numericArray() / .booleanArray() / .datetimeArray() / ' +
250
+ '.passthrough() / .custom(). Every family takes the same argument as `@column()` did ' +
251
+ "(`@column.text('db_name')`, `@column.number({ primaryKey: true })`). The bare form inferred the " +
252
+ 'type from `emitDecoratorMetadata`, which esbuild (tsx/vite/vitest) never emits and standard ' +
253
+ 'decorators do not have, so it silently produced untyped reads.');
254
+ }
228
255
  /**
229
256
  * Serialize array using driver's type cast if available.
230
257
  * Falls back to PostgreSQL format for backward compatibility.
@@ -293,85 +320,104 @@ function serializeJson(val, typeCast) {
293
320
  /**
294
321
  * Column decorator for defining model properties.
295
322
  *
296
- * **Auto-inference**: For simple types (boolean, number, Date, bigint),
297
- * type conversion is automatically inferred from the TypeScript property type.
298
- * No need to use explicit variants like `@column.boolean()`.
299
- *
300
- * Auto-inferred types:
301
- * ```typescript
302
- * @column() id?: number; // Auto: Number conversion
303
- * @column() name?: string; // No conversion needed
304
- * @column() is_active?: boolean; // Auto: Boolean conversion
305
- * @column() created_at?: Date; // Auto: DateTime conversion
306
- * @column() large_id?: bigint; // Auto: BigInt conversion
307
- * @column('custom_name') prop?: string; // Custom column name
308
- * ```
323
+ * **A column declares its own type with a family** — the family decides both the TypeScript type the
324
+ * model declares and the value `find()` gives back, on every toolchain and under either decorator
325
+ * protocol. Nothing is inferred from the TypeScript type annotation.
309
326
  *
310
- * Explicit type conversion required (cannot be auto-inferred):
311
327
  * ```typescript
312
- * @column.stringArray() tags?: string[]; // Array element type unknown
313
- * @column.intArray() scores?: number[]; // Array element type unknown
314
- * @column.json<MyType>() data?: MyType; // Generic type unknown
315
- * @column.date() birth_date?: string; // date vs datetime distinction
328
+ * @column.number({ primaryKey: true, autoIncrement: true }) id?: number;
329
+ * @column.text() name?: string; // TEXT / VARCHAR / CHAR / ENUM — no cast needed
330
+ * @column.text('custom_name') prop?: string; // custom column name
331
+ * @column.decimal() price?: string; // NUMERIC / DECIMAL — exact decimal string
332
+ * @column.boolean() is_active?: boolean;
333
+ * @column.datetime() created_at?: string; // TZ-attached string, NOT a JS Date
334
+ * @column.date() birth_date?: string; // 'YYYY-MM-DD'
335
+ * @column.bigint() large_id?: bigint; // bc `int` — a JS bigint
336
+ * @column.uuid() ext_id?: string;
337
+ * @column.stringArray() tags?: string[];
338
+ * @column.intArray() scores?: number[];
339
+ * @column.json<MyType>() data?: MyType;
340
+ * @column.date() birth_date?: string; // 'YYYY-MM-DD' — not a datetime
341
+ * @column.passthrough() payload?: unknown; // BYTEA / BLOB — no cast exists
342
+ * @column.custom((v) => String(v).toUpperCase()) status?: string;
316
343
  * ```
317
344
  *
318
- * Note: The explicit variants (`@column.boolean()`, `@column.datetime()`, etc.)
319
- * still work and can be used when you want to be explicit about the conversion.
345
+ * Every family takes what the removed bare `@column()` took: a column name, or {@link ColumnOptions}.
346
+ * The bare form inferred the type from `emitDecoratorMetadata`, which esbuild (tsx / vite / vitest)
347
+ * never emits and TC39 standard decorators do not have, so it silently produced untyped reads; it now
348
+ * reports the migration instead (see {@link requireColumnFamily}).
320
349
  *
321
350
  * @category Decorators
322
351
  */
323
352
  exports.column = Object.assign(
324
- // Basic @column() - no type conversion
325
- createColumnDecorator(), {
353
+ // The bare `@column()` declared no type — it is rejected, with the migration in the message.
354
+ (_columnNameOrOptions) => requireColumnFamily(), {
326
355
  // ============================================
327
356
  // Primitive Types
328
357
  // ============================================
358
+ /**
359
+ * Text type (TEXT / VARCHAR / CHAR / ENUM / citext) — the driver value is already a string, so no
360
+ * cast is applied. The family still DECLARES the column so the typed read types it as `TEXT`.
361
+ * @example @column.text() name?: string;
362
+ * @example @column.text('user_name') name?: string;
363
+ */
364
+ text: (columnNameOrOptions) => createColumnDecorator((v) => materializeString('text', v), // the family DECLARES `string`, so it hands back one
365
+ (val) => rejectNonText('text', val), // …and it cannot SERIALIZE a Date or an object
366
+ undefined, // no sqlCast family (a text literal needs no SQL cast)
367
+ 'TEXT' // §4.1 token: types the SCP typed read
368
+ )(columnNameOrOptions),
369
+ /**
370
+ * Exact decimal type (NUMERIC / DECIMAL) — read back as its exact decimal STRING (a JS number
371
+ * rounds past 2^53). The family states the column is `numeric`, so a write binds the string as a
372
+ * numeric — a batch write (`createMany` / `updateMany`) included.
373
+ * @example @column.decimal() price?: string;
374
+ */
375
+ decimal: (columnNameOrOptions) => createColumnDecorator((v) => materializeString('decimal', v), (val) => rejectNonText('decimal', val), 'numeric' // sqlCast for WHERE / INSERT / batch-write type casting
376
+ )(columnNameOrOptions),
329
377
  /**
330
378
  * Boolean type conversion
331
379
  * Converts 't'/'f', 'true'/'false', 1/0 to boolean
332
380
  * Preserves null for nullable columns, undefined stays undefined
333
381
  * @example @column.boolean() is_active?: boolean;
334
382
  */
335
- boolean: (columnName) => createColumnDecorator(
383
+ boolean: (columnNameOrOptions) => createColumnDecorator(
336
384
  // v2 read contract: BOOLEAN → boolean, via the shared materializer (issue #9).
337
385
  (v) => {
338
386
  if (v === undefined)
339
387
  return undefined;
340
388
  return (0, coltype_1.materializeCell)(v, 'bool');
341
389
  }, undefined, // no custom serialize
342
- false, // allow auto-inference
343
390
  'boolean' // sqlCast for updateMany type inference
344
- )(columnName),
391
+ )(columnNameOrOptions),
345
392
  /**
346
393
  * Number type conversion (from string)
347
394
  * Preserves null for nullable columns, undefined stays undefined
348
395
  * @example @column.number() amount?: number;
349
396
  */
350
- number: (columnName) => createColumnDecorator((v) => {
397
+ number: (columnNameOrOptions) => createColumnDecorator((v) => {
351
398
  if (v === undefined)
352
399
  return undefined;
353
400
  if (v === null)
354
401
  return null;
355
402
  const n = Number(v);
356
403
  return isNaN(n) ? null : n;
357
- })(columnName),
404
+ })(columnNameOrOptions),
358
405
  /**
359
406
  * BigInt type conversion
360
407
  * Preserves null for nullable columns, undefined stays undefined
361
- * @example @column.bigint() large_id?: bigint;
408
+ * @example @column.bigint() large_id?: bigint; // bc `int` — a JS bigint, checked i64
362
409
  */
363
- bigint: (columnName) => createColumnDecorator(
364
- // v2 read contract: BIGINT/INT8 → EXACT decimal STRING (no i64 rounding, JSON-safe), via the
365
- // shared materializer (issue #9). The declared TS field type may still be `bigint` in existing
366
- // models, but the runtime value is now the exact string — that IS the realignment.
410
+ bigint: (columnNameOrOptions) => createColumnDecorator(
411
+ // An integer column reads back as a JS `bigint` — behavior-contracts' `int` value model
412
+ // (`int` = bigint, checked i64), which is what the drivers already hand over. `JSON.stringify`
413
+ // does not serialize a bigint: give the field a `toJSON`, or convert at the boundary.
367
414
  (v) => {
368
415
  if (v === undefined)
369
416
  return undefined;
370
- return (0, coltype_1.materializeCell)(v, 'int64');
417
+ return (0, coltype_1.materializeCell)(v, 'int');
371
418
  }, undefined, // no custom serialize
372
- false, // allow auto-inference
373
419
  'bigint' // sqlCast for WHERE/INSERT type casting
374
- )(columnName),
420
+ )(columnNameOrOptions),
375
421
  // ============================================
376
422
  // Date/Time Types
377
423
  // ============================================
@@ -383,9 +429,9 @@ createColumnDecorator(), {
383
429
  * - PostgreSQL: Serializes to ISO 8601 UTC string with 'Z' suffix for explicit timezone
384
430
  * - MySQL/SQLite: Passes Date object to driver (driver-dependent timezone handling)
385
431
  *
386
- * @example @column.datetime() created_at?: Date;
432
+ * @example @column.datetime() created_at?: string; // TZ-attached string, NOT a JS Date
387
433
  */
388
- datetime: (columnName) => createColumnDecorator(
434
+ datetime: (columnNameOrOptions) => createColumnDecorator(
389
435
  // v2 read contract: DATE/TIMESTAMP/TIMESTAMPTZ/DATETIME/TIME → TZ-attached STRING (NOT a
390
436
  // TZ-shifted JS Date), via the shared materializer (issue #9). A driver already returning the
391
437
  // native textual form passes through; a JS Date is rendered to its lossless ISO instant.
@@ -402,9 +448,8 @@ createColumnDecorator(), {
402
448
  return typeCast?.serializeDatetime(val) ?? val;
403
449
  }
404
450
  return val;
405
- }, false, // allow auto-inference
406
- 'timestamp' // sqlCast for updateMany type inference
407
- )(columnName),
451
+ }, 'timestamp' // sqlCast for updateMany type inference
452
+ )(columnNameOrOptions),
408
453
  /**
409
454
  * Date type conversion — returns YYYY-MM-DD string.
410
455
  * Preserves null for nullable columns, undefined stays undefined.
@@ -412,9 +457,9 @@ createColumnDecorator(), {
412
457
  * DB values (Date object or string) are normalized to 'YYYY-MM-DD' string.
413
458
  * On write, string values are passed through; Date objects are formatted as 'YYYY-MM-DD'.
414
459
  *
415
- * @example @column.date() birth_date?: string;
460
+ * @example @column.date() birth_date?: string; // 'YYYY-MM-DD'
416
461
  */
417
- date: (columnName) => createColumnDecorator((v) => {
462
+ date: (columnNameOrOptions) => createColumnDecorator((v) => {
418
463
  if (v === undefined)
419
464
  return undefined;
420
465
  if (v === null)
@@ -440,9 +485,8 @@ createColumnDecorator(), {
440
485
  return isNaN(val.getTime()) ? null : (0, TypeCast_1.formatLocalDate)(val);
441
486
  }
442
487
  return val;
443
- }, false, // allow auto-inference
444
- 'date' // sqlCast for updateMany type inference
445
- )(columnName),
488
+ }, 'date' // sqlCast for updateMany type inference
489
+ )(columnNameOrOptions),
446
490
  // ============================================
447
491
  // Array Types
448
492
  // ============================================
@@ -451,60 +495,65 @@ createColumnDecorator(), {
451
495
  * Preserves null for nullable columns, undefined stays undefined
452
496
  * @example @column.stringArray() tags?: string[];
453
497
  */
454
- stringArray: (columnName) => createColumnDecorator((v) => {
498
+ stringArray: (columnNameOrOptions) => createColumnDecorator((v) => {
455
499
  if (v === undefined)
456
500
  return undefined;
457
501
  if (v === null)
458
502
  return null;
459
503
  return (0, TypeCast_1.castToStringArray)(v);
460
- }, serializeArray, false, 'text[]')(columnName),
504
+ }, serializeArray, 'text[]')(columnNameOrOptions),
461
505
  /**
462
506
  * Integer array type conversion (integer[])
463
507
  * Preserves null for nullable columns, undefined stays undefined
464
508
  * @example @column.intArray() scores?: number[];
465
509
  */
466
- intArray: (columnName) => createColumnDecorator((v) => {
510
+ intArray: (columnNameOrOptions) => createColumnDecorator((v) => {
467
511
  if (v === undefined)
468
512
  return undefined;
469
513
  if (v === null)
470
514
  return null;
471
515
  return (0, TypeCast_1.castToIntegerArray)(v);
472
- }, serializeArray, false, 'int[]')(columnName),
516
+ }, serializeArray, 'int[]')(columnNameOrOptions),
473
517
  /**
474
518
  * Numeric array type conversion (numeric[], allows null elements)
475
519
  * Preserves null for nullable columns, undefined stays undefined
476
520
  * @example @column.numericArray() values?: (number | null)[];
477
521
  */
478
- numericArray: (columnName) => createColumnDecorator((v) => {
522
+ numericArray: (columnNameOrOptions) => createColumnDecorator((v) => {
479
523
  if (v === undefined)
480
524
  return undefined;
481
525
  if (v === null)
482
526
  return null;
483
527
  return (0, TypeCast_1.castToNumericArray)(v);
484
- }, serializeArray, false, 'numeric[]')(columnName),
528
+ }, serializeArray, 'numeric[]')(columnNameOrOptions),
485
529
  /**
486
530
  * Boolean array type conversion (boolean[])
487
531
  * Preserves null for nullable columns, undefined stays undefined
488
532
  * @example @column.booleanArray() flags?: (boolean | null)[];
489
533
  */
490
- booleanArray: (columnName) => createColumnDecorator((v) => {
534
+ booleanArray: (columnNameOrOptions) => createColumnDecorator((v) => {
491
535
  if (v === undefined)
492
536
  return undefined;
493
537
  if (v === null)
494
538
  return null;
495
539
  return (0, TypeCast_1.castToBooleanArray)(v);
496
- }, serializeBooleanArray, false, 'boolean[]')(columnName),
540
+ }, serializeBooleanArray, 'boolean[]')(columnNameOrOptions),
497
541
  /**
498
542
  * DateTime array type conversion (timestamp[])
499
543
  * Preserves null for nullable columns, undefined stays undefined
500
- * @example @column.datetimeArray() event_dates?: (Date | null)[];
544
+ * @example @column.datetimeArray() event_dates?: (string | null)[];
501
545
  */
502
- datetimeArray: (columnName) => createColumnDecorator((v) => {
546
+ datetimeArray: (columnNameOrOptions) => createColumnDecorator(
547
+ // Element-wise the SAME contract as `@column.datetime()`: a TZ-attached STRING, never a
548
+ // TZ-shifted JS Date. The array family used to be the one place a `Date` still came back, so
549
+ // `JSON.stringify` of a row silently re-rendered those elements in UTC — the exact hazard #9
550
+ // closed for the scalar.
551
+ (v) => {
503
552
  if (v === undefined)
504
553
  return undefined;
505
554
  if (v === null)
506
555
  return null;
507
- return (0, TypeCast_1.castToDatetimeArray)(v);
556
+ return (0, TypeCast_1.castToDatetimeArray)(v).map((d) => (d === null ? null : (0, coltype_1.materializeCell)(d, 'date')));
508
557
  },
509
558
  // DateTime arrays serialize each Date to ISO string
510
559
  (val) => {
@@ -514,7 +563,7 @@ createColumnDecorator(), {
514
563
  return val;
515
564
  const mapped = val.map(v => v === null ? 'NULL' : (v instanceof Date ? v.toISOString() : String(v)));
516
565
  return `{${mapped.join(',')}}`;
517
- })(columnName),
566
+ })(columnNameOrOptions),
518
567
  // ============================================
519
568
  // JSON Types
520
569
  // ============================================
@@ -524,13 +573,13 @@ createColumnDecorator(), {
524
573
  * @example @column.json() metadata?: Record<string, unknown>;
525
574
  * @example @column.json<UserSettings>() settings?: UserSettings;
526
575
  */
527
- json: (columnName) => createColumnDecorator((v) => {
576
+ json: (columnNameOrOptions) => createColumnDecorator((v) => {
528
577
  if (v === undefined)
529
578
  return undefined;
530
579
  if (v === null)
531
580
  return null;
532
581
  return (0, TypeCast_1.castToJson)(v);
533
- }, serializeJson, false, 'jsonb')(columnName),
582
+ }, serializeJson, 'jsonb')(columnNameOrOptions),
534
583
  // ============================================
535
584
  // UUID Type (PostgreSQL)
536
585
  // ============================================
@@ -560,37 +609,71 @@ createColumnDecorator(), {
560
609
  return null;
561
610
  // UUID values from DB are typically already strings
562
611
  return String(v);
563
- }, undefined, // No serialization needed - handled by sqlCast
564
- true, // Skip auto-inference
612
+ }, (val) => rejectNonText('uuid', val), // the ::uuid cast handles the rest
565
613
  'uuid' // SQL type for casting
566
614
  )(columnNameOrOptions),
567
615
  // ============================================
568
616
  // Custom Type Conversion
569
617
  // ============================================
618
+ /**
619
+ * The driver's value, UNCAST — for a column whose type the library has no cast for (`BYTEA` /
620
+ * `BLOB` arrives as a driver Buffer, a vendor type as whatever the driver decodes). It states
621
+ * "this column is passed through", which is a DECLARATION; the removed bare `@column()` was the
622
+ * absence of one, and produced this same passthrough by accident for every unresolvable type.
623
+ * @example @column.passthrough() payload?: unknown;
624
+ */
625
+ passthrough: (columnNameOrOptions) => createColumnDecorator()(columnNameOrOptions),
570
626
  /**
571
627
  * Custom type conversion with user-provided function
572
628
  * @example @column.custom((v) => String(v).toUpperCase()) status?: string;
573
629
  * @example @column.custom((v) => v, (v) => JSON.stringify(v)) data?: MyType; // with serializer
574
630
  */
575
- custom: (castFn, serializeFn, columnName) => createColumnDecorator(castFn, serializeFn)(columnName),
631
+ custom: (castFn, serializeFn, columnNameOrOptions) => createColumnDecorator(castFn, serializeFn)(columnNameOrOptions),
576
632
  });
577
633
  // ============================================
578
634
  // Relation Decorators
579
635
  // ============================================
580
636
  /**
581
- * Register relation metadata on the model class
637
+ * Record one relation on the class being defined — the ONE place `@hasMany` / `@belongsTo` / `@hasOne`
638
+ * land, for both decorator protocols. The getter itself is installed once, on the prototype, by
639
+ * {@link applyModelDecorator}; see {@link createRelationDecorator} for why the standard protocol needs
640
+ * one extra step to let that getter through.
582
641
  */
583
- function registerRelation(target, propertyKey, type, keysFactory, options) {
584
- const constructor = target.constructor;
585
- // Get or create relations array
586
- const relations = Reflect.getMetadata(RELATIONS_KEY, constructor) || [];
587
- relations.push({
588
- propertyKey,
589
- type,
590
- keysFactory,
591
- options,
592
- });
593
- Reflect.defineMetadata(RELATIONS_KEY, relations, constructor);
642
+ function registerRelation(relations, propertyKey, type, keysFactory, options) {
643
+ relations.push({ propertyKey, type, keysFactory, options });
644
+ }
645
+ /**
646
+ * Build a relation decorator that accepts BOTH protocols.
647
+ *
648
+ * Under legacy decorators a relation is declared `declare posts: Promise<Post[]>` — `declare` emits no
649
+ * field, so the prototype getter `@model` installs is what the instance sees. TC39 standard decorators
650
+ * REJECT a decorated `declare` field (TS1206), so the relation is declared `posts!: Promise<Post[]>`
651
+ * instead — and a class field IS defined on the instance (as `undefined`), which would SHADOW that
652
+ * prototype getter. The initializer below deletes the shadowing own property, so both protocols end up
653
+ * reading the exact same getter; the getter is not duplicated for the standard path.
654
+ */
655
+ function createRelationDecorator(type, keys, options) {
656
+ return function (targetOrValue, keyOrContext) {
657
+ if (isStandardContext(keyOrContext)) {
658
+ const propKey = String(keyOrContext.name);
659
+ registerRelation(standardRelationList(classMetadataBag(keyOrContext, propKey)), propKey, type, keys, options);
660
+ if (typeof keyOrContext.addInitializer !== 'function') {
661
+ throw new Error(`litedbmodel: the standard-decorator context for relation '${propKey}' has no ` +
662
+ '`addInitializer`, so the class field cannot be removed and would shadow the relation getter.');
663
+ }
664
+ keyOrContext.addInitializer(function () {
665
+ // Only under DEFINE semantics does the field land as an own property that shadows the
666
+ // prototype getter; under SET semantics the accessor's setter already swallowed it, and
667
+ // deleting a property that is not there still costs the object its shape.
668
+ if (Object.prototype.hasOwnProperty.call(this, propKey)) {
669
+ delete this[propKey];
670
+ }
671
+ });
672
+ return;
673
+ }
674
+ const propKey = String(keyOrContext);
675
+ registerRelation(legacyRelationList(targetOrValue.constructor), propKey, type, keys, options);
676
+ };
594
677
  }
595
678
  /**
596
679
  * HasMany relation decorator (1:N).
@@ -601,7 +684,7 @@ function registerRelation(target, propertyKey, type, keysFactory, options) {
601
684
  *
602
685
  * @example
603
686
  * ```typescript
604
- * // Single key relation
687
+ * // Single key relation (legacy decorators: `declare`; standard decorators: `posts!: …`)
605
688
  * @hasMany(() => [User.id, Post.author_id])
606
689
  * declare posts: Promise<Post[]>;
607
690
  *
@@ -623,9 +706,7 @@ function registerRelation(target, propertyKey, type, keysFactory, options) {
623
706
  * @category Decorators
624
707
  */
625
708
  function hasMany(keys, options) {
626
- return function (target, propertyKey) {
627
- registerRelation(target, String(propertyKey), 'hasMany', keys, options);
628
- };
709
+ return createRelationDecorator('hasMany', keys, options);
629
710
  }
630
711
  /**
631
712
  * BelongsTo relation decorator (N:1).
@@ -651,9 +732,7 @@ function hasMany(keys, options) {
651
732
  * @category Decorators
652
733
  */
653
734
  function belongsTo(keys, options) {
654
- return function (target, propertyKey) {
655
- registerRelation(target, String(propertyKey), 'belongsTo', keys, options);
656
- };
735
+ return createRelationDecorator('belongsTo', keys, options);
657
736
  }
658
737
  /**
659
738
  * HasOne relation decorator (1:1).
@@ -679,9 +758,7 @@ function belongsTo(keys, options) {
679
758
  * @category Decorators
680
759
  */
681
760
  function hasOne(keys, options) {
682
- return function (target, propertyKey) {
683
- registerRelation(target, String(propertyKey), 'hasOne', keys, options);
684
- };
761
+ return createRelationDecorator('hasOne', keys, options);
685
762
  }
686
763
  /**
687
764
  * Get relation metadata from a model class
@@ -693,16 +770,62 @@ function getRelationMeta(modelClass) {
693
770
  []);
694
771
  }
695
772
  // Implementation
696
- function model(tableNameOrConstructor, options) {
773
+ function model(tableNameOrConstructor,
774
+ // `@model('t', options)` passes ModelOptions; a bare `@model` under the standard protocol passes the
775
+ // ClassDecoratorContext in this position instead.
776
+ optionsOrContext) {
697
777
  // Called as @model('table_name') or @model('table_name', options)
698
778
  if (typeof tableNameOrConstructor === 'string') {
699
779
  const tableName = tableNameOrConstructor;
700
- return function (constructor) {
701
- return applyModelDecorator(constructor, tableName, options);
780
+ const options = optionsOrContext;
781
+ return function (constructor, context) {
782
+ return applyToClass(constructor, context, tableName, options);
783
+ };
784
+ }
785
+ // Called as `@model` without parentheses. The standard protocol invokes a CLASS decorator as
786
+ // `(value, context)`, so the second argument here is that CONTEXT, not `ModelOptions` — reading it
787
+ // as options and applying the legacy path registered a model with NO columns at all, silently.
788
+ return applyToClass(tableNameOrConstructor, optionsOrContext);
789
+ }
790
+ /**
791
+ * Apply `@model` under either decorator protocol. The ONE place the class decorator's protocol
792
+ * difference lives, so `@model` and `@model('t')` cannot diverge.
793
+ *
794
+ * Under the standard protocol the class is not finished when its decorator runs: a bundler that
795
+ * preserves names (esbuild's `--keep-names`, which `tsx` turns on) re-defines `Class.name` AFTER the
796
+ * decorator returns. Installing the static column accessors here would either be clobbered by that,
797
+ * or — since they are non-configurable — make it throw `Cannot redefine property: name` on any model
798
+ * with a `name` column. A class decorator's extra initializer is the one hook that runs after the
799
+ * class is fully formed, so the model is assembled there.
800
+ */
801
+ function applyToClass(constructor, context, tableName, options) {
802
+ const members = modelMembers(constructor, context);
803
+ if (!isStandardContext(context))
804
+ return applyModelDecorator(constructor, members, tableName, options);
805
+ // Fail-closed, exactly as `classMetadataBag` does: assembling nothing and returning the class is how
806
+ // a model ends up with no columns and no error — the defect this file exists to remove.
807
+ if (typeof context.addInitializer !== 'function') {
808
+ throw new Error(`litedbmodel: the standard-decorator context for '${constructor.name}' has no ` +
809
+ '`addInitializer`, so `@model` cannot assemble the model after the class is defined.');
810
+ }
811
+ context.addInitializer(function () {
812
+ applyModelDecorator(this, members, tableName, options);
813
+ });
814
+ return constructor;
815
+ }
816
+ /** The columns + relations `@column.*` / `@hasMany` … recorded for this class, per protocol. */
817
+ function modelMembers(constructor, context) {
818
+ if (isStandardContext(context)) {
819
+ const bag = classMetadataBag(context, String(constructor.name ?? 'model'));
820
+ return {
821
+ columns: bag[COLUMNS_KEY] ?? new Map(),
822
+ relations: bag[RELATIONS_KEY] ?? [],
702
823
  };
703
824
  }
704
- // Called as @model (without parentheses or arguments)
705
- return applyModelDecorator(tableNameOrConstructor);
825
+ return {
826
+ columns: Reflect.getMetadata(COLUMNS_KEY, constructor) ?? new Map(),
827
+ relations: Reflect.getMetadata(RELATIONS_KEY, constructor) ?? [],
828
+ };
706
829
  }
707
830
  /**
708
831
  * Check if keys are composite (array of pairs) or single pair
@@ -738,9 +861,8 @@ function parseKeys(keys) {
738
861
  /**
739
862
  * Internal function to apply the model decorator
740
863
  */
741
- function applyModelDecorator(constructor, tableName, options) {
742
- const columns = Reflect.getMetadata(COLUMNS_KEY, constructor) || new Map();
743
- const relations = Reflect.getMetadata(RELATIONS_KEY, constructor) || [];
864
+ function applyModelDecorator(constructor, members, tableName, options) {
865
+ const { columns, relations } = members;
744
866
  const modelName = constructor.name;
745
867
  // 0. Set TABLE_NAME if provided
746
868
  if (tableName) {
@@ -807,7 +929,7 @@ function applyModelDecorator(constructor, tableName, options) {
807
929
  const effectiveTableName = tableName ?? modelName.toLowerCase();
808
930
  for (const [propKey, meta] of columns) {
809
931
  Object.defineProperty(constructor, propKey, {
810
- value: (0, Column_1.createColumn)(meta.columnName, effectiveTableName, modelName, propKey, meta.sqlCast),
932
+ value: (0, Column_1.createColumn)(meta.columnName, effectiveTableName, modelName, propKey, meta.sqlCast, meta.serialize),
811
933
  writable: false,
812
934
  enumerable: true,
813
935
  configurable: false,
@@ -865,18 +987,38 @@ function applyModelDecorator(constructor, tableName, options) {
865
987
  const { sourceKeys, targetKeys, targetModelName } = parseKeys(keys);
866
988
  // Build relation config
867
989
  const order = options?.order ? (0, Column_1.orderToString)(options.order()) : null;
868
- const conditions = options?.where ? (0, Column_1.condsToRecord)(options.where()) : undefined;
869
990
  // Call internal relation method
870
991
  return this._loadRelation(type, targetModelName, {
871
992
  sourceKeys,
872
993
  targetKeys,
873
994
  order,
874
- conditions,
995
+ where: options?.where?.(),
875
996
  limit: options?.limit,
876
997
  hardLimit: options?.hardLimit,
877
998
  relationName: propertyKey,
878
999
  });
879
1000
  },
1001
+ /**
1002
+ * A relation is a getter, but a class FIELD declaring it still writes to the property. Which
1003
+ * write it is depends on `useDefineForClassFields`: with define semantics (the default from
1004
+ * target ES2022) the field is `defineProperty`-d onto the instance, bypassing this setter — the
1005
+ * standard protocol's initializer removes that shadowing property. With SET semantics (the
1006
+ * default below ES2022) the field ASSIGNS, and an accessor with no setter throws
1007
+ * `Cannot set property … which has only a getter` on EVERY instantiation.
1008
+ *
1009
+ * So: the field's own `undefined` is ignored, and any other value is honoured as an own
1010
+ * property — priming a relation (a loaded graph, a test double) reads back what was set.
1011
+ */
1012
+ set: function (value) {
1013
+ if (value === undefined)
1014
+ return;
1015
+ Object.defineProperty(this, propertyKey, {
1016
+ value,
1017
+ writable: true,
1018
+ enumerable: true,
1019
+ configurable: true,
1020
+ });
1021
+ },
880
1022
  enumerable: true,
881
1023
  configurable: false,
882
1024
  });