metal-orm 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2352 -226
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +605 -40
  5. package/dist/index.d.ts +605 -40
  6. package/dist/index.js +2324 -226
  7. package/dist/index.js.map +1 -1
  8. package/package.json +22 -17
  9. package/src/bulk/bulk-context.ts +83 -0
  10. package/src/bulk/bulk-delete-executor.ts +89 -0
  11. package/src/bulk/bulk-executor.base.ts +73 -0
  12. package/src/bulk/bulk-insert-executor.ts +74 -0
  13. package/src/bulk/bulk-types.ts +70 -0
  14. package/src/bulk/bulk-update-executor.ts +192 -0
  15. package/src/bulk/bulk-upsert-executor.ts +95 -0
  16. package/src/bulk/bulk-utils.ts +91 -0
  17. package/src/bulk/index.ts +18 -0
  18. package/src/codegen/typescript.ts +30 -21
  19. package/src/core/ast/expression-builders.ts +107 -10
  20. package/src/core/ast/expression-nodes.ts +52 -22
  21. package/src/core/ast/expression-visitor.ts +23 -13
  22. package/src/core/dialect/abstract.ts +30 -17
  23. package/src/core/dialect/mysql/index.ts +20 -5
  24. package/src/core/execution/db-executor.ts +96 -64
  25. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  26. package/src/core/execution/executors/mssql-executor.ts +66 -34
  27. package/src/core/execution/executors/mysql-executor.ts +98 -66
  28. package/src/core/execution/executors/postgres-executor.ts +33 -11
  29. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  30. package/src/decorators/bootstrap.ts +482 -398
  31. package/src/decorators/column-decorator.ts +87 -96
  32. package/src/decorators/decorator-metadata.ts +100 -24
  33. package/src/decorators/entity.ts +27 -24
  34. package/src/decorators/relations.ts +231 -149
  35. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  36. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  37. package/src/dto/apply-filter.ts +568 -551
  38. package/src/index.ts +16 -9
  39. package/src/orm/entity-hydration.ts +116 -72
  40. package/src/orm/entity-metadata.ts +347 -301
  41. package/src/orm/entity-relations.ts +264 -207
  42. package/src/orm/entity.ts +199 -199
  43. package/src/orm/execute.ts +13 -13
  44. package/src/orm/lazy-batch/morph-many.ts +70 -0
  45. package/src/orm/lazy-batch/morph-one.ts +69 -0
  46. package/src/orm/lazy-batch/morph-to.ts +59 -0
  47. package/src/orm/lazy-batch.ts +4 -1
  48. package/src/orm/orm-session.ts +170 -104
  49. package/src/orm/pooled-executor-factory.ts +99 -58
  50. package/src/orm/query-logger.ts +49 -40
  51. package/src/orm/relation-change-processor.ts +198 -96
  52. package/src/orm/relations/belongs-to.ts +143 -143
  53. package/src/orm/relations/has-many.ts +204 -204
  54. package/src/orm/relations/has-one.ts +174 -174
  55. package/src/orm/relations/many-to-many.ts +288 -288
  56. package/src/orm/relations/morph-many.ts +156 -0
  57. package/src/orm/relations/morph-one.ts +151 -0
  58. package/src/orm/relations/morph-to.ts +162 -0
  59. package/src/orm/save-graph.ts +116 -1
  60. package/src/query-builder/expression-table-mapper.ts +5 -0
  61. package/src/query-builder/hydration-manager.ts +345 -345
  62. package/src/query-builder/hydration-planner.ts +178 -148
  63. package/src/query-builder/relation-conditions.ts +171 -151
  64. package/src/query-builder/relation-cte-builder.ts +5 -1
  65. package/src/query-builder/relation-filter-utils.ts +9 -6
  66. package/src/query-builder/relation-include-strategies.ts +44 -2
  67. package/src/query-builder/relation-join-strategies.ts +8 -1
  68. package/src/query-builder/relation-service.ts +250 -241
  69. package/src/query-builder/select/cursor-pagination.ts +323 -0
  70. package/src/query-builder/select/select-operations.ts +110 -105
  71. package/src/query-builder/select.ts +42 -1
  72. package/src/query-builder/update-include.ts +4 -0
  73. package/src/schema/relation.ts +296 -188
  74. package/src/schema/types.ts +138 -123
  75. package/src/tree/tree-decorator.ts +127 -137
