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.
- package/README.md +146 -86
- package/dist/Column.d.ts +30 -6
- package/dist/Column.d.ts.map +1 -1
- package/dist/Column.js +30 -10
- package/dist/Column.js.map +1 -1
- package/dist/DBModel.d.ts +28 -16
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +60 -46
- package/dist/DBModel.js.map +1 -1
- package/dist/LazyRelation.d.ts +2 -2
- package/dist/LazyRelation.js +2 -2
- package/dist/TypeCast.d.ts +2 -2
- package/dist/TypeCast.d.ts.map +1 -1
- package/dist/TypeCast.js +7 -9
- package/dist/TypeCast.js.map +1 -1
- package/dist/decorators.d.ts +128 -60
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +393 -251
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/MysqlHelper.d.ts +3 -3
- package/dist/drivers/MysqlHelper.d.ts.map +1 -1
- package/dist/drivers/MysqlHelper.js +3 -17
- package/dist/drivers/MysqlHelper.js.map +1 -1
- package/dist/drivers/MysqlSqlBuilder.d.ts.map +1 -1
- package/dist/drivers/MysqlSqlBuilder.js +2 -13
- package/dist/drivers/MysqlSqlBuilder.js.map +1 -1
- package/dist/drivers/PostgresHelper.d.ts +3 -2
- package/dist/drivers/PostgresHelper.d.ts.map +1 -1
- package/dist/drivers/PostgresHelper.js +3 -16
- package/dist/drivers/PostgresHelper.js.map +1 -1
- package/dist/drivers/PostgresSqlBuilder.d.ts.map +1 -1
- package/dist/drivers/PostgresSqlBuilder.js +2 -14
- package/dist/drivers/PostgresSqlBuilder.js.map +1 -1
- package/dist/drivers/SqliteHelper.d.ts +4 -2
- package/dist/drivers/SqliteHelper.d.ts.map +1 -1
- package/dist/drivers/SqliteHelper.js +4 -2
- package/dist/drivers/SqliteHelper.js.map +1 -1
- package/dist/drivers/SqliteSqlBuilder.d.ts.map +1 -1
- package/dist/drivers/SqliteSqlBuilder.js +2 -13
- package/dist/drivers/SqliteSqlBuilder.js.map +1 -1
- package/dist/index.cjs +861 -816
- 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 +12 -15
- package/dist/scp/decorator-adapter.d.ts.map +1 -1
- package/dist/scp/decorator-adapter.js +13 -15
- package/dist/scp/decorator-adapter.js.map +1 -1
- package/dist/scp/index.cjs +237 -233
- package/dist/scp/index.mjs +237 -233
- 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 +10 -10
- package/docs/api/classes/DBHandler.md +1 -1
- package/docs/api/classes/DBModel.md +84 -84
- 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 +6 -6
- 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 +4 -3
- 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 +2 -2
- package/docs/api/functions/formatUTCDate.md +2 -2
- package/docs/api/functions/getDBConfig.md +1 -1
- package/docs/api/functions/getDBHandler.md +1 -1
- package/docs/api/functions/getTransactionConnection.md +2 -2
- package/docs/api/functions/getTransactionContext.md +2 -2
- 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 +99 -58
- package/docs/api/interfaces/Column.md +32 -31
- 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 +14 -10
- package/docs/sql-tagged-template.md +5 -5
- package/eslint-plugin/rules/require-declare-for-relations.js +59 -4
- package/package.json +8 -7
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,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
|
-
*
|
|
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
|
+
* 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
|
|
193
|
-
* @param
|
|
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,
|
|
220
|
+
function createColumnDecorator(typeCast, serialize, sqlCast, baseSqlType) {
|
|
196
221
|
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;
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
* **
|
|
297
|
-
*
|
|
298
|
-
*
|
|
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.
|
|
313
|
-
* @column.
|
|
314
|
-
* @column.
|
|
315
|
-
* @column.
|
|
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
|
-
*
|
|
319
|
-
*
|
|
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
|
-
//
|
|
325
|
-
|
|
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: (
|
|
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
|
-
)(
|
|
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: (
|
|
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
|
-
})(
|
|
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: (
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
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, '
|
|
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
|
-
)(
|
|
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: (
|
|
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
|
-
},
|
|
406
|
-
|
|
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: (
|
|
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
|
-
},
|
|
444
|
-
|
|
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: (
|
|
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,
|
|
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: (
|
|
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,
|
|
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: (
|
|
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,
|
|
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: (
|
|
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,
|
|
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?: (
|
|
544
|
+
* @example @column.datetimeArray() event_dates?: (string | null)[];
|
|
501
545
|
*/
|
|
502
|
-
datetimeArray: (
|
|
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
|
-
})(
|
|
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: (
|
|
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,
|
|
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
|
-
},
|
|
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,
|
|
631
|
+
custom: (castFn, serializeFn, columnNameOrOptions) => createColumnDecorator(castFn, serializeFn)(columnNameOrOptions),
|
|
576
632
|
});
|
|
577
633
|
// ============================================
|
|
578
634
|
// Relation Decorators
|
|
579
635
|
// ============================================
|
|
580
636
|
/**
|
|
581
|
-
*
|
|
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(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
701
|
-
|
|
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
|
-
|
|
705
|
-
|
|
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
|
|
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
|
-
|
|
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
|
});
|