litedbmodel 2.2.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -86
- package/dist/Column.d.ts +17 -4
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js.map +1 -1
- package/dist/DBModel.d.ts +13 -14
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +20 -21
- package/dist/DBModel.js.map +1 -1
- package/dist/LazyRelation.d.ts +2 -2
- package/dist/LazyRelation.js +2 -2
- package/dist/decorators.d.ts +120 -60
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +379 -248
- package/dist/decorators.js.map +1 -1
- package/dist/index.cjs +283 -217
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scp/coltype.d.ts +14 -5
- package/dist/scp/coltype.d.ts.map +1 -1
- package/dist/scp/coltype.js +35 -36
- package/dist/scp/coltype.js.map +1 -1
- package/dist/scp/decorator-adapter.d.ts +11 -15
- package/dist/scp/decorator-adapter.d.ts.map +1 -1
- package/dist/scp/decorator-adapter.js +11 -15
- package/dist/scp/decorator-adapter.js.map +1 -1
- package/dist/scp/index.cjs +186 -193
- package/dist/scp/index.mjs +186 -193
- package/dist/scp/makesql/json-array.d.ts +6 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -1
- package/dist/scp/makesql/json-array.js +17 -2
- package/dist/scp/makesql/json-array.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +1 -3
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +8 -10
- package/dist/scp/relation.js.map +1 -1
- package/docs/BENCHMARK.md +3 -3
- package/docs/COMPARISON.md +7 -7
- package/docs/api/README.md +18 -18
- package/docs/api/classes/Conditions.md +1 -1
- package/docs/api/classes/DBHandler.md +1 -1
- package/docs/api/classes/DBModel.md +82 -82
- package/docs/api/classes/LimitExceededError.md +1 -1
- package/docs/api/classes/Middleware.md +1 -1
- package/docs/api/classes/SqlRaw.md +1 -1
- package/docs/api/classes/SqlRef.md +1 -1
- package/docs/api/classes/Values.md +1 -1
- package/docs/api/classes/WriteInReadOnlyContextError.md +1 -1
- package/docs/api/classes/WriteOutsideTransactionError.md +1 -1
- package/docs/api/functions/belongsTo.md +10 -4
- package/docs/api/functions/closeAllPools.md +1 -1
- package/docs/api/functions/condsToRecord.md +2 -2
- package/docs/api/functions/createMiddleware.md +1 -1
- package/docs/api/functions/createPostgresDriver.md +1 -1
- package/docs/api/functions/createSqliteDriver.md +1 -1
- package/docs/api/functions/dbCast.md +1 -1
- package/docs/api/functions/dbCastIn.md +1 -1
- package/docs/api/functions/dbDynamic.md +1 -1
- package/docs/api/functions/dbFalse.md +1 -1
- package/docs/api/functions/dbImmediate.md +1 -1
- package/docs/api/functions/dbIn.md +1 -1
- package/docs/api/functions/dbNotNull.md +1 -1
- package/docs/api/functions/dbNow.md +1 -1
- package/docs/api/functions/dbNull.md +1 -1
- package/docs/api/functions/dbRaw.md +1 -1
- package/docs/api/functions/dbTrue.md +1 -1
- package/docs/api/functions/dbUuid.md +1 -1
- package/docs/api/functions/dbUuidIn.md +1 -1
- package/docs/api/functions/formatLocalDate.md +1 -1
- package/docs/api/functions/formatUTCDate.md +1 -1
- package/docs/api/functions/getDBConfig.md +1 -1
- package/docs/api/functions/getDBHandler.md +1 -1
- package/docs/api/functions/getTransactionConnection.md +1 -1
- package/docs/api/functions/getTransactionContext.md +1 -1
- package/docs/api/functions/hasMany.md +11 -5
- package/docs/api/functions/hasOne.md +10 -4
- package/docs/api/functions/initDBHandler.md +1 -1
- package/docs/api/functions/isAnySqlFragment.md +1 -1
- package/docs/api/functions/isConnectionError.md +1 -1
- package/docs/api/functions/isSqlCondition.md +1 -1
- package/docs/api/functions/isSqlFragment.md +1 -1
- package/docs/api/functions/isSqlRaw.md +1 -1
- package/docs/api/functions/isSqlRef.md +1 -1
- package/docs/api/functions/isSqlTypedFragment.md +1 -1
- package/docs/api/functions/model.md +8 -155
- package/docs/api/functions/parentRef.md +1 -1
- package/docs/api/functions/sql.md +1 -1
- package/docs/api/globals.md +96 -58
- package/docs/api/interfaces/Column.md +1 -1
- package/docs/api/interfaces/ColumnDecorator.md +81 -0
- package/docs/api/interfaces/ColumnOptions.md +6 -10
- package/docs/api/interfaces/CreatedMiddlewareClass.md +1 -1
- package/docs/api/interfaces/DBConfig.md +1 -1
- package/docs/api/interfaces/DBConfigOptions.md +1 -1
- package/docs/api/interfaces/DeleteOptions.md +1 -1
- package/docs/api/interfaces/InsertOptions.md +1 -1
- package/docs/api/interfaces/LimitConfig.md +1 -1
- package/docs/api/interfaces/MiddlewareConfig.md +1 -1
- package/docs/api/interfaces/ModelClassDecorator.md +64 -0
- package/docs/api/interfaces/ModelOptions.md +1 -1
- package/docs/api/interfaces/PkeyResult.md +1 -1
- package/docs/api/interfaces/RelationDecorator.md +72 -0
- package/docs/api/interfaces/SelectOptions.md +1 -1
- package/docs/api/interfaces/SqlCondition.md +1 -1
- package/docs/api/interfaces/SqlFragment.md +1 -1
- package/docs/api/interfaces/SqlTypedFragment.md +1 -1
- package/docs/api/interfaces/TransactionOptions.md +1 -1
- package/docs/api/interfaces/UpdateManyOptions.md +1 -1
- package/docs/api/interfaces/UpdateOptions.md +1 -1
- package/docs/api-guide.md +17 -17
- package/docs/architecture.md +3 -3
- package/docs/sql-tagged-template.md +5 -5
- package/eslint-plugin/rules/require-declare-for-relations.js +59 -4
- package/package.json +11 -10
package/dist/decorators.js
CHANGED
|
@@ -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()`
|
|
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()`
|
|
38
|
-
// - `@column.datetime()`
|
|
39
|
-
// - `@column.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,196 @@ 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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
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
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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:
|
|
180
|
-
serialize:
|
|
150
|
+
typeCast: options.typeCast,
|
|
151
|
+
serialize: options.serialize,
|
|
181
152
|
primaryKey: options.primaryKey,
|
|
182
153
|
autoIncrement: options.autoIncrement,
|
|
183
|
-
sqlCast:
|
|
184
|
-
...(baseSqlType !== undefined ? { baseSqlType } : {}),
|
|
154
|
+
sqlCast: options.sqlCast,
|
|
155
|
+
...(options.baseSqlType !== undefined ? { baseSqlType: options.baseSqlType } : {}),
|
|
185
156
|
});
|
|
186
|
-
|
|
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
|
-
*
|
|
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
|
+
* Build one `@column.*` family decorator. The returned decorator accepts BOTH protocols: legacy hands
|
|
196
|
+
* it `(prototype, propertyKey)`, TC39 standard hands it `(undefined, context)`. Everything that
|
|
197
|
+
* describes the COLUMN (cast, serializer, SQL family) is fixed here by the family itself — nothing is
|
|
198
|
+
* read back from the compiler, so every toolchain produces the same model.
|
|
199
|
+
*
|
|
190
200
|
* @param typeCast - Function to convert DB value to JS value (read)
|
|
191
201
|
* @param serialize - Function to convert JS value to DB value (write)
|
|
192
|
-
* @param
|
|
193
|
-
* @param
|
|
202
|
+
* @param sqlCast - SQL type family for casting in conditions / typed reads (e.g. `'uuid'`)
|
|
203
|
+
* @param baseSqlType - §4.1 SQL-type token for a family that applies no cast (`@column.text()`)
|
|
194
204
|
*/
|
|
195
|
-
function createColumnDecorator(typeCast, serialize,
|
|
205
|
+
function createColumnDecorator(typeCast, serialize, sqlCast, baseSqlType) {
|
|
196
206
|
return function (columnNameOrOptions) {
|
|
197
|
-
return function (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
207
|
+
return function (targetOrValue, keyOrContext) {
|
|
208
|
+
if (isStandardContext(keyOrContext)) {
|
|
209
|
+
const propKey = String(keyOrContext.name);
|
|
210
|
+
const parsed = parseColumnOptions(columnNameOrOptions, propKey);
|
|
211
|
+
registerColumn(standardColumnMap(classMetadataBag(keyOrContext, propKey)), propKey, {
|
|
212
|
+
...parsed, typeCast, serialize, sqlCast, baseSqlType,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
215
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
serialize,
|
|
220
|
-
skipAutoInfer: shouldSkipAutoInfer,
|
|
221
|
-
primaryKey,
|
|
222
|
-
autoIncrement,
|
|
223
|
-
sqlCast,
|
|
216
|
+
const propKey = String(keyOrContext);
|
|
217
|
+
const parsed = parseColumnOptions(columnNameOrOptions, propKey);
|
|
218
|
+
registerColumn(legacyColumnMap(targetOrValue.constructor), propKey, {
|
|
219
|
+
...parsed, typeCast, serialize, sqlCast, baseSqlType,
|
|
224
220
|
});
|
|
225
221
|
};
|
|
226
222
|
};
|
|
227
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* The bare `@column()` — REMOVED. It declared no type: it relied on `design:type`, which only exists
|
|
226
|
+
* under legacy decorators AND `emitDecoratorMetadata`, which esbuild (tsx / vite / vitest) does not
|
|
227
|
+
* implement and TC39 standard decorators do not have. Where that metadata was absent the column
|
|
228
|
+
* silently got NO cast and the raw driver value reached the caller — an `integer` read back as a
|
|
229
|
+
* `bigint`, a `numeric` as a `string` (issue #286). A column states its own type instead.
|
|
230
|
+
*/
|
|
231
|
+
function requireColumnFamily() {
|
|
232
|
+
throw new Error('litedbmodel: `@column()` no longer declares a column — a column must state its type with a ' +
|
|
233
|
+
'family: @column.text() / .number() / .boolean() / .bigint() / .datetime() / .date() / .uuid() / ' +
|
|
234
|
+
'.json() / .stringArray() / .intArray() / .numericArray() / .booleanArray() / .datetimeArray() / ' +
|
|
235
|
+
'.passthrough() / .custom(). Every family takes the same argument as `@column()` did ' +
|
|
236
|
+
"(`@column.text('db_name')`, `@column.number({ primaryKey: true })`). The bare form inferred the " +
|
|
237
|
+
'type from `emitDecoratorMetadata`, which esbuild (tsx/vite/vitest) never emits and standard ' +
|
|
238
|
+
'decorators do not have, so it silently produced untyped reads.');
|
|
239
|
+
}
|
|
228
240
|
/**
|
|
229
241
|
* Serialize array using driver's type cast if available.
|
|
230
242
|
* Falls back to PostgreSQL format for backward compatibility.
|
|
@@ -293,85 +305,107 @@ function serializeJson(val, typeCast) {
|
|
|
293
305
|
/**
|
|
294
306
|
* Column decorator for defining model properties.
|
|
295
307
|
*
|
|
296
|
-
* **
|
|
297
|
-
*
|
|
298
|
-
*
|
|
308
|
+
* **A column declares its own type with a family** — the family decides both the TypeScript type the
|
|
309
|
+
* model declares and the value `find()` gives back, on every toolchain and under either decorator
|
|
310
|
+
* protocol. Nothing is inferred from the TypeScript type annotation.
|
|
299
311
|
*
|
|
300
|
-
* Auto-inferred types:
|
|
301
312
|
* ```typescript
|
|
302
|
-
* @column() id?: number;
|
|
303
|
-
* @column() name?: string;
|
|
304
|
-
* @column()
|
|
305
|
-
* @column()
|
|
306
|
-
* @column()
|
|
307
|
-
* @column(
|
|
313
|
+
* @column.number({ primaryKey: true, autoIncrement: true }) id?: number;
|
|
314
|
+
* @column.text() name?: string; // TEXT / VARCHAR / CHAR / ENUM — no cast needed
|
|
315
|
+
* @column.text('custom_name') prop?: string; // custom column name
|
|
316
|
+
* @column.boolean() is_active?: boolean;
|
|
317
|
+
* @column.datetime() created_at?: string; // TZ-attached string, NOT a JS Date
|
|
318
|
+
* @column.date() birth_date?: string; // 'YYYY-MM-DD'
|
|
319
|
+
* @column.bigint() large_id?: bigint; // bc `int` — a JS bigint
|
|
320
|
+
* @column.uuid() ext_id?: string;
|
|
321
|
+
* @column.stringArray() tags?: string[];
|
|
322
|
+
* @column.intArray() scores?: number[];
|
|
323
|
+
* @column.json<MyType>() data?: MyType;
|
|
324
|
+
* @column.date() birth_date?: string; // 'YYYY-MM-DD' — not a datetime
|
|
325
|
+
* @column.passthrough() payload?: unknown; // BYTEA / BLOB — no cast exists
|
|
326
|
+
* @column.custom((v) => String(v).toUpperCase()) status?: string;
|
|
308
327
|
* ```
|
|
309
328
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* @column.json<MyType>() data?: MyType; // Generic type unknown
|
|
315
|
-
* @column.date() birth_date?: string; // date vs datetime distinction
|
|
316
|
-
* ```
|
|
317
|
-
*
|
|
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.
|
|
329
|
+
* Every family takes what the removed bare `@column()` took: a column name, or {@link ColumnOptions}.
|
|
330
|
+
* The bare form inferred the type from `emitDecoratorMetadata`, which esbuild (tsx / vite / vitest)
|
|
331
|
+
* never emits and TC39 standard decorators do not have, so it silently produced untyped reads; it now
|
|
332
|
+
* reports the migration instead (see {@link requireColumnFamily}).
|
|
320
333
|
*
|
|
321
334
|
* @category Decorators
|
|
322
335
|
*/
|
|
323
336
|
exports.column = Object.assign(
|
|
324
|
-
//
|
|
325
|
-
|
|
337
|
+
// The bare `@column()` declared no type — it is rejected, with the migration in the message.
|
|
338
|
+
(_columnNameOrOptions) => requireColumnFamily(), {
|
|
326
339
|
// ============================================
|
|
327
340
|
// Primitive Types
|
|
328
341
|
// ============================================
|
|
342
|
+
/**
|
|
343
|
+
* Text type (TEXT / VARCHAR / CHAR / ENUM / citext) — the driver value is already a string, so no
|
|
344
|
+
* cast is applied. The family still DECLARES the column so the typed read types it as `TEXT`.
|
|
345
|
+
* @example @column.text() name?: string;
|
|
346
|
+
* @example @column.text('user_name') name?: string;
|
|
347
|
+
*/
|
|
348
|
+
text: (columnNameOrOptions) => createColumnDecorator(
|
|
349
|
+
// The family DECLARES `string`, so it must hand back one. Most drivers already do, but SQLite
|
|
350
|
+
// is dynamically typed: it returns a JS number for a `NUMERIC`/`DECIMAL` column whatever the
|
|
351
|
+
// declaration says, and the declared type would be a lie on that dialect alone.
|
|
352
|
+
(v) => {
|
|
353
|
+
if (v === undefined || v === null)
|
|
354
|
+
return v;
|
|
355
|
+
if (typeof v === 'string')
|
|
356
|
+
return v;
|
|
357
|
+
if (typeof v === 'number' || typeof v === 'bigint' || typeof v === 'boolean')
|
|
358
|
+
return String(v);
|
|
359
|
+
throw new Error(`materialize text: driver returned ${typeof v} for a text column. Use @column.passthrough() ` +
|
|
360
|
+
'for a BYTEA/BLOB column, or @column.json<T>() for JSON.');
|
|
361
|
+
}, (val) => rejectNonText('text', val), // …and it cannot SERIALIZE a Date or an object
|
|
362
|
+
undefined, // no sqlCast family (a text literal needs no SQL cast)
|
|
363
|
+
'TEXT' // §4.1 token: types the SCP typed read
|
|
364
|
+
)(columnNameOrOptions),
|
|
329
365
|
/**
|
|
330
366
|
* Boolean type conversion
|
|
331
367
|
* Converts 't'/'f', 'true'/'false', 1/0 to boolean
|
|
332
368
|
* Preserves null for nullable columns, undefined stays undefined
|
|
333
369
|
* @example @column.boolean() is_active?: boolean;
|
|
334
370
|
*/
|
|
335
|
-
boolean: (
|
|
371
|
+
boolean: (columnNameOrOptions) => createColumnDecorator(
|
|
336
372
|
// v2 read contract: BOOLEAN → boolean, via the shared materializer (issue #9).
|
|
337
373
|
(v) => {
|
|
338
374
|
if (v === undefined)
|
|
339
375
|
return undefined;
|
|
340
376
|
return (0, coltype_1.materializeCell)(v, 'bool');
|
|
341
377
|
}, undefined, // no custom serialize
|
|
342
|
-
false, // allow auto-inference
|
|
343
378
|
'boolean' // sqlCast for updateMany type inference
|
|
344
|
-
)(
|
|
379
|
+
)(columnNameOrOptions),
|
|
345
380
|
/**
|
|
346
381
|
* Number type conversion (from string)
|
|
347
382
|
* Preserves null for nullable columns, undefined stays undefined
|
|
348
383
|
* @example @column.number() amount?: number;
|
|
349
384
|
*/
|
|
350
|
-
number: (
|
|
385
|
+
number: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
351
386
|
if (v === undefined)
|
|
352
387
|
return undefined;
|
|
353
388
|
if (v === null)
|
|
354
389
|
return null;
|
|
355
390
|
const n = Number(v);
|
|
356
391
|
return isNaN(n) ? null : n;
|
|
357
|
-
})(
|
|
392
|
+
})(columnNameOrOptions),
|
|
358
393
|
/**
|
|
359
394
|
* BigInt type conversion
|
|
360
395
|
* Preserves null for nullable columns, undefined stays undefined
|
|
361
|
-
* @example @column.bigint() large_id?: bigint;
|
|
396
|
+
* @example @column.bigint() large_id?: bigint; // bc `int` — a JS bigint, checked i64
|
|
362
397
|
*/
|
|
363
|
-
bigint: (
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
398
|
+
bigint: (columnNameOrOptions) => createColumnDecorator(
|
|
399
|
+
// An integer column reads back as a JS `bigint` — behavior-contracts' `int` value model
|
|
400
|
+
// (`int` = bigint, checked i64), which is what the drivers already hand over. `JSON.stringify`
|
|
401
|
+
// does not serialize a bigint: give the field a `toJSON`, or convert at the boundary.
|
|
367
402
|
(v) => {
|
|
368
403
|
if (v === undefined)
|
|
369
404
|
return undefined;
|
|
370
|
-
return (0, coltype_1.materializeCell)(v, '
|
|
405
|
+
return (0, coltype_1.materializeCell)(v, 'int');
|
|
371
406
|
}, undefined, // no custom serialize
|
|
372
|
-
false, // allow auto-inference
|
|
373
407
|
'bigint' // sqlCast for WHERE/INSERT type casting
|
|
374
|
-
)(
|
|
408
|
+
)(columnNameOrOptions),
|
|
375
409
|
// ============================================
|
|
376
410
|
// Date/Time Types
|
|
377
411
|
// ============================================
|
|
@@ -383,9 +417,9 @@ createColumnDecorator(), {
|
|
|
383
417
|
* - PostgreSQL: Serializes to ISO 8601 UTC string with 'Z' suffix for explicit timezone
|
|
384
418
|
* - MySQL/SQLite: Passes Date object to driver (driver-dependent timezone handling)
|
|
385
419
|
*
|
|
386
|
-
* @example @column.datetime() created_at?: Date
|
|
420
|
+
* @example @column.datetime() created_at?: string; // TZ-attached string, NOT a JS Date
|
|
387
421
|
*/
|
|
388
|
-
datetime: (
|
|
422
|
+
datetime: (columnNameOrOptions) => createColumnDecorator(
|
|
389
423
|
// v2 read contract: DATE/TIMESTAMP/TIMESTAMPTZ/DATETIME/TIME → TZ-attached STRING (NOT a
|
|
390
424
|
// TZ-shifted JS Date), via the shared materializer (issue #9). A driver already returning the
|
|
391
425
|
// native textual form passes through; a JS Date is rendered to its lossless ISO instant.
|
|
@@ -402,9 +436,8 @@ createColumnDecorator(), {
|
|
|
402
436
|
return typeCast?.serializeDatetime(val) ?? val;
|
|
403
437
|
}
|
|
404
438
|
return val;
|
|
405
|
-
},
|
|
406
|
-
|
|
407
|
-
)(columnName),
|
|
439
|
+
}, 'timestamp' // sqlCast for updateMany type inference
|
|
440
|
+
)(columnNameOrOptions),
|
|
408
441
|
/**
|
|
409
442
|
* Date type conversion — returns YYYY-MM-DD string.
|
|
410
443
|
* Preserves null for nullable columns, undefined stays undefined.
|
|
@@ -412,9 +445,9 @@ createColumnDecorator(), {
|
|
|
412
445
|
* DB values (Date object or string) are normalized to 'YYYY-MM-DD' string.
|
|
413
446
|
* On write, string values are passed through; Date objects are formatted as 'YYYY-MM-DD'.
|
|
414
447
|
*
|
|
415
|
-
* @example @column.date() birth_date?: string;
|
|
448
|
+
* @example @column.date() birth_date?: string; // 'YYYY-MM-DD'
|
|
416
449
|
*/
|
|
417
|
-
date: (
|
|
450
|
+
date: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
418
451
|
if (v === undefined)
|
|
419
452
|
return undefined;
|
|
420
453
|
if (v === null)
|
|
@@ -440,9 +473,8 @@ createColumnDecorator(), {
|
|
|
440
473
|
return isNaN(val.getTime()) ? null : (0, TypeCast_1.formatLocalDate)(val);
|
|
441
474
|
}
|
|
442
475
|
return val;
|
|
443
|
-
},
|
|
444
|
-
|
|
445
|
-
)(columnName),
|
|
476
|
+
}, 'date' // sqlCast for updateMany type inference
|
|
477
|
+
)(columnNameOrOptions),
|
|
446
478
|
// ============================================
|
|
447
479
|
// Array Types
|
|
448
480
|
// ============================================
|
|
@@ -451,60 +483,65 @@ createColumnDecorator(), {
|
|
|
451
483
|
* Preserves null for nullable columns, undefined stays undefined
|
|
452
484
|
* @example @column.stringArray() tags?: string[];
|
|
453
485
|
*/
|
|
454
|
-
stringArray: (
|
|
486
|
+
stringArray: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
455
487
|
if (v === undefined)
|
|
456
488
|
return undefined;
|
|
457
489
|
if (v === null)
|
|
458
490
|
return null;
|
|
459
491
|
return (0, TypeCast_1.castToStringArray)(v);
|
|
460
|
-
}, serializeArray,
|
|
492
|
+
}, serializeArray, 'text[]')(columnNameOrOptions),
|
|
461
493
|
/**
|
|
462
494
|
* Integer array type conversion (integer[])
|
|
463
495
|
* Preserves null for nullable columns, undefined stays undefined
|
|
464
496
|
* @example @column.intArray() scores?: number[];
|
|
465
497
|
*/
|
|
466
|
-
intArray: (
|
|
498
|
+
intArray: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
467
499
|
if (v === undefined)
|
|
468
500
|
return undefined;
|
|
469
501
|
if (v === null)
|
|
470
502
|
return null;
|
|
471
503
|
return (0, TypeCast_1.castToIntegerArray)(v);
|
|
472
|
-
}, serializeArray,
|
|
504
|
+
}, serializeArray, 'int[]')(columnNameOrOptions),
|
|
473
505
|
/**
|
|
474
506
|
* Numeric array type conversion (numeric[], allows null elements)
|
|
475
507
|
* Preserves null for nullable columns, undefined stays undefined
|
|
476
508
|
* @example @column.numericArray() values?: (number | null)[];
|
|
477
509
|
*/
|
|
478
|
-
numericArray: (
|
|
510
|
+
numericArray: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
479
511
|
if (v === undefined)
|
|
480
512
|
return undefined;
|
|
481
513
|
if (v === null)
|
|
482
514
|
return null;
|
|
483
515
|
return (0, TypeCast_1.castToNumericArray)(v);
|
|
484
|
-
}, serializeArray,
|
|
516
|
+
}, serializeArray, 'numeric[]')(columnNameOrOptions),
|
|
485
517
|
/**
|
|
486
518
|
* Boolean array type conversion (boolean[])
|
|
487
519
|
* Preserves null for nullable columns, undefined stays undefined
|
|
488
520
|
* @example @column.booleanArray() flags?: (boolean | null)[];
|
|
489
521
|
*/
|
|
490
|
-
booleanArray: (
|
|
522
|
+
booleanArray: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
491
523
|
if (v === undefined)
|
|
492
524
|
return undefined;
|
|
493
525
|
if (v === null)
|
|
494
526
|
return null;
|
|
495
527
|
return (0, TypeCast_1.castToBooleanArray)(v);
|
|
496
|
-
}, serializeBooleanArray,
|
|
528
|
+
}, serializeBooleanArray, 'boolean[]')(columnNameOrOptions),
|
|
497
529
|
/**
|
|
498
530
|
* DateTime array type conversion (timestamp[])
|
|
499
531
|
* Preserves null for nullable columns, undefined stays undefined
|
|
500
|
-
* @example @column.datetimeArray() event_dates?: (
|
|
532
|
+
* @example @column.datetimeArray() event_dates?: (string | null)[];
|
|
501
533
|
*/
|
|
502
|
-
datetimeArray: (
|
|
534
|
+
datetimeArray: (columnNameOrOptions) => createColumnDecorator(
|
|
535
|
+
// Element-wise the SAME contract as `@column.datetime()`: a TZ-attached STRING, never a
|
|
536
|
+
// TZ-shifted JS Date. The array family used to be the one place a `Date` still came back, so
|
|
537
|
+
// `JSON.stringify` of a row silently re-rendered those elements in UTC — the exact hazard #9
|
|
538
|
+
// closed for the scalar.
|
|
539
|
+
(v) => {
|
|
503
540
|
if (v === undefined)
|
|
504
541
|
return undefined;
|
|
505
542
|
if (v === null)
|
|
506
543
|
return null;
|
|
507
|
-
return (0, TypeCast_1.castToDatetimeArray)(v);
|
|
544
|
+
return (0, TypeCast_1.castToDatetimeArray)(v).map((d) => (d === null ? null : (0, coltype_1.materializeCell)(d, 'date')));
|
|
508
545
|
},
|
|
509
546
|
// DateTime arrays serialize each Date to ISO string
|
|
510
547
|
(val) => {
|
|
@@ -514,7 +551,7 @@ createColumnDecorator(), {
|
|
|
514
551
|
return val;
|
|
515
552
|
const mapped = val.map(v => v === null ? 'NULL' : (v instanceof Date ? v.toISOString() : String(v)));
|
|
516
553
|
return `{${mapped.join(',')}}`;
|
|
517
|
-
})(
|
|
554
|
+
})(columnNameOrOptions),
|
|
518
555
|
// ============================================
|
|
519
556
|
// JSON Types
|
|
520
557
|
// ============================================
|
|
@@ -524,13 +561,13 @@ createColumnDecorator(), {
|
|
|
524
561
|
* @example @column.json() metadata?: Record<string, unknown>;
|
|
525
562
|
* @example @column.json<UserSettings>() settings?: UserSettings;
|
|
526
563
|
*/
|
|
527
|
-
json: (
|
|
564
|
+
json: (columnNameOrOptions) => createColumnDecorator((v) => {
|
|
528
565
|
if (v === undefined)
|
|
529
566
|
return undefined;
|
|
530
567
|
if (v === null)
|
|
531
568
|
return null;
|
|
532
569
|
return (0, TypeCast_1.castToJson)(v);
|
|
533
|
-
}, serializeJson,
|
|
570
|
+
}, serializeJson, 'jsonb')(columnNameOrOptions),
|
|
534
571
|
// ============================================
|
|
535
572
|
// UUID Type (PostgreSQL)
|
|
536
573
|
// ============================================
|
|
@@ -560,37 +597,71 @@ createColumnDecorator(), {
|
|
|
560
597
|
return null;
|
|
561
598
|
// UUID values from DB are typically already strings
|
|
562
599
|
return String(v);
|
|
563
|
-
},
|
|
564
|
-
true, // Skip auto-inference
|
|
600
|
+
}, (val) => rejectNonText('uuid', val), // the ::uuid cast handles the rest
|
|
565
601
|
'uuid' // SQL type for casting
|
|
566
602
|
)(columnNameOrOptions),
|
|
567
603
|
// ============================================
|
|
568
604
|
// Custom Type Conversion
|
|
569
605
|
// ============================================
|
|
606
|
+
/**
|
|
607
|
+
* The driver's value, UNCAST — for a column whose type the library has no cast for (`BYTEA` /
|
|
608
|
+
* `BLOB` arrives as a driver Buffer, a vendor type as whatever the driver decodes). It states
|
|
609
|
+
* "this column is passed through", which is a DECLARATION; the removed bare `@column()` was the
|
|
610
|
+
* absence of one, and produced this same passthrough by accident for every unresolvable type.
|
|
611
|
+
* @example @column.passthrough() payload?: unknown;
|
|
612
|
+
*/
|
|
613
|
+
passthrough: (columnNameOrOptions) => createColumnDecorator()(columnNameOrOptions),
|
|
570
614
|
/**
|
|
571
615
|
* Custom type conversion with user-provided function
|
|
572
616
|
* @example @column.custom((v) => String(v).toUpperCase()) status?: string;
|
|
573
617
|
* @example @column.custom((v) => v, (v) => JSON.stringify(v)) data?: MyType; // with serializer
|
|
574
618
|
*/
|
|
575
|
-
custom: (castFn, serializeFn,
|
|
619
|
+
custom: (castFn, serializeFn, columnNameOrOptions) => createColumnDecorator(castFn, serializeFn)(columnNameOrOptions),
|
|
576
620
|
});
|
|
577
621
|
// ============================================
|
|
578
622
|
// Relation Decorators
|
|
579
623
|
// ============================================
|
|
580
624
|
/**
|
|
581
|
-
*
|
|
625
|
+
* Record one relation on the class being defined — the ONE place `@hasMany` / `@belongsTo` / `@hasOne`
|
|
626
|
+
* land, for both decorator protocols. The getter itself is installed once, on the prototype, by
|
|
627
|
+
* {@link applyModelDecorator}; see {@link createRelationDecorator} for why the standard protocol needs
|
|
628
|
+
* one extra step to let that getter through.
|
|
582
629
|
*/
|
|
583
|
-
function registerRelation(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
630
|
+
function registerRelation(relations, propertyKey, type, keysFactory, options) {
|
|
631
|
+
relations.push({ propertyKey, type, keysFactory, options });
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Build a relation decorator that accepts BOTH protocols.
|
|
635
|
+
*
|
|
636
|
+
* Under legacy decorators a relation is declared `declare posts: Promise<Post[]>` — `declare` emits no
|
|
637
|
+
* field, so the prototype getter `@model` installs is what the instance sees. TC39 standard decorators
|
|
638
|
+
* REJECT a decorated `declare` field (TS1206), so the relation is declared `posts!: Promise<Post[]>`
|
|
639
|
+
* instead — and a class field IS defined on the instance (as `undefined`), which would SHADOW that
|
|
640
|
+
* prototype getter. The initializer below deletes the shadowing own property, so both protocols end up
|
|
641
|
+
* reading the exact same getter; the getter is not duplicated for the standard path.
|
|
642
|
+
*/
|
|
643
|
+
function createRelationDecorator(type, keys, options) {
|
|
644
|
+
return function (targetOrValue, keyOrContext) {
|
|
645
|
+
if (isStandardContext(keyOrContext)) {
|
|
646
|
+
const propKey = String(keyOrContext.name);
|
|
647
|
+
registerRelation(standardRelationList(classMetadataBag(keyOrContext, propKey)), propKey, type, keys, options);
|
|
648
|
+
if (typeof keyOrContext.addInitializer !== 'function') {
|
|
649
|
+
throw new Error(`litedbmodel: the standard-decorator context for relation '${propKey}' has no ` +
|
|
650
|
+
'`addInitializer`, so the class field cannot be removed and would shadow the relation getter.');
|
|
651
|
+
}
|
|
652
|
+
keyOrContext.addInitializer(function () {
|
|
653
|
+
// Only under DEFINE semantics does the field land as an own property that shadows the
|
|
654
|
+
// prototype getter; under SET semantics the accessor's setter already swallowed it, and
|
|
655
|
+
// deleting a property that is not there still costs the object its shape.
|
|
656
|
+
if (Object.prototype.hasOwnProperty.call(this, propKey)) {
|
|
657
|
+
delete this[propKey];
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const propKey = String(keyOrContext);
|
|
663
|
+
registerRelation(legacyRelationList(targetOrValue.constructor), propKey, type, keys, options);
|
|
664
|
+
};
|
|
594
665
|
}
|
|
595
666
|
/**
|
|
596
667
|
* HasMany relation decorator (1:N).
|
|
@@ -601,7 +672,7 @@ function registerRelation(target, propertyKey, type, keysFactory, options) {
|
|
|
601
672
|
*
|
|
602
673
|
* @example
|
|
603
674
|
* ```typescript
|
|
604
|
-
* // Single key relation
|
|
675
|
+
* // Single key relation (legacy decorators: `declare`; standard decorators: `posts!: …`)
|
|
605
676
|
* @hasMany(() => [User.id, Post.author_id])
|
|
606
677
|
* declare posts: Promise<Post[]>;
|
|
607
678
|
*
|
|
@@ -623,9 +694,7 @@ function registerRelation(target, propertyKey, type, keysFactory, options) {
|
|
|
623
694
|
* @category Decorators
|
|
624
695
|
*/
|
|
625
696
|
function hasMany(keys, options) {
|
|
626
|
-
return
|
|
627
|
-
registerRelation(target, String(propertyKey), 'hasMany', keys, options);
|
|
628
|
-
};
|
|
697
|
+
return createRelationDecorator('hasMany', keys, options);
|
|
629
698
|
}
|
|
630
699
|
/**
|
|
631
700
|
* BelongsTo relation decorator (N:1).
|
|
@@ -651,9 +720,7 @@ function hasMany(keys, options) {
|
|
|
651
720
|
* @category Decorators
|
|
652
721
|
*/
|
|
653
722
|
function belongsTo(keys, options) {
|
|
654
|
-
return
|
|
655
|
-
registerRelation(target, String(propertyKey), 'belongsTo', keys, options);
|
|
656
|
-
};
|
|
723
|
+
return createRelationDecorator('belongsTo', keys, options);
|
|
657
724
|
}
|
|
658
725
|
/**
|
|
659
726
|
* HasOne relation decorator (1:1).
|
|
@@ -679,9 +746,7 @@ function belongsTo(keys, options) {
|
|
|
679
746
|
* @category Decorators
|
|
680
747
|
*/
|
|
681
748
|
function hasOne(keys, options) {
|
|
682
|
-
return
|
|
683
|
-
registerRelation(target, String(propertyKey), 'hasOne', keys, options);
|
|
684
|
-
};
|
|
749
|
+
return createRelationDecorator('hasOne', keys, options);
|
|
685
750
|
}
|
|
686
751
|
/**
|
|
687
752
|
* Get relation metadata from a model class
|
|
@@ -693,16 +758,62 @@ function getRelationMeta(modelClass) {
|
|
|
693
758
|
[]);
|
|
694
759
|
}
|
|
695
760
|
// Implementation
|
|
696
|
-
function model(tableNameOrConstructor,
|
|
761
|
+
function model(tableNameOrConstructor,
|
|
762
|
+
// `@model('t', options)` passes ModelOptions; a bare `@model` under the standard protocol passes the
|
|
763
|
+
// ClassDecoratorContext in this position instead.
|
|
764
|
+
optionsOrContext) {
|
|
697
765
|
// Called as @model('table_name') or @model('table_name', options)
|
|
698
766
|
if (typeof tableNameOrConstructor === 'string') {
|
|
699
767
|
const tableName = tableNameOrConstructor;
|
|
700
|
-
|
|
701
|
-
|
|
768
|
+
const options = optionsOrContext;
|
|
769
|
+
return function (constructor, context) {
|
|
770
|
+
return applyToClass(constructor, context, tableName, options);
|
|
702
771
|
};
|
|
703
772
|
}
|
|
704
|
-
// Called as
|
|
705
|
-
|
|
773
|
+
// Called as `@model` without parentheses. The standard protocol invokes a CLASS decorator as
|
|
774
|
+
// `(value, context)`, so the second argument here is that CONTEXT, not `ModelOptions` — reading it
|
|
775
|
+
// as options and applying the legacy path registered a model with NO columns at all, silently.
|
|
776
|
+
return applyToClass(tableNameOrConstructor, optionsOrContext);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Apply `@model` under either decorator protocol. The ONE place the class decorator's protocol
|
|
780
|
+
* difference lives, so `@model` and `@model('t')` cannot diverge.
|
|
781
|
+
*
|
|
782
|
+
* Under the standard protocol the class is not finished when its decorator runs: a bundler that
|
|
783
|
+
* preserves names (esbuild's `--keep-names`, which `tsx` turns on) re-defines `Class.name` AFTER the
|
|
784
|
+
* decorator returns. Installing the static column accessors here would either be clobbered by that,
|
|
785
|
+
* or — since they are non-configurable — make it throw `Cannot redefine property: name` on any model
|
|
786
|
+
* with a `name` column. A class decorator's extra initializer is the one hook that runs after the
|
|
787
|
+
* class is fully formed, so the model is assembled there.
|
|
788
|
+
*/
|
|
789
|
+
function applyToClass(constructor, context, tableName, options) {
|
|
790
|
+
const members = modelMembers(constructor, context);
|
|
791
|
+
if (!isStandardContext(context))
|
|
792
|
+
return applyModelDecorator(constructor, members, tableName, options);
|
|
793
|
+
// Fail-closed, exactly as `classMetadataBag` does: assembling nothing and returning the class is how
|
|
794
|
+
// a model ends up with no columns and no error — the defect this file exists to remove.
|
|
795
|
+
if (typeof context.addInitializer !== 'function') {
|
|
796
|
+
throw new Error(`litedbmodel: the standard-decorator context for '${constructor.name}' has no ` +
|
|
797
|
+
'`addInitializer`, so `@model` cannot assemble the model after the class is defined.');
|
|
798
|
+
}
|
|
799
|
+
context.addInitializer(function () {
|
|
800
|
+
applyModelDecorator(this, members, tableName, options);
|
|
801
|
+
});
|
|
802
|
+
return constructor;
|
|
803
|
+
}
|
|
804
|
+
/** The columns + relations `@column.*` / `@hasMany` … recorded for this class, per protocol. */
|
|
805
|
+
function modelMembers(constructor, context) {
|
|
806
|
+
if (isStandardContext(context)) {
|
|
807
|
+
const bag = classMetadataBag(context, String(constructor.name ?? 'model'));
|
|
808
|
+
return {
|
|
809
|
+
columns: bag[COLUMNS_KEY] ?? new Map(),
|
|
810
|
+
relations: bag[RELATIONS_KEY] ?? [],
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
columns: Reflect.getMetadata(COLUMNS_KEY, constructor) ?? new Map(),
|
|
815
|
+
relations: Reflect.getMetadata(RELATIONS_KEY, constructor) ?? [],
|
|
816
|
+
};
|
|
706
817
|
}
|
|
707
818
|
/**
|
|
708
819
|
* Check if keys are composite (array of pairs) or single pair
|
|
@@ -738,9 +849,8 @@ function parseKeys(keys) {
|
|
|
738
849
|
/**
|
|
739
850
|
* Internal function to apply the model decorator
|
|
740
851
|
*/
|
|
741
|
-
function applyModelDecorator(constructor, tableName, options) {
|
|
742
|
-
const columns
|
|
743
|
-
const relations = Reflect.getMetadata(RELATIONS_KEY, constructor) || [];
|
|
852
|
+
function applyModelDecorator(constructor, members, tableName, options) {
|
|
853
|
+
const { columns, relations } = members;
|
|
744
854
|
const modelName = constructor.name;
|
|
745
855
|
// 0. Set TABLE_NAME if provided
|
|
746
856
|
if (tableName) {
|
|
@@ -877,6 +987,27 @@ function applyModelDecorator(constructor, tableName, options) {
|
|
|
877
987
|
relationName: propertyKey,
|
|
878
988
|
});
|
|
879
989
|
},
|
|
990
|
+
/**
|
|
991
|
+
* A relation is a getter, but a class FIELD declaring it still writes to the property. Which
|
|
992
|
+
* write it is depends on `useDefineForClassFields`: with define semantics (the default from
|
|
993
|
+
* target ES2022) the field is `defineProperty`-d onto the instance, bypassing this setter — the
|
|
994
|
+
* standard protocol's initializer removes that shadowing property. With SET semantics (the
|
|
995
|
+
* default below ES2022) the field ASSIGNS, and an accessor with no setter throws
|
|
996
|
+
* `Cannot set property … which has only a getter` on EVERY instantiation.
|
|
997
|
+
*
|
|
998
|
+
* So: the field's own `undefined` is ignored, and any other value is honoured as an own
|
|
999
|
+
* property — priming a relation (a loaded graph, a test double) reads back what was set.
|
|
1000
|
+
*/
|
|
1001
|
+
set: function (value) {
|
|
1002
|
+
if (value === undefined)
|
|
1003
|
+
return;
|
|
1004
|
+
Object.defineProperty(this, propertyKey, {
|
|
1005
|
+
value,
|
|
1006
|
+
writable: true,
|
|
1007
|
+
enumerable: true,
|
|
1008
|
+
configurable: true,
|
|
1009
|
+
});
|
|
1010
|
+
},
|
|
880
1011
|
enumerable: true,
|
|
881
1012
|
configurable: false,
|
|
882
1013
|
});
|