@@ -1,398 +1,482 @@
1
- import { SelectQueryBuilder } from '../query-builder/select.js';
2
- import {
3
- hasMany,
4
- hasOne,
5
- belongsTo,
6
- belongsToMany,
7
- RelationKinds,
8
- type HasManyRelation,
9
- type HasOneRelation,
10
- type BelongsToRelation,
11
- type BelongsToManyRelation,
12
- type RelationDef
13
- } from '../schema/relation.js';
14
- import { TableDef } from '../schema/table.js';
15
- import { isTableDef } from '../schema/table-guards.js';
16
- import {
17
- buildTableDef,
18
- EntityConstructor,
19
- EntityMetadata,
20
- EntityOrTableTarget,
21
- EntityOrTableTargetResolver,
22
- getAllEntityMetadata,
23
- getEntityMetadata,
24
- addRelationMetadata,
25
- addTransformerMetadata,
26
- type RelationMetadata
27
- } from '../orm/entity-metadata.js';
28
- import { getDecoratorMetadata } from './decorator-metadata.js';
29
-
30
- import { tableRef, type TableRef } from '../schema/table.js';
31
- import {
32
- SelectableKeys,
33
- ColumnDef,
34
- HasManyCollection,
35
- HasOneReference,
36
- BelongsToReference,
37
- ManyToManyCollection,
38
- EntityInstance
39
- } from '../schema/types.js';
40
-
41
- const unwrapTarget = (target: EntityOrTableTargetResolver): EntityOrTableTarget => {
42
- if (typeof target === 'function' && (target as Function).prototype === undefined) {
43
- return (target as () => EntityOrTableTarget)();
44
- }
45
- return target as EntityOrTableTarget;
46
- };
47
-
48
- const resolveTableTarget = (
49
- target: EntityOrTableTargetResolver,
50
- tableMap: Map<EntityConstructor, TableDef>
51
- ): TableDef => {
52
- const resolved = unwrapTarget(target);
53
- if (isTableDef(resolved)) {
54
- return resolved;
55
- }
56
- const table = tableMap.get(resolved as EntityConstructor);
57
- if (!table) {
58
- throw new Error(`Entity '${(resolved as EntityConstructor).name}' is not registered with decorators`);
59
- }
60
- return table;
61
- };
62
-
63
- const toSnakeCase = (value: string): string => {
64
- return value
65
- .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
66
- .replace(/[^a-z0-9_]+/gi, '_')
67
- .replace(/__+/g, '_')
68
- .replace(/^_|_$/g, '')
69
- .toLowerCase();
70
- };
71
-
72
- const normalizeEntityName = (value: string): string => {
73
- const stripped = value.replace(/Entity$/i, '');
74
- const normalized = toSnakeCase(stripped || value);
75
- return normalized || 'unknown';
76
- };
77
-
78
- const getPivotKeyBaseFromTarget = (target: EntityOrTableTargetResolver): string => {
79
- const resolved = unwrapTarget(target);
80
- if (isTableDef(resolved)) {
81
- return toSnakeCase(resolved.name || 'unknown');
82
- }
83
- const ctor = resolved as EntityConstructor;
84
- return normalizeEntityName(ctor.name || 'unknown');
85
- };
86
-
87
- const getPivotKeyBaseFromRoot = (meta: EntityMetadata): string => {
88
- return normalizeEntityName(meta.target.name || meta.tableName || 'unknown');
89
- };
90
-
91
- const buildRelationDefinitions = (
92
- meta: EntityMetadata,
93
- tableMap: Map<EntityConstructor, TableDef>
94
- ): Record<string, RelationDef> => {
95
- const relations: Record<string, RelationDef> = {};
96
-
97
- for (const [name, relation] of Object.entries(meta.relations)) {
98
- switch (relation.kind) {
99
- case RelationKinds.HasOne: {
100
- const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
101
- relations[name] = hasOne(
102
- resolveTableTarget(relation.target, tableMap),
103
- foreignKey,
104
- relation.localKey,
105
- relation.cascade
106
- );
107
- break;
108
- }
109
- case RelationKinds.HasMany: {
110
- const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
111
- relations[name] = hasMany(
112
- resolveTableTarget(relation.target, tableMap),
113
- foreignKey,
114
- relation.localKey,
115
- relation.cascade
116
- );
117
- break;
118
- }
119
- case RelationKinds.BelongsTo: {
120
- relations[name] = belongsTo(
121
- resolveTableTarget(relation.target, tableMap),
122
- relation.foreignKey,
123
- relation.localKey,
124
- relation.cascade
125
- );
126
- break;
127
- }
128
- case RelationKinds.BelongsToMany: {
129
- const pivotForeignKeyToRoot =
130
- relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
131
- const pivotForeignKeyToTarget =
132
- relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
133
- relations[name] = belongsToMany(
134
- resolveTableTarget(relation.target, tableMap),
135
- resolveTableTarget(relation.pivotTable, tableMap),
136
- {
137
- pivotForeignKeyToRoot,
138
- pivotForeignKeyToTarget,
139
- localKey: relation.localKey,
140
- targetKey: relation.targetKey,
141
- pivotPrimaryKey: relation.pivotPrimaryKey,
142
- defaultPivotColumns: relation.defaultPivotColumns,
143
- cascade: relation.cascade
144
- }
145
- );
146
- break;
147
- }
148
- }
149
- }
150
-
151
- return relations;
152
- };
153
-
154
- /**
155
- * Bootstraps all entities by building their table definitions and relations.
156
- * @returns An array of table definitions for all bootstrapped entities.
157
- */
158
- export const bootstrapEntities = (): TableDef[] => {
159
- const metas = getAllEntityMetadata();
160
- const tableMap = new Map<EntityConstructor, TableDef>();
161
-
162
- // Process decorator metadata for each entity
163
- for (const meta of metas) {
164
- const decoratorMetadata = getDecoratorMetadata(meta.target);
165
- if (decoratorMetadata?.transformers) {
166
- for (const { propertyName, metadata } of decoratorMetadata.transformers) {
167
- addTransformerMetadata(meta.target, propertyName, metadata);
168
- }
169
- }
170
-
171
- const table = buildTableDef(meta);
172
- tableMap.set(meta.target, table);
173
- }
174
-
175
- for (const meta of metas) {
176
- const table = meta.table!;
177
- const relations = buildRelationDefinitions(meta, tableMap);
178
- table.relations = relations;
179
- }
180
-
181
- return metas.map(meta => meta.table!) as TableDef[];
182
- };
183
-
184
- /**
185
- * Builds a single RelationDef from RelationMetadata using the current set of
186
- * already-bootstrapped entity tables as the resolution map.
187
- */
188
- const resolveSingleRelation = (
189
- relationName: string,
190
- relation: RelationMetadata,
191
- rootMeta: EntityMetadata
192
- ): RelationDef => {
193
- // Build a tableMap from all entities that are already bootstrapped
194
- const tableMap = new Map<EntityConstructor, TableDef>();
195
- for (const m of getAllEntityMetadata()) {
196
- if (m.table) tableMap.set(m.target, m.table);
197
- }
198
-
199
- switch (relation.kind) {
200
- case RelationKinds.HasOne: {
201
- const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
202
- return hasOne(
203
- resolveTableTarget(relation.target, tableMap),
204
- foreignKey,
205
- relation.localKey,
206
- relation.cascade
207
- );
208
- }
209
- case RelationKinds.HasMany: {
210
- const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
211
- return hasMany(
212
- resolveTableTarget(relation.target, tableMap),
213
- foreignKey,
214
- relation.localKey,
215
- relation.cascade
216
- );
217
- }
218
- case RelationKinds.BelongsTo: {
219
- return belongsTo(
220
- resolveTableTarget(relation.target, tableMap),
221
- relation.foreignKey,
222
- relation.localKey,
223
- relation.cascade
224
- );
225
- }
226
- case RelationKinds.BelongsToMany: {
227
- const pivotForeignKeyToRoot =
228
- relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
229
- const pivotForeignKeyToTarget =
230
- relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
231
- return belongsToMany(
232
- resolveTableTarget(relation.target, tableMap),
233
- resolveTableTarget(relation.pivotTable, tableMap),
234
- {
235
- pivotForeignKeyToRoot,
236
- pivotForeignKeyToTarget,
237
- localKey: relation.localKey,
238
- targetKey: relation.targetKey,
239
- pivotPrimaryKey: relation.pivotPrimaryKey,
240
- defaultPivotColumns: relation.defaultPivotColumns,
241
- cascade: relation.cascade
242
- }
243
- );
244
- }
245
- default:
246
- throw new Error(`Unknown relation kind for relation '${relationName}'`);
247
- }
248
- };
249
-
250
- /**
251
- * Adds (or replaces) a single named relation on a decorator-based entity at any
252
- * time — before or after `bootstrapEntities()` has been called.
253
- *
254
- * - Always writes the metadata into the entity's `EntityMetadata.relations` so
255
- * that a future bootstrap will include it.
256
- * - If the entity table has already been built, the resolved `RelationDef` is
257
- * also patched directly into `table.relations` so the change is immediately
258
- * visible to query builders and hydration.
259
- *
260
- * @param ctor - The entity class decorated with `@Entity`
261
- * @param name - The relation property name (key used in `.include()`)
262
- * @param relation - Relation metadata in the same format as decorators expect
263
- *
264
- * @example
265
- * ```ts
266
- * // Same options as @HasMany decorator, usable at runtime
267
- * addEntityRelation(User, 'comments', {
268
- * kind: RelationKinds.HasMany,
269
- * propertyKey: 'comments',
270
- * target: () => Comment,
271
- * foreignKey: 'user_id',
272
- * });
273
- * ```
274
- */
275
- export const addEntityRelation = (
276
- ctor: EntityConstructor,
277
- name: string,
278
- relation: RelationMetadata
279
- ): void => {
280
- // Check registration BEFORE auto-creating metadata via addRelationMetadata
281
- const meta = getEntityMetadata(ctor);
282
- if (!meta) {
283
- throw new Error(`Entity '${ctor.name}' is not registered. Did you decorate it with @Entity?`);
284
- }
285
-
286
- // 1. Write to the decorator-layer metadata store (survives a future bootstrap)
287
- addRelationMetadata(ctor, name, relation);
288
-
289
- // 2. If the table is already built, patch it immediately
290
- if (meta.table) {
291
- meta.table.relations[name] = resolveSingleRelation(name, relation, meta);
292
- }
293
- };
294
-
295
- /**
296
- * Gets the table definition for a given entity constructor.
297
- * Bootstraps entities if necessary.
298
- * @param ctor - The entity constructor.
299
- * @returns The table definition or undefined if not found.
300
- */
301
- export const getTableDefFromEntity = <TTable extends TableDef = TableDef>(ctor: EntityConstructor): TTable | undefined => {
302
- const meta = getEntityMetadata(ctor);
303
- if (!meta) return undefined;
304
- if (!meta.table) {
305
- bootstrapEntities();
306
- }
307
- if (!meta.table) {
308
- throw new Error(`Failed to build table definition for entity '${ctor.name}'`);
309
- }
310
- return meta.table as TTable;
311
- };
312
-
313
- /**
314
- * Creates a select query builder for the given entity.
315
- * @param ctor - The entity constructor.
316
- * @returns A select query builder for the entity.
317
- */
318
- type NonFunctionKeys<T> = {
319
- [K in keyof T]-?: T[K] extends (...args: unknown[]) => unknown ? never : K
320
- }[keyof T];
321
-
322
- type RelationKeys<TEntity extends object> =
323
- Exclude<NonFunctionKeys<TEntity>, SelectableKeys<TEntity>> & string;
324
-
325
- type EntityTable<TEntity extends object> =
326
- Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
327
- relations: {
328
- [K in RelationKeys<TEntity>]:
329
- NonNullable<TEntity[K]> extends HasManyCollection<infer TChild>
330
- ? HasManyRelation<EntityTable<NonNullable<TChild> & object>>
331
- : NonNullable<TEntity[K]> extends ManyToManyCollection<infer TTarget, infer TPivot>
332
- ? BelongsToManyRelation<
333
- EntityTable<NonNullable<TTarget> & object>,
334
- TPivot extends object ? EntityTable<NonNullable<TPivot> & object> : TableDef
335
- >
336
- : NonNullable<TEntity[K]> extends HasOneReference<infer TChild>
337
- ? HasOneRelation<EntityTable<NonNullable<TChild> & object>>
338
- : NonNullable<TEntity[K]> extends BelongsToReference<infer TParent>
339
- ? BelongsToRelation<EntityTable<NonNullable<TParent> & object>>
340
- : NonNullable<TEntity[K]> extends object
341
- ? BelongsToRelation<EntityTable<NonNullable<TEntity[K]> & object>>
342
- : never;
343
- };
344
- };
345
-
346
- export type DecoratedEntityInstance<TEntity extends object> =
347
- TEntity & EntityInstance<EntityTable<TEntity>>;
348
-
349
- export const selectFromEntity = <TEntity extends object>(
350
- ctor: EntityConstructor<TEntity>
351
- ): SelectQueryBuilder<DecoratedEntityInstance<TEntity>, EntityTable<TEntity>> => {
352
- const table = getTableDefFromEntity(ctor);
353
- if (!table) {
354
- throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
355
- }
356
- return new SelectQueryBuilder(
357
- table as unknown as EntityTable<TEntity>,
358
- undefined,
359
- undefined,
360
- undefined,
361
- undefined,
362
- undefined,
363
- ctor
364
- );
365
- };
366
-
367
- /**
368
- * Public API: opt-in ergonomic entity reference (decorator-level).
369
- *
370
- * Lazily bootstraps entity metadata (via getTableDefFromEntity) and returns a
371
- * `tableRef(...)`-style proxy so users can write `u.id` instead of `u.columns.id`.
372
- */
373
- export const entityRef = <TEntity extends object>(
374
- ctor: EntityConstructor<TEntity>
375
- ): TableRef<EntityTable<TEntity>> => {
376
- const table = getTableDefFromEntity(ctor);
377
- if (!table) {
378
- throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
379
- }
380
- return tableRef(table as EntityTable<TEntity>);
381
- };
382
-
383
- type EntityRefsTuple<T extends readonly EntityConstructor<object>[]> = {
384
- [K in keyof T]: T[K] extends EntityConstructor<infer TEntity>
385
- ? TableRef<EntityTable<TEntity & object>>
386
- : never;
387
- };
388
-
389
- /**
390
- * Public API: variadic entity references.
391
- * Usage:
392
- * const [u, p] = entityRefs(User, Post);
393
- */
394
- export const entityRefs = <T extends readonly EntityConstructor<object>[]>(
395
- ...ctors: T
396
- ): EntityRefsTuple<T> => {
397
- return ctors.map(ctor => entityRef(ctor)) as EntityRefsTuple<T>;
398
- };
1
+ import { SelectQueryBuilder } from '../query-builder/select.js';
2
+ import {
3
+ hasMany,
4
+ hasOne,
5
+ belongsTo,
6
+ belongsToMany,
7
+ morphTo,
8
+ morphOne,
9
+ morphMany,
10
+ RelationKinds,
11
+ type HasManyRelation,
12
+ type HasOneRelation,
13
+ type BelongsToRelation,
14
+ type BelongsToManyRelation,
15
+ type RelationDef
16
+ } from '../schema/relation.js';
17
+ import { TableDef } from '../schema/table.js';
18
+ import { isTableDef } from '../schema/table-guards.js';
19
+ import {
20
+ buildTableDef,
21
+ EntityConstructor,
22
+ EntityMetadata,
23
+ EntityOrTableTarget,
24
+ EntityOrTableTargetResolver,
25
+ getAllEntityMetadata,
26
+ getEntityMetadata,
27
+ addRelationMetadata,
28
+ addTransformerMetadata,
29
+ type RelationMetadata
30
+ } from '../orm/entity-metadata.js';
31
+ import { getDecoratorMetadata } from './decorator-metadata.js';
32
+
33
+ import { tableRef, type TableRef } from '../schema/table.js';
34
+ import {
35
+ SelectableKeys,
36
+ ColumnDef,
37
+ HasManyCollection,
38
+ HasOneReference,
39
+ BelongsToReference,
40
+ ManyToManyCollection,
41
+ EntityInstance
42
+ } from '../schema/types.js';
43
+
44
+ const unwrapTarget = (target: EntityOrTableTargetResolver): EntityOrTableTarget => {
45
+ if (typeof target === 'function' && (target as Function).prototype === undefined) {
46
+ return (target as () => EntityOrTableTarget)();
47
+ }
48
+ return target as EntityOrTableTarget;
49
+ };
50
+
51
+ const resolveTableTarget = (
52
+ target: EntityOrTableTargetResolver,
53
+ tableMap: Map<EntityConstructor, TableDef>
54
+ ): TableDef => {
55
+ const resolved = unwrapTarget(target);
56
+ if (isTableDef(resolved)) {
57
+ return resolved;
58
+ }
59
+ const table = tableMap.get(resolved as EntityConstructor);
60
+ if (!table) {
61
+ throw new Error(`Entity '${(resolved as EntityConstructor).name}' is not registered with decorators`);
62
+ }
63
+ return table;
64
+ };
65
+
66
+ const toSnakeCase = (value: string): string => {
67
+ return value
68
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
69
+ .replace(/[^a-z0-9_]+/gi, '_')
70
+ .replace(/__+/g, '_')
71
+ .replace(/^_|_$/g, '')
72
+ .toLowerCase();
73
+ };
74
+
75
+ const normalizeEntityName = (value: string): string => {
76
+ const stripped = value.replace(/Entity$/i, '');
77
+ const normalized = toSnakeCase(stripped || value);
78
+ return normalized || 'unknown';
79
+ };
80
+
81
+ const getPivotKeyBaseFromTarget = (target: EntityOrTableTargetResolver): string => {
82
+ const resolved = unwrapTarget(target);
83
+ if (isTableDef(resolved)) {
84
+ return toSnakeCase(resolved.name || 'unknown');
85
+ }
86
+ const ctor = resolved as EntityConstructor;
87
+ return normalizeEntityName(ctor.name || 'unknown');
88
+ };
89
+
90
+ const getPivotKeyBaseFromRoot = (meta: EntityMetadata): string => {
91
+ return normalizeEntityName(meta.target.name || meta.tableName || 'unknown');
92
+ };
93
+
94
+ const buildRelationDefinitions = (
95
+ meta: EntityMetadata,
96
+ tableMap: Map<EntityConstructor, TableDef>
97
+ ): Record<string, RelationDef> => {
98
+ const relations: Record<string, RelationDef> = {};
99
+
100
+ for (const [name, relation] of Object.entries(meta.relations)) {
101
+ switch (relation.kind) {
102
+ case RelationKinds.HasOne: {
103
+ const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
104
+ relations[name] = hasOne(
105
+ resolveTableTarget(relation.target, tableMap),
106
+ foreignKey,
107
+ relation.localKey,
108
+ relation.cascade
109
+ );
110
+ break;
111
+ }
112
+ case RelationKinds.HasMany: {
113
+ const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
114
+ relations[name] = hasMany(
115
+ resolveTableTarget(relation.target, tableMap),
116
+ foreignKey,
117
+ relation.localKey,
118
+ relation.cascade
119
+ );
120
+ break;
121
+ }
122
+ case RelationKinds.BelongsTo: {
123
+ relations[name] = belongsTo(
124
+ resolveTableTarget(relation.target, tableMap),
125
+ relation.foreignKey,
126
+ relation.localKey,
127
+ relation.cascade
128
+ );
129
+ break;
130
+ }
131
+ case RelationKinds.BelongsToMany: {
132
+ const pivotForeignKeyToRoot =
133
+ relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(meta)}_id`;
134
+ const pivotForeignKeyToTarget =
135
+ relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
136
+ relations[name] = belongsToMany(
137
+ resolveTableTarget(relation.target, tableMap),
138
+ resolveTableTarget(relation.pivotTable, tableMap),
139
+ {
140
+ pivotForeignKeyToRoot,
141
+ pivotForeignKeyToTarget,
142
+ localKey: relation.localKey,
143
+ targetKey: relation.targetKey,
144
+ pivotPrimaryKey: relation.pivotPrimaryKey,
145
+ defaultPivotColumns: relation.defaultPivotColumns,
146
+ cascade: relation.cascade
147
+ }
148
+ );
149
+ break;
150
+ }
151
+ case RelationKinds.MorphOne: {
152
+ relations[name] = morphOne(
153
+ resolveTableTarget(relation.target, tableMap),
154
+ {
155
+ as: relation.morphName,
156
+ typeValue: relation.typeValue,
157
+ typeField: relation.typeField,
158
+ idField: relation.idField,
159
+ localKey: relation.localKey,
160
+ cascade: relation.cascade
161
+ }
162
+ );
163
+ break;
164
+ }
165
+ case RelationKinds.MorphMany: {
166
+ relations[name] = morphMany(
167
+ resolveTableTarget(relation.target, tableMap),
168
+ {
169
+ as: relation.morphName,
170
+ typeValue: relation.typeValue,
171
+ typeField: relation.typeField,
172
+ idField: relation.idField,
173
+ localKey: relation.localKey,
174
+ cascade: relation.cascade
175
+ }
176
+ );
177
+ break;
178
+ }
179
+ case RelationKinds.MorphTo: {
180
+ const resolvedTargets: Record<string, TableDef> = {};
181
+ for (const [typeValue, targetResolver] of Object.entries(relation.targets)) {
182
+ resolvedTargets[typeValue] = resolveTableTarget(targetResolver, tableMap);
183
+ }
184
+ relations[name] = morphTo({
185
+ typeField: relation.typeField,
186
+ idField: relation.idField,
187
+ targets: resolvedTargets,
188
+ targetKey: relation.targetKey,
189
+ cascade: relation.cascade
190
+ });
191
+ break;
192
+ }
193
+ }
194
+ }
195
+
196
+ return relations;
197
+ };
198
+
199
+ /**
200
+ * Bootstraps all entities by building their table definitions and relations.
201
+ * @returns An array of table definitions for all bootstrapped entities.
202
+ */
203
+ export const bootstrapEntities = (): TableDef[] => {
204
+ const metas = getAllEntityMetadata();
205
+ const tableMap = new Map<EntityConstructor, TableDef>();
206
+
207
+ // Process decorator metadata for each entity
208
+ for (const meta of metas) {
209
+ const decoratorMetadata = getDecoratorMetadata(meta.target);
210
+ if (decoratorMetadata?.transformers) {
211
+ for (const { propertyName, metadata } of decoratorMetadata.transformers) {
212
+ addTransformerMetadata(meta.target, propertyName, metadata);
213
+ }
214
+ }
215
+
216
+ const table = buildTableDef(meta);
217
+ tableMap.set(meta.target, table);
218
+ }
219
+
220
+ for (const meta of metas) {
221
+ const table = meta.table!;
222
+ const relations = buildRelationDefinitions(meta, tableMap);
223
+ table.relations = relations;
224
+ }
225
+
226
+ return metas.map(meta => meta.table!) as TableDef[];
227
+ };
228
+
229
+ /**
230
+ * Builds a single RelationDef from RelationMetadata using the current set of
231
+ * already-bootstrapped entity tables as the resolution map.
232
+ */
233
+ const resolveSingleRelation = (
234
+ relationName: string,
235
+ relation: RelationMetadata,
236
+ rootMeta: EntityMetadata
237
+ ): RelationDef => {
238
+ // Build a tableMap from all entities that are already bootstrapped
239
+ const tableMap = new Map<EntityConstructor, TableDef>();
240
+ for (const m of getAllEntityMetadata()) {
241
+ if (m.table) tableMap.set(m.target, m.table);
242
+ }
243
+
244
+ switch (relation.kind) {
245
+ case RelationKinds.HasOne: {
246
+ const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
247
+ return hasOne(
248
+ resolveTableTarget(relation.target, tableMap),
249
+ foreignKey,
250
+ relation.localKey,
251
+ relation.cascade
252
+ );
253
+ }
254
+ case RelationKinds.HasMany: {
255
+ const foreignKey = relation.foreignKey ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
256
+ return hasMany(
257
+ resolveTableTarget(relation.target, tableMap),
258
+ foreignKey,
259
+ relation.localKey,
260
+ relation.cascade
261
+ );
262
+ }
263
+ case RelationKinds.BelongsTo: {
264
+ return belongsTo(
265
+ resolveTableTarget(relation.target, tableMap),
266
+ relation.foreignKey,
267
+ relation.localKey,
268
+ relation.cascade
269
+ );
270
+ }
271
+ case RelationKinds.BelongsToMany: {
272
+ const pivotForeignKeyToRoot =
273
+ relation.pivotForeignKeyToRoot ?? `${getPivotKeyBaseFromRoot(rootMeta)}_id`;
274
+ const pivotForeignKeyToTarget =
275
+ relation.pivotForeignKeyToTarget ?? `${getPivotKeyBaseFromTarget(relation.target)}_id`;
276
+ return belongsToMany(
277
+ resolveTableTarget(relation.target, tableMap),
278
+ resolveTableTarget(relation.pivotTable, tableMap),
279
+ {
280
+ pivotForeignKeyToRoot,
281
+ pivotForeignKeyToTarget,
282
+ localKey: relation.localKey,
283
+ targetKey: relation.targetKey,
284
+ pivotPrimaryKey: relation.pivotPrimaryKey,
285
+ defaultPivotColumns: relation.defaultPivotColumns,
286
+ cascade: relation.cascade
287
+ }
288
+ );
289
+ }
290
+ case RelationKinds.MorphOne: {
291
+ return morphOne(
292
+ resolveTableTarget(relation.target, tableMap),
293
+ {
294
+ as: relation.morphName,
295
+ typeValue: relation.typeValue,
296
+ typeField: relation.typeField,
297
+ idField: relation.idField,
298
+ localKey: relation.localKey,
299
+ cascade: relation.cascade
300
+ }
301
+ );
302
+ }
303
+ case RelationKinds.MorphMany: {
304
+ return morphMany(
305
+ resolveTableTarget(relation.target, tableMap),
306
+ {
307
+ as: relation.morphName,
308
+ typeValue: relation.typeValue,
309
+ typeField: relation.typeField,
310
+ idField: relation.idField,
311
+ localKey: relation.localKey,
312
+ cascade: relation.cascade
313
+ }
314
+ );
315
+ }
316
+ case RelationKinds.MorphTo: {
317
+ const resolvedTargets: Record<string, TableDef> = {};
318
+ for (const [typeValue, targetResolver] of Object.entries(relation.targets)) {
319
+ resolvedTargets[typeValue] = resolveTableTarget(targetResolver, tableMap);
320
+ }
321
+ return morphTo({
322
+ typeField: relation.typeField,
323
+ idField: relation.idField,
324
+ targets: resolvedTargets,
325
+ targetKey: relation.targetKey,
326
+ cascade: relation.cascade
327
+ });
328
+ }
329
+ default:
330
+ throw new Error(`Unknown relation kind for relation '${relationName}'`);
331
+ }
332
+ };
333
+
334
+ /**
335
+ * Adds (or replaces) a single named relation on a decorator-based entity at any
336
+ * time before or after `bootstrapEntities()` has been called.
337
+ *
338
+ * - Always writes the metadata into the entity's `EntityMetadata.relations` so
339
+ * that a future bootstrap will include it.
340
+ * - If the entity table has already been built, the resolved `RelationDef` is
341
+ * also patched directly into `table.relations` so the change is immediately
342
+ * visible to query builders and hydration.
343
+ *
344
+ * @param ctor - The entity class decorated with `@Entity`
345
+ * @param name - The relation property name (key used in `.include()`)
346
+ * @param relation - Relation metadata in the same format as decorators expect
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * // Same options as @HasMany decorator, usable at runtime
351
+ * addEntityRelation(User, 'comments', {
352
+ * kind: RelationKinds.HasMany,
353
+ * propertyKey: 'comments',
354
+ * target: () => Comment,
355
+ * foreignKey: 'user_id',
356
+ * });
357
+ * ```
358
+ */
359
+ export const addEntityRelation = (
360
+ ctor: EntityConstructor,
361
+ name: string,
362
+ relation: RelationMetadata
363
+ ): void => {
364
+ // Check registration BEFORE auto-creating metadata via addRelationMetadata
365
+ const meta = getEntityMetadata(ctor);
366
+ if (!meta) {
367
+ throw new Error(`Entity '${ctor.name}' is not registered. Did you decorate it with @Entity?`);
368
+ }
369
+
370
+ // 1. Write to the decorator-layer metadata store (survives a future bootstrap)
371
+ addRelationMetadata(ctor, name, relation);
372
+
373
+ // 2. If the table is already built, patch it immediately
374
+ if (meta.table) {
375
+ meta.table.relations[name] = resolveSingleRelation(name, relation, meta);
376
+ }
377
+ };
378
+
379
+ /**
380
+ * Gets the table definition for a given entity constructor.
381
+ * Bootstraps entities if necessary.
382
+ * @param ctor - The entity constructor.
383
+ * @returns The table definition or undefined if not found.
384
+ */
385
+ export const getTableDefFromEntity = <TTable extends TableDef = TableDef>(ctor: EntityConstructor): TTable | undefined => {
386
+ const meta = getEntityMetadata(ctor);
387
+ if (!meta) return undefined;
388
+ if (!meta.table) {
389
+ bootstrapEntities();
390
+ }
391
+ if (!meta.table) {
392
+ throw new Error(`Failed to build table definition for entity '${ctor.name}'`);
393
+ }
394
+ return meta.table as TTable;
395
+ };
396
+
397
+ /**
398
+ * Creates a select query builder for the given entity.
399
+ * @param ctor - The entity constructor.
400
+ * @returns A select query builder for the entity.
401
+ */
402
+ type NonFunctionKeys<T> = {
403
+ [K in keyof T]-?: T[K] extends (...args: unknown[]) => unknown ? never : K
404
+ }[keyof T];
405
+
406
+ type RelationKeys<TEntity extends object> =
407
+ Exclude<NonFunctionKeys<TEntity>, SelectableKeys<TEntity>> & string;
408
+
409
+ type EntityTable<TEntity extends object> =
410
+ Omit<TableDef<{ [K in SelectableKeys<TEntity>]: ColumnDef }>, 'relations'> & {
411
+ relations: {
412
+ [K in RelationKeys<TEntity>]:
413
+ NonNullable<TEntity[K]> extends HasManyCollection<infer TChild>
414
+ ? HasManyRelation<EntityTable<NonNullable<TChild> & object>>
415
+ : NonNullable<TEntity[K]> extends ManyToManyCollection<infer TTarget, infer TPivot>
416
+ ? BelongsToManyRelation<
417
+ EntityTable<NonNullable<TTarget> & object>,
418
+ TPivot extends object ? EntityTable<NonNullable<TPivot> & object> : TableDef
419
+ >
420
+ : NonNullable<TEntity[K]> extends HasOneReference<infer TChild>
421
+ ? HasOneRelation<EntityTable<NonNullable<TChild> & object>>
422
+ : NonNullable<TEntity[K]> extends BelongsToReference<infer TParent>
423
+ ? BelongsToRelation<EntityTable<NonNullable<TParent> & object>>
424
+ : NonNullable<TEntity[K]> extends object
425
+ ? BelongsToRelation<EntityTable<NonNullable<TEntity[K]> & object>>
426
+ : never;
427
+ };
428
+ };
429
+
430
+ export type DecoratedEntityInstance<TEntity extends object> =
431
+ TEntity & EntityInstance<EntityTable<TEntity>>;
432
+
433
+ export const selectFromEntity = <TEntity extends object>(
434
+ ctor: EntityConstructor<TEntity>
435
+ ): SelectQueryBuilder<DecoratedEntityInstance<TEntity>, EntityTable<TEntity>> => {
436
+ const table = getTableDefFromEntity(ctor);
437
+ if (!table) {
438
+ throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
439
+ }
440
+ return new SelectQueryBuilder(
441
+ table as unknown as EntityTable<TEntity>,
442
+ undefined,
443
+ undefined,
444
+ undefined,
445
+ undefined,
446
+ undefined,
447
+ ctor
448
+ );
449
+ };
450
+
451
+ /**
452
+ * Public API: opt-in ergonomic entity reference (decorator-level).
453
+ *
454
+ * Lazily bootstraps entity metadata (via getTableDefFromEntity) and returns a
455
+ * `tableRef(...)`-style proxy so users can write `u.id` instead of `u.columns.id`.
456
+ */
457
+ export const entityRef = <TEntity extends object>(
458
+ ctor: EntityConstructor<TEntity>
459
+ ): TableRef<EntityTable<TEntity>> => {
460
+ const table = getTableDefFromEntity(ctor);
461
+ if (!table) {
462
+ throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
463
+ }
464
+ return tableRef(table as EntityTable<TEntity>);
465
+ };
466
+
467
+ type EntityRefsTuple<T extends readonly EntityConstructor<object>[]> = {
468
+ [K in keyof T]: T[K] extends EntityConstructor<infer TEntity>
469
+ ? TableRef<EntityTable<TEntity & object>>
470
+ : never;
471
+ };
472
+
473
+ /**
474
+ * Public API: variadic entity references.
475
+ * Usage:
476
+ * const [u, p] = entityRefs(User, Post);
477
+ */
478
+ export const entityRefs = <T extends readonly EntityConstructor<object>[]>(
479
+ ...ctors: T
480
+ ): EntityRefsTuple<T> => {
481
+ return ctors.map(ctor => entityRef(ctor)) as EntityRefsTuple<T>;
482
+ };