metal-orm 1.0.56 → 1.0.58
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 +41 -33
- package/dist/index.cjs +1461 -195
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +541 -114
- package/dist/index.d.ts +541 -114
- package/dist/index.js +1424 -195
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
- package/src/codegen/naming-strategy.ts +3 -1
- package/src/codegen/typescript.ts +20 -10
- package/src/core/ast/aggregate-functions.ts +14 -0
- package/src/core/ast/builders.ts +38 -20
- package/src/core/ast/expression-builders.ts +70 -2
- package/src/core/ast/expression-nodes.ts +305 -274
- package/src/core/ast/expression-visitor.ts +11 -1
- package/src/core/ast/expression.ts +4 -0
- package/src/core/ast/query.ts +3 -0
- package/src/core/ddl/introspect/catalogs/mysql.ts +5 -0
- package/src/core/ddl/introspect/catalogs/sqlite.ts +3 -0
- package/src/core/ddl/introspect/functions/mssql.ts +13 -0
- package/src/core/ddl/introspect/mssql.ts +4 -0
- package/src/core/ddl/introspect/mysql.ts +4 -0
- package/src/core/ddl/introspect/sqlite.ts +4 -0
- package/src/core/dialect/abstract.ts +552 -531
- package/src/core/dialect/base/function-table-formatter.ts +9 -30
- package/src/core/dialect/base/sql-dialect.ts +24 -0
- package/src/core/dialect/mssql/functions.ts +40 -2
- package/src/core/dialect/mysql/functions.ts +16 -2
- package/src/core/dialect/postgres/functions.ts +66 -2
- package/src/core/dialect/postgres/index.ts +17 -4
- package/src/core/dialect/postgres/table-functions.ts +27 -0
- package/src/core/dialect/sqlite/functions.ts +34 -0
- package/src/core/dialect/sqlite/index.ts +17 -1
- package/src/core/driver/database-driver.ts +9 -1
- package/src/core/driver/mssql-driver.ts +3 -0
- package/src/core/driver/mysql-driver.ts +3 -0
- package/src/core/driver/postgres-driver.ts +3 -0
- package/src/core/driver/sqlite-driver.ts +3 -0
- package/src/core/execution/executors/mssql-executor.ts +5 -0
- package/src/core/execution/executors/mysql-executor.ts +5 -0
- package/src/core/execution/executors/postgres-executor.ts +5 -0
- package/src/core/execution/executors/sqlite-executor.ts +5 -0
- package/src/core/functions/array.ts +26 -0
- package/src/core/functions/control-flow.ts +69 -0
- package/src/core/functions/datetime.ts +50 -0
- package/src/core/functions/definitions/aggregate.ts +16 -0
- package/src/core/functions/definitions/control-flow.ts +24 -0
- package/src/core/functions/definitions/datetime.ts +36 -0
- package/src/core/functions/definitions/helpers.ts +29 -0
- package/src/core/functions/definitions/json.ts +49 -0
- package/src/core/functions/definitions/numeric.ts +55 -0
- package/src/core/functions/definitions/string.ts +43 -0
- package/src/core/functions/function-registry.ts +48 -0
- package/src/core/functions/group-concat-helpers.ts +57 -0
- package/src/core/functions/json.ts +38 -0
- package/src/core/functions/numeric.ts +14 -0
- package/src/core/functions/standard-strategy.ts +86 -115
- package/src/core/functions/standard-table-strategy.ts +13 -0
- package/src/core/functions/table-types.ts +15 -0
- package/src/core/functions/text.ts +57 -0
- package/src/core/sql/sql.ts +59 -38
- package/src/decorators/bootstrap.ts +41 -4
- package/src/index.ts +18 -11
- package/src/orm/entity-meta.ts +6 -3
- package/src/orm/entity.ts +81 -14
- package/src/orm/execute.ts +87 -20
- package/src/orm/hydration-context.ts +10 -0
- package/src/orm/identity-map.ts +19 -0
- package/src/orm/interceptor-pipeline.ts +4 -0
- package/src/orm/lazy-batch.ts +237 -54
- package/src/orm/relations/belongs-to.ts +19 -2
- package/src/orm/relations/has-many.ts +23 -9
- package/src/orm/relations/has-one.ts +19 -2
- package/src/orm/relations/many-to-many.ts +59 -4
- package/src/orm/save-graph-types.ts +2 -2
- package/src/orm/save-graph.ts +18 -18
- package/src/query-builder/relation-conditions.ts +80 -59
- package/src/query-builder/relation-service.ts +399 -95
- package/src/query-builder/relation-types.ts +2 -2
- package/src/query-builder/select.ts +124 -106
- package/src/schema/table-guards.ts +6 -0
- package/src/schema/types.ts +109 -85
package/src/orm/lazy-batch.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { BelongsToManyRelation, HasManyRelation, HasOneRelation, BelongsToRelation } from '../schema/relation.js';
|
|
3
3
|
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
4
|
-
import { inList, LiteralNode } from '../core/ast/expression.js';
|
|
4
|
+
import { ExpressionNode, inList, LiteralNode } from '../core/ast/expression.js';
|
|
5
5
|
import { EntityContext } from './entity-context.js';
|
|
6
6
|
import type { QueryResult } from '../core/execution/db-executor.js';
|
|
7
7
|
import { ColumnDef } from '../schema/column-types.js';
|
|
8
8
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
9
|
+
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
10
|
+
import { buildDefaultPivotColumns } from '../query-builder/relation-utils.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* An array of database rows, each represented as a record of string keys to unknown values.
|
|
@@ -17,16 +19,35 @@ type Rows = Record<string, unknown>[];
|
|
|
17
19
|
*/
|
|
18
20
|
type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const hasColumns = (columns?: readonly string[]): columns is readonly string[] =>
|
|
23
|
+
Boolean(columns && columns.length > 0);
|
|
24
|
+
|
|
25
|
+
const buildColumnSelection = (
|
|
26
|
+
table: TableDef,
|
|
27
|
+
columns: string[],
|
|
28
|
+
missingMsg: (col: string) => string
|
|
29
|
+
): Record<string, ColumnDef> => {
|
|
30
|
+
return columns.reduce((acc, column) => {
|
|
31
|
+
const def = table.columns[column];
|
|
32
|
+
if (!def) {
|
|
33
|
+
throw new Error(missingMsg(column));
|
|
34
|
+
}
|
|
35
|
+
acc[column] = def;
|
|
28
36
|
return acc;
|
|
29
37
|
}, {} as Record<string, ColumnDef>);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const filterRow = (row: Record<string, unknown>, columns: Set<string>): Record<string, unknown> => {
|
|
41
|
+
const filtered: Record<string, unknown> = {};
|
|
42
|
+
for (const column of columns) {
|
|
43
|
+
if (column in row) {
|
|
44
|
+
filtered[column] = row[column];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return filtered;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const filterRows = (rows: Rows, columns: Set<string>): Rows => rows.map(row => filterRow(row, columns));
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Extracts rows from query results into a standardized format.
|
|
@@ -54,11 +75,11 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
|
|
|
54
75
|
* @param qb - The select query builder.
|
|
55
76
|
* @returns A promise resolving to the rows from the query.
|
|
56
77
|
*/
|
|
57
|
-
const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
|
|
58
|
-
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
59
|
-
const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
60
|
-
return rowsFromResults(results);
|
|
61
|
-
};
|
|
78
|
+
const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
|
|
79
|
+
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
80
|
+
const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
81
|
+
return rowsFromResults(results);
|
|
82
|
+
};
|
|
62
83
|
|
|
63
84
|
/**
|
|
64
85
|
* Converts a value to a string key, handling null and undefined as empty string.
|
|
@@ -100,16 +121,21 @@ const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[
|
|
|
100
121
|
* @param keys - The set of keys to match.
|
|
101
122
|
* @returns A promise resolving to the matching rows.
|
|
102
123
|
*/
|
|
103
|
-
const fetchRowsForKeys = async (
|
|
104
|
-
ctx: EntityContext,
|
|
105
|
-
table: TableDef,
|
|
106
|
-
column: ColumnDef,
|
|
107
|
-
keys: Set<unknown
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
const fetchRowsForKeys = async (
|
|
125
|
+
ctx: EntityContext,
|
|
126
|
+
table: TableDef,
|
|
127
|
+
column: ColumnDef,
|
|
128
|
+
keys: Set<unknown>,
|
|
129
|
+
selection: Record<string, ColumnDef>,
|
|
130
|
+
filter?: ExpressionNode
|
|
131
|
+
): Promise<Rows> => {
|
|
132
|
+
let qb = new SelectQueryBuilder(table).select(selection);
|
|
133
|
+
qb = qb.where(inList(column, buildInListValues(keys)));
|
|
134
|
+
if (filter) {
|
|
135
|
+
qb = qb.where(filter);
|
|
136
|
+
}
|
|
137
|
+
return executeQuery(ctx, qb);
|
|
138
|
+
};
|
|
113
139
|
|
|
114
140
|
/**
|
|
115
141
|
* Groups rows by the value of a key column, allowing multiple rows per key.
|
|
@@ -160,8 +186,9 @@ const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<st
|
|
|
160
186
|
export const loadHasManyRelation = async (
|
|
161
187
|
ctx: EntityContext,
|
|
162
188
|
rootTable: TableDef,
|
|
163
|
-
|
|
164
|
-
relation: HasManyRelation
|
|
189
|
+
relationName: string,
|
|
190
|
+
relation: HasManyRelation,
|
|
191
|
+
options?: RelationIncludeOptions
|
|
165
192
|
): Promise<Map<string, Rows>> => {
|
|
166
193
|
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
167
194
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
@@ -174,8 +201,33 @@ export const loadHasManyRelation = async (
|
|
|
174
201
|
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
175
202
|
if (!fkColumn) return new Map();
|
|
176
203
|
|
|
177
|
-
const
|
|
178
|
-
|
|
204
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
205
|
+
const targetPrimaryKey = findPrimaryKey(relation.target);
|
|
206
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
207
|
+
if (!selectedColumns.includes(targetPrimaryKey)) {
|
|
208
|
+
selectedColumns.push(targetPrimaryKey);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const queryColumns = new Set(selectedColumns);
|
|
212
|
+
queryColumns.add(relation.foreignKey);
|
|
213
|
+
|
|
214
|
+
const selection = buildColumnSelection(
|
|
215
|
+
relation.target,
|
|
216
|
+
Array.from(queryColumns),
|
|
217
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, options?.filter);
|
|
221
|
+
const grouped = groupRowsByMany(rows, relation.foreignKey);
|
|
222
|
+
|
|
223
|
+
if (!requestedColumns) return grouped;
|
|
224
|
+
|
|
225
|
+
const visibleColumns = new Set(selectedColumns);
|
|
226
|
+
const filtered = new Map<string, Rows>();
|
|
227
|
+
for (const [key, bucket] of grouped.entries()) {
|
|
228
|
+
filtered.set(key, filterRows(bucket, visibleColumns));
|
|
229
|
+
}
|
|
230
|
+
return filtered;
|
|
179
231
|
};
|
|
180
232
|
|
|
181
233
|
/**
|
|
@@ -189,8 +241,9 @@ export const loadHasManyRelation = async (
|
|
|
189
241
|
export const loadHasOneRelation = async (
|
|
190
242
|
ctx: EntityContext,
|
|
191
243
|
rootTable: TableDef,
|
|
192
|
-
|
|
193
|
-
relation: HasOneRelation
|
|
244
|
+
relationName: string,
|
|
245
|
+
relation: HasOneRelation,
|
|
246
|
+
options?: RelationIncludeOptions
|
|
194
247
|
): Promise<Map<string, Record<string, unknown>>> => {
|
|
195
248
|
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
196
249
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
@@ -203,8 +256,33 @@ export const loadHasOneRelation = async (
|
|
|
203
256
|
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
204
257
|
if (!fkColumn) return new Map();
|
|
205
258
|
|
|
206
|
-
const
|
|
207
|
-
|
|
259
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
260
|
+
const targetPrimaryKey = findPrimaryKey(relation.target);
|
|
261
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
262
|
+
if (!selectedColumns.includes(targetPrimaryKey)) {
|
|
263
|
+
selectedColumns.push(targetPrimaryKey);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const queryColumns = new Set(selectedColumns);
|
|
267
|
+
queryColumns.add(relation.foreignKey);
|
|
268
|
+
|
|
269
|
+
const selection = buildColumnSelection(
|
|
270
|
+
relation.target,
|
|
271
|
+
Array.from(queryColumns),
|
|
272
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, options?.filter);
|
|
276
|
+
const grouped = groupRowsByUnique(rows, relation.foreignKey);
|
|
277
|
+
|
|
278
|
+
if (!requestedColumns) return grouped;
|
|
279
|
+
|
|
280
|
+
const visibleColumns = new Set(selectedColumns);
|
|
281
|
+
const filtered = new Map<string, Record<string, unknown>>();
|
|
282
|
+
for (const [key, row] of grouped.entries()) {
|
|
283
|
+
filtered.set(key, filterRow(row, visibleColumns));
|
|
284
|
+
}
|
|
285
|
+
return filtered;
|
|
208
286
|
};
|
|
209
287
|
|
|
210
288
|
/**
|
|
@@ -215,25 +293,91 @@ export const loadHasOneRelation = async (
|
|
|
215
293
|
* @param relation - The belongs-to relation definition.
|
|
216
294
|
* @returns A promise resolving to a map of foreign keys to single related rows.
|
|
217
295
|
*/
|
|
218
|
-
export const loadBelongsToRelation = async (
|
|
219
|
-
ctx: EntityContext,
|
|
220
|
-
rootTable: TableDef,
|
|
221
|
-
|
|
222
|
-
relation: BelongsToRelation
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
296
|
+
export const loadBelongsToRelation = async (
|
|
297
|
+
ctx: EntityContext,
|
|
298
|
+
rootTable: TableDef,
|
|
299
|
+
relationName: string,
|
|
300
|
+
relation: BelongsToRelation,
|
|
301
|
+
options?: RelationIncludeOptions
|
|
302
|
+
): Promise<Map<string, Record<string, unknown>>> => {
|
|
303
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
304
|
+
|
|
305
|
+
const getForeignKeys = (): Set<unknown> => collectKeysFromRoots(roots, relation.foreignKey);
|
|
306
|
+
let foreignKeys = getForeignKeys();
|
|
307
|
+
|
|
308
|
+
if (!foreignKeys.size) {
|
|
309
|
+
const pkName = findPrimaryKey(rootTable);
|
|
310
|
+
const pkColumn = rootTable.columns[pkName];
|
|
311
|
+
const fkColumn = rootTable.columns[relation.foreignKey];
|
|
312
|
+
|
|
313
|
+
if (pkColumn && fkColumn) {
|
|
314
|
+
const missingKeys = new Set<unknown>();
|
|
315
|
+
const entityByPk = new Map<unknown, Record<string, unknown>>();
|
|
316
|
+
|
|
317
|
+
for (const tracked of roots) {
|
|
318
|
+
const entity = tracked.entity as Record<string, unknown>;
|
|
319
|
+
const pkValue = entity[pkName];
|
|
320
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
321
|
+
const fkValue = entity[relation.foreignKey];
|
|
322
|
+
if (fkValue === undefined || fkValue === null) {
|
|
323
|
+
missingKeys.add(pkValue);
|
|
324
|
+
entityByPk.set(pkValue, entity);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (missingKeys.size) {
|
|
329
|
+
const selection = buildColumnSelection(
|
|
330
|
+
rootTable,
|
|
331
|
+
[pkName, relation.foreignKey],
|
|
332
|
+
column => `Column '${column}' not found on table '${rootTable.name}'`
|
|
333
|
+
);
|
|
334
|
+
const keyRows = await fetchRowsForKeys(ctx, rootTable, pkColumn, missingKeys, selection);
|
|
335
|
+
for (const row of keyRows) {
|
|
336
|
+
const pkValue = row[pkName];
|
|
337
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
338
|
+
const entity = entityByPk.get(pkValue);
|
|
339
|
+
if (!entity) continue;
|
|
340
|
+
const fkValue = row[relation.foreignKey];
|
|
341
|
+
if (fkValue !== undefined && fkValue !== null) {
|
|
342
|
+
entity[relation.foreignKey] = fkValue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
foreignKeys = getForeignKeys();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!foreignKeys.size) {
|
|
351
|
+
return new Map();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
232
355
|
const pkColumn = relation.target.columns[targetKey];
|
|
233
356
|
if (!pkColumn) return new Map();
|
|
234
357
|
|
|
235
|
-
const
|
|
236
|
-
|
|
358
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
359
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
360
|
+
if (!selectedColumns.includes(targetKey)) {
|
|
361
|
+
selectedColumns.push(targetKey);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const selection = buildColumnSelection(
|
|
365
|
+
relation.target,
|
|
366
|
+
selectedColumns,
|
|
367
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys, selection, options?.filter);
|
|
371
|
+
const grouped = groupRowsByUnique(rows, targetKey);
|
|
372
|
+
|
|
373
|
+
if (!requestedColumns) return grouped;
|
|
374
|
+
|
|
375
|
+
const visibleColumns = new Set(selectedColumns);
|
|
376
|
+
const filtered = new Map<string, Record<string, unknown>>();
|
|
377
|
+
for (const [key, row] of grouped.entries()) {
|
|
378
|
+
filtered.set(key, filterRow(row, visibleColumns));
|
|
379
|
+
}
|
|
380
|
+
return filtered;
|
|
237
381
|
};
|
|
238
382
|
|
|
239
383
|
/**
|
|
@@ -247,8 +391,9 @@ export const loadBelongsToRelation = async (
|
|
|
247
391
|
export const loadBelongsToManyRelation = async (
|
|
248
392
|
ctx: EntityContext,
|
|
249
393
|
rootTable: TableDef,
|
|
250
|
-
|
|
251
|
-
relation: BelongsToManyRelation
|
|
394
|
+
relationName: string,
|
|
395
|
+
relation: BelongsToManyRelation,
|
|
396
|
+
options?: RelationIncludeOptions
|
|
252
397
|
): Promise<Map<string, Rows>> => {
|
|
253
398
|
const rootKey = relation.localKey || findPrimaryKey(rootTable);
|
|
254
399
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
@@ -261,9 +406,32 @@ export const loadBelongsToManyRelation = async (
|
|
|
261
406
|
const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
262
407
|
if (!pivotColumn) return new Map();
|
|
263
408
|
|
|
264
|
-
const
|
|
409
|
+
const pivotColumnsRequested = hasColumns(options?.pivot?.columns) ? [...options!.pivot!.columns] : undefined;
|
|
410
|
+
const useIncludeDefaults = options !== undefined;
|
|
411
|
+
let pivotSelectedColumns: string[];
|
|
412
|
+
if (pivotColumnsRequested) {
|
|
413
|
+
pivotSelectedColumns = [...pivotColumnsRequested];
|
|
414
|
+
} else if (useIncludeDefaults) {
|
|
415
|
+
const pivotPk = relation.pivotPrimaryKey || findPrimaryKey(relation.pivotTable);
|
|
416
|
+
pivotSelectedColumns = relation.defaultPivotColumns ?? buildDefaultPivotColumns(relation, pivotPk);
|
|
417
|
+
} else {
|
|
418
|
+
pivotSelectedColumns = Object.keys(relation.pivotTable.columns);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const pivotQueryColumns = new Set(pivotSelectedColumns);
|
|
422
|
+
pivotQueryColumns.add(relation.pivotForeignKeyToRoot);
|
|
423
|
+
pivotQueryColumns.add(relation.pivotForeignKeyToTarget);
|
|
424
|
+
|
|
425
|
+
const pivotSelection = buildColumnSelection(
|
|
426
|
+
relation.pivotTable,
|
|
427
|
+
Array.from(pivotQueryColumns),
|
|
428
|
+
column => `Column '${column}' not found on pivot table '${relation.pivotTable.name}'`
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds, pivotSelection);
|
|
265
432
|
const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
|
|
266
433
|
const targetIds = new Set<unknown>();
|
|
434
|
+
const pivotVisibleColumns = new Set(pivotSelectedColumns);
|
|
267
435
|
|
|
268
436
|
for (const pivot of pivotRows) {
|
|
269
437
|
const rootValue = pivot[relation.pivotForeignKeyToRoot];
|
|
@@ -274,7 +442,7 @@ export const loadBelongsToManyRelation = async (
|
|
|
274
442
|
const bucket = rootLookup.get(toKey(rootValue)) ?? [];
|
|
275
443
|
bucket.push({
|
|
276
444
|
targetId: targetValue,
|
|
277
|
-
pivot:
|
|
445
|
+
pivot: pivotVisibleColumns.size ? filterRow(pivot, pivotVisibleColumns) : {}
|
|
278
446
|
});
|
|
279
447
|
rootLookup.set(toKey(rootValue), bucket);
|
|
280
448
|
targetIds.add(targetValue);
|
|
@@ -288,8 +456,23 @@ export const loadBelongsToManyRelation = async (
|
|
|
288
456
|
const targetPkColumn = relation.target.columns[targetKey];
|
|
289
457
|
if (!targetPkColumn) return new Map();
|
|
290
458
|
|
|
291
|
-
const
|
|
459
|
+
const targetRequestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
460
|
+
const targetSelectedColumns = targetRequestedColumns
|
|
461
|
+
? [...targetRequestedColumns]
|
|
462
|
+
: Object.keys(relation.target.columns);
|
|
463
|
+
if (!targetSelectedColumns.includes(targetKey)) {
|
|
464
|
+
targetSelectedColumns.push(targetKey);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const targetSelection = buildColumnSelection(
|
|
468
|
+
relation.target,
|
|
469
|
+
targetSelectedColumns,
|
|
470
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const targetRows = await fetchRowsForKeys(ctx, relation.target, targetPkColumn, targetIds, targetSelection, options?.filter);
|
|
292
474
|
const targetMap = groupRowsByUnique(targetRows, targetKey);
|
|
475
|
+
const targetVisibleColumns = new Set(targetSelectedColumns);
|
|
293
476
|
const result = new Map<string, Rows>();
|
|
294
477
|
|
|
295
478
|
for (const [rootId, entries] of rootLookup.entries()) {
|
|
@@ -298,7 +481,7 @@ export const loadBelongsToManyRelation = async (
|
|
|
298
481
|
const targetRow = targetMap.get(toKey(entry.targetId));
|
|
299
482
|
if (!targetRow) continue;
|
|
300
483
|
bucket.push({
|
|
301
|
-
...targetRow,
|
|
484
|
+
...(targetRequestedColumns ? filterRow(targetRow, targetVisibleColumns) : targetRow),
|
|
302
485
|
_pivot: entry.pivot
|
|
303
486
|
});
|
|
304
487
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BelongsToReferenceApi } from '../../schema/types.js';
|
|
2
2
|
import { EntityContext } from '../entity-context.js';
|
|
3
3
|
import { RelationKey } from '../runtime-types.js';
|
|
4
4
|
import { BelongsToRelation } from '../../schema/relation.js';
|
|
@@ -20,10 +20,27 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Default implementation of a belongs-to reference.
|
|
25
|
+
* Manages a reference to a parent entity from a child entity through a foreign key.
|
|
26
|
+
*
|
|
27
|
+
* @template TParent The type of the parent entity.
|
|
28
|
+
*/
|
|
29
|
+
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
24
30
|
private loaded = false;
|
|
25
31
|
private current: TParent | null = null;
|
|
26
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @param ctx The entity context for tracking changes.
|
|
35
|
+
* @param meta Metadata for the child entity.
|
|
36
|
+
* @param root The child entity instance (carrying the foreign key).
|
|
37
|
+
* @param relationName The name of the relation.
|
|
38
|
+
* @param relation Relation definition.
|
|
39
|
+
* @param rootTable Table definition of the child entity.
|
|
40
|
+
* @param loader Function to load the parent entity.
|
|
41
|
+
* @param createEntity Function to create entity instances from rows.
|
|
42
|
+
* @param targetKey The primary key of the target (parent) table.
|
|
43
|
+
*/
|
|
27
44
|
constructor(
|
|
28
45
|
private readonly ctx: EntityContext,
|
|
29
46
|
private readonly meta: EntityMeta<TableDef>,
|
|
@@ -69,15 +69,29 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
69
69
|
this.items = rows.map(row => this.createEntity(row));
|
|
70
70
|
this.loaded = true;
|
|
71
71
|
return this.items;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Gets the current items in the collection.
|
|
76
|
-
* @returns Array of child entities
|
|
77
|
-
*/
|
|
78
|
-
getItems(): TChild[] {
|
|
79
|
-
return this.items;
|
|
80
|
-
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the current items in the collection.
|
|
76
|
+
* @returns Array of child entities
|
|
77
|
+
*/
|
|
78
|
+
getItems(): TChild[] {
|
|
79
|
+
return this.items;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Array-compatible length for testing frameworks.
|
|
84
|
+
*/
|
|
85
|
+
get length(): number {
|
|
86
|
+
return this.items.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enables iteration over the collection like an array.
|
|
91
|
+
*/
|
|
92
|
+
[Symbol.iterator](): Iterator<TChild> {
|
|
93
|
+
return this.items[Symbol.iterator]();
|
|
94
|
+
}
|
|
81
95
|
|
|
82
96
|
/**
|
|
83
97
|
* Adds a new child entity to the collection.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HasOneReferenceApi } from '../../schema/types.js';
|
|
2
2
|
import { EntityContext } from '../entity-context.js';
|
|
3
3
|
import { RelationKey } from '../runtime-types.js';
|
|
4
4
|
import { HasOneRelation } from '../../schema/relation.js';
|
|
@@ -20,10 +20,27 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Default implementation of a has-one reference.
|
|
25
|
+
* Manages a reference to a child entity where the child carries the foreign key.
|
|
26
|
+
*
|
|
27
|
+
* @template TChild The type of the child entity.
|
|
28
|
+
*/
|
|
29
|
+
export class DefaultHasOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
|
|
24
30
|
private loaded = false;
|
|
25
31
|
private current: TChild | null = null;
|
|
26
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @param ctx The entity context for tracking changes.
|
|
35
|
+
* @param meta Metadata for the parent entity.
|
|
36
|
+
* @param root The parent entity instance.
|
|
37
|
+
* @param relationName The name of the relation.
|
|
38
|
+
* @param relation Relation definition.
|
|
39
|
+
* @param rootTable Table definition of the parent entity.
|
|
40
|
+
* @param loader Function to load the child entity.
|
|
41
|
+
* @param createEntity Function to create entity instances from rows.
|
|
42
|
+
* @param localKey The local key on the parent entity used for the relation.
|
|
43
|
+
*/
|
|
27
44
|
constructor(
|
|
28
45
|
private readonly ctx: EntityContext,
|
|
29
46
|
private readonly meta: EntityMeta<TableDef>,
|
|
@@ -21,10 +21,28 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Default implementation of a many-to-many collection.
|
|
26
|
+
* Manages the relationship between two entities through a pivot table.
|
|
27
|
+
* Supports lazy loading, attaching/detaching entities, and syncing by IDs.
|
|
28
|
+
*
|
|
29
|
+
* @template TTarget The type of the target entities in the collection.
|
|
30
|
+
*/
|
|
24
31
|
export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
|
|
25
32
|
private loaded = false;
|
|
26
33
|
private items: TTarget[] = [];
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param ctx The entity context for tracking changes.
|
|
37
|
+
* @param meta Metadata for the root entity.
|
|
38
|
+
* @param root The root entity instance.
|
|
39
|
+
* @param relationName The name of the relation.
|
|
40
|
+
* @param relation Relation definition.
|
|
41
|
+
* @param rootTable Table definition of the root entity.
|
|
42
|
+
* @param loader Function to load the collection items.
|
|
43
|
+
* @param createEntity Function to create entity instances from rows.
|
|
44
|
+
* @param localKey The local key used for joining.
|
|
45
|
+
*/
|
|
28
46
|
constructor(
|
|
29
47
|
private readonly ctx: EntityContext,
|
|
30
48
|
private readonly meta: EntityMeta<TableDef>,
|
|
@@ -40,6 +58,10 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
40
58
|
this.hydrateFromCache();
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Loads the collection items if not already loaded.
|
|
63
|
+
* @returns A promise that resolves to the array of target entities.
|
|
64
|
+
*/
|
|
43
65
|
async load(): Promise<TTarget[]> {
|
|
44
66
|
if (this.loaded) return this.items;
|
|
45
67
|
const map = await this.loader();
|
|
@@ -56,10 +78,33 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
56
78
|
return this.items;
|
|
57
79
|
}
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Returns the currently loaded items.
|
|
83
|
+
* @returns Array of target entities.
|
|
84
|
+
*/
|
|
85
|
+
getItems(): TTarget[] {
|
|
86
|
+
return this.items;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Array-compatible length for testing frameworks.
|
|
91
|
+
*/
|
|
92
|
+
get length(): number {
|
|
93
|
+
return this.items.length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enables iteration over the collection like an array.
|
|
98
|
+
*/
|
|
99
|
+
[Symbol.iterator](): Iterator<TTarget> {
|
|
100
|
+
return this.items[Symbol.iterator]();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Attaches an entity to the collection.
|
|
105
|
+
* Registers an 'attach' change in the entity context.
|
|
106
|
+
* @param target Entity instance or its primary key value.
|
|
107
|
+
*/
|
|
63
108
|
attach(target: TTarget | number | string): void {
|
|
64
109
|
const entity = this.ensureEntity(target);
|
|
65
110
|
const id = this.extractId(entity);
|
|
@@ -80,6 +125,11 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
80
125
|
);
|
|
81
126
|
}
|
|
82
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Detaches an entity from the collection.
|
|
130
|
+
* Registers a 'detach' change in the entity context.
|
|
131
|
+
* @param target Entity instance or its primary key value.
|
|
132
|
+
*/
|
|
83
133
|
detach(target: TTarget | number | string): void {
|
|
84
134
|
const id = typeof target === 'number' || typeof target === 'string'
|
|
85
135
|
? target
|
|
@@ -101,6 +151,11 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
101
151
|
);
|
|
102
152
|
}
|
|
103
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Syncs the collection with a list of IDs.
|
|
156
|
+
* Attaches missing IDs and detaches IDs not in the list.
|
|
157
|
+
* @param ids Array of primary key values to sync with.
|
|
158
|
+
*/
|
|
104
159
|
async syncByIds(ids: (number | string)[]): Promise<void> {
|
|
105
160
|
await this.load();
|
|
106
161
|
const normalized = new Set(ids.map(id => toKey(id)));
|
|
@@ -10,8 +10,8 @@ type AnyFn = (...args: unknown[]) => unknown;
|
|
|
10
10
|
|
|
11
11
|
type RelationWrapper =
|
|
12
12
|
| HasManyCollection<unknown>
|
|
13
|
-
| HasOneReference
|
|
14
|
-
| BelongsToReference
|
|
13
|
+
| HasOneReference
|
|
14
|
+
| BelongsToReference
|
|
15
15
|
| ManyToManyCollection<unknown>;
|
|
16
16
|
|
|
17
17
|
type FunctionKeys<T> = {
|
package/src/orm/save-graph.ts
CHANGED
|
@@ -184,15 +184,15 @@ const handleHasMany = async (
|
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
const handleHasOne = async (
|
|
188
|
-
session: OrmSession,
|
|
189
|
-
root: AnyEntity,
|
|
190
|
-
relationName: string,
|
|
191
|
-
relation: HasOneRelation,
|
|
192
|
-
payload: unknown,
|
|
193
|
-
options: SaveGraphOptions
|
|
194
|
-
): Promise<void> => {
|
|
195
|
-
const ref = root[relationName] as unknown as HasOneReference<
|
|
187
|
+
const handleHasOne = async (
|
|
188
|
+
session: OrmSession,
|
|
189
|
+
root: AnyEntity,
|
|
190
|
+
relationName: string,
|
|
191
|
+
relation: HasOneRelation,
|
|
192
|
+
payload: unknown,
|
|
193
|
+
options: SaveGraphOptions
|
|
194
|
+
): Promise<void> => {
|
|
195
|
+
const ref = root[relationName] as unknown as HasOneReference<object>;
|
|
196
196
|
if (payload === undefined) return;
|
|
197
197
|
if (payload === null) {
|
|
198
198
|
ref.set(null);
|
|
@@ -212,15 +212,15 @@ const handleHasOne = async (
|
|
|
212
212
|
}
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
-
const handleBelongsTo = async (
|
|
216
|
-
session: OrmSession,
|
|
217
|
-
root: AnyEntity,
|
|
218
|
-
relationName: string,
|
|
219
|
-
relation: BelongsToRelation,
|
|
220
|
-
payload: unknown,
|
|
221
|
-
options: SaveGraphOptions
|
|
222
|
-
): Promise<void> => {
|
|
223
|
-
const ref = root[relationName] as unknown as BelongsToReference<
|
|
215
|
+
const handleBelongsTo = async (
|
|
216
|
+
session: OrmSession,
|
|
217
|
+
root: AnyEntity,
|
|
218
|
+
relationName: string,
|
|
219
|
+
relation: BelongsToRelation,
|
|
220
|
+
payload: unknown,
|
|
221
|
+
options: SaveGraphOptions
|
|
222
|
+
): Promise<void> => {
|
|
223
|
+
const ref = root[relationName] as unknown as BelongsToReference<object>;
|
|
224
224
|
if (payload === undefined) return;
|
|
225
225
|
if (payload === null) {
|
|
226
226
|
ref.set(null);
|