metal-orm 1.0.58 → 1.0.60
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 +34 -31
- package/dist/index.cjs +1583 -901
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +400 -129
- package/dist/index.d.ts +400 -129
- package/dist/index.js +1575 -901
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ddl/schema-generator.ts +44 -1
- package/src/decorators/bootstrap.ts +183 -146
- package/src/decorators/column-decorator.ts +8 -49
- package/src/decorators/decorator-metadata.ts +10 -46
- package/src/decorators/entity.ts +30 -40
- package/src/decorators/relations.ts +30 -56
- package/src/index.ts +7 -7
- package/src/orm/entity-hydration.ts +72 -0
- package/src/orm/entity-meta.ts +13 -11
- package/src/orm/entity-metadata.ts +240 -238
- package/src/orm/entity-relation-cache.ts +39 -0
- package/src/orm/entity-relations.ts +207 -0
- package/src/orm/entity.ts +124 -410
- package/src/orm/execute.ts +4 -4
- package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
- package/src/orm/lazy-batch/belongs-to.ts +108 -0
- package/src/orm/lazy-batch/has-many.ts +69 -0
- package/src/orm/lazy-batch/has-one.ts +68 -0
- package/src/orm/lazy-batch/shared.ts +125 -0
- package/src/orm/lazy-batch.ts +4 -492
- package/src/orm/relations/many-to-many.ts +2 -1
- package/src/query-builder/relation-cte-builder.ts +63 -0
- package/src/query-builder/relation-filter-utils.ts +159 -0
- package/src/query-builder/relation-include-strategies.ts +177 -0
- package/src/query-builder/relation-join-planner.ts +80 -0
- package/src/query-builder/relation-service.ts +119 -479
- package/src/query-builder/relation-types.ts +41 -10
- package/src/query-builder/select/projection-facet.ts +23 -23
- package/src/query-builder/select/select-operations.ts +145 -0
- package/src/query-builder/select.ts +329 -221
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- package/src/schema/types.ts +14 -12
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { BelongsToManyRelation } from '../../schema/relation.js';
|
|
3
|
+
import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
|
|
4
|
+
import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
|
|
5
|
+
import { buildDefaultPivotColumns } from '../../query-builder/relation-utils.js';
|
|
6
|
+
import { EntityContext } from '../entity-context.js';
|
|
7
|
+
import {
|
|
8
|
+
buildColumnSelection,
|
|
9
|
+
collectKeysFromRoots,
|
|
10
|
+
fetchRowsForKeys,
|
|
11
|
+
filterRow,
|
|
12
|
+
groupRowsByUnique,
|
|
13
|
+
hasColumns,
|
|
14
|
+
toKey,
|
|
15
|
+
Rows
|
|
16
|
+
} from './shared.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads related entities for a belongs-to-many relation in batch, including pivot data.
|
|
20
|
+
* @param ctx - The entity context.
|
|
21
|
+
* @param rootTable - The root table of the relation.
|
|
22
|
+
* @param _relationName - The name of the relation (unused).
|
|
23
|
+
* @param relation - The belongs-to-many relation definition.
|
|
24
|
+
* @returns A promise resolving to a map of root keys to arrays of related rows with pivot data.
|
|
25
|
+
*/
|
|
26
|
+
export const loadBelongsToManyRelation = async (
|
|
27
|
+
ctx: EntityContext,
|
|
28
|
+
rootTable: TableDef,
|
|
29
|
+
relationName: string,
|
|
30
|
+
relation: BelongsToManyRelation,
|
|
31
|
+
options?: RelationIncludeOptions
|
|
32
|
+
): Promise<Map<string, Rows>> => {
|
|
33
|
+
const rootKey = relation.localKey || findPrimaryKey(rootTable);
|
|
34
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
35
|
+
const rootIds = collectKeysFromRoots(roots, rootKey);
|
|
36
|
+
|
|
37
|
+
if (!rootIds.size) {
|
|
38
|
+
return new Map();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
42
|
+
if (!pivotColumn) return new Map();
|
|
43
|
+
|
|
44
|
+
const pivotColumnsRequested = hasColumns(options?.pivot?.columns) ? [...options!.pivot!.columns] : undefined;
|
|
45
|
+
const useIncludeDefaults = options !== undefined;
|
|
46
|
+
let pivotSelectedColumns: string[];
|
|
47
|
+
if (pivotColumnsRequested) {
|
|
48
|
+
pivotSelectedColumns = [...pivotColumnsRequested];
|
|
49
|
+
} else if (useIncludeDefaults) {
|
|
50
|
+
const pivotPk = relation.pivotPrimaryKey || findPrimaryKey(relation.pivotTable);
|
|
51
|
+
pivotSelectedColumns = relation.defaultPivotColumns ?? buildDefaultPivotColumns(relation, pivotPk);
|
|
52
|
+
} else {
|
|
53
|
+
pivotSelectedColumns = Object.keys(relation.pivotTable.columns);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pivotQueryColumns = new Set(pivotSelectedColumns);
|
|
57
|
+
pivotQueryColumns.add(relation.pivotForeignKeyToRoot);
|
|
58
|
+
pivotQueryColumns.add(relation.pivotForeignKeyToTarget);
|
|
59
|
+
|
|
60
|
+
const pivotSelection = buildColumnSelection(
|
|
61
|
+
relation.pivotTable,
|
|
62
|
+
Array.from(pivotQueryColumns),
|
|
63
|
+
column => `Column '${column}' not found on pivot table '${relation.pivotTable.name}'`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds, pivotSelection);
|
|
67
|
+
const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
|
|
68
|
+
const targetIds = new Set<unknown>();
|
|
69
|
+
const pivotVisibleColumns = new Set(pivotSelectedColumns);
|
|
70
|
+
|
|
71
|
+
for (const pivot of pivotRows) {
|
|
72
|
+
const rootValue = pivot[relation.pivotForeignKeyToRoot];
|
|
73
|
+
const targetValue = pivot[relation.pivotForeignKeyToTarget];
|
|
74
|
+
if (rootValue === null || rootValue === undefined || targetValue === null || targetValue === undefined) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const bucket = rootLookup.get(toKey(rootValue)) ?? [];
|
|
78
|
+
bucket.push({
|
|
79
|
+
targetId: targetValue,
|
|
80
|
+
pivot: pivotVisibleColumns.size ? filterRow(pivot, pivotVisibleColumns) : {}
|
|
81
|
+
});
|
|
82
|
+
rootLookup.set(toKey(rootValue), bucket);
|
|
83
|
+
targetIds.add(targetValue);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!targetIds.size) {
|
|
87
|
+
return new Map();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targetKey = relation.targetKey || findPrimaryKey(relation.target);
|
|
91
|
+
const targetPkColumn = relation.target.columns[targetKey];
|
|
92
|
+
if (!targetPkColumn) return new Map();
|
|
93
|
+
|
|
94
|
+
const targetRequestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
95
|
+
const targetSelectedColumns = targetRequestedColumns
|
|
96
|
+
? [...targetRequestedColumns]
|
|
97
|
+
: Object.keys(relation.target.columns);
|
|
98
|
+
if (!targetSelectedColumns.includes(targetKey)) {
|
|
99
|
+
targetSelectedColumns.push(targetKey);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const targetSelection = buildColumnSelection(
|
|
103
|
+
relation.target,
|
|
104
|
+
targetSelectedColumns,
|
|
105
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const targetRows = await fetchRowsForKeys(
|
|
109
|
+
ctx,
|
|
110
|
+
relation.target,
|
|
111
|
+
targetPkColumn,
|
|
112
|
+
targetIds,
|
|
113
|
+
targetSelection,
|
|
114
|
+
options?.filter
|
|
115
|
+
);
|
|
116
|
+
const targetMap = groupRowsByUnique(targetRows, targetKey);
|
|
117
|
+
const targetVisibleColumns = new Set(targetSelectedColumns);
|
|
118
|
+
const result = new Map<string, Rows>();
|
|
119
|
+
|
|
120
|
+
for (const [rootId, entries] of rootLookup.entries()) {
|
|
121
|
+
const bucket: Rows = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const targetRow = targetMap.get(toKey(entry.targetId));
|
|
124
|
+
if (!targetRow) continue;
|
|
125
|
+
bucket.push({
|
|
126
|
+
...(targetRequestedColumns ? filterRow(targetRow, targetVisibleColumns) : targetRow),
|
|
127
|
+
_pivot: entry.pivot
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
result.set(rootId, bucket);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { BelongsToRelation } from '../../schema/relation.js';
|
|
3
|
+
import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
|
|
4
|
+
import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
|
|
5
|
+
import { EntityContext } from '../entity-context.js';
|
|
6
|
+
import {
|
|
7
|
+
buildColumnSelection,
|
|
8
|
+
collectKeysFromRoots,
|
|
9
|
+
fetchRowsForKeys,
|
|
10
|
+
filterRow,
|
|
11
|
+
groupRowsByUnique,
|
|
12
|
+
hasColumns
|
|
13
|
+
} from './shared.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads related entities for a belongs-to relation in batch.
|
|
17
|
+
* @param ctx - The entity context.
|
|
18
|
+
* @param rootTable - The root table of the relation.
|
|
19
|
+
* @param _relationName - The name of the relation (unused).
|
|
20
|
+
* @param relation - The belongs-to relation definition.
|
|
21
|
+
* @returns A promise resolving to a map of foreign keys to single related rows.
|
|
22
|
+
*/
|
|
23
|
+
export const loadBelongsToRelation = async (
|
|
24
|
+
ctx: EntityContext,
|
|
25
|
+
rootTable: TableDef,
|
|
26
|
+
relationName: string,
|
|
27
|
+
relation: BelongsToRelation,
|
|
28
|
+
options?: RelationIncludeOptions
|
|
29
|
+
): Promise<Map<string, Record<string, unknown>>> => {
|
|
30
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
31
|
+
|
|
32
|
+
const getForeignKeys = (): Set<unknown> => collectKeysFromRoots(roots, relation.foreignKey);
|
|
33
|
+
let foreignKeys = getForeignKeys();
|
|
34
|
+
|
|
35
|
+
if (!foreignKeys.size) {
|
|
36
|
+
const pkName = findPrimaryKey(rootTable);
|
|
37
|
+
const pkColumn = rootTable.columns[pkName];
|
|
38
|
+
const fkColumn = rootTable.columns[relation.foreignKey];
|
|
39
|
+
|
|
40
|
+
if (pkColumn && fkColumn) {
|
|
41
|
+
const missingKeys = new Set<unknown>();
|
|
42
|
+
const entityByPk = new Map<unknown, Record<string, unknown>>();
|
|
43
|
+
|
|
44
|
+
for (const tracked of roots) {
|
|
45
|
+
const entity = tracked.entity as Record<string, unknown>;
|
|
46
|
+
const pkValue = entity[pkName];
|
|
47
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
48
|
+
const fkValue = entity[relation.foreignKey];
|
|
49
|
+
if (fkValue === undefined || fkValue === null) {
|
|
50
|
+
missingKeys.add(pkValue);
|
|
51
|
+
entityByPk.set(pkValue, entity);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (missingKeys.size) {
|
|
56
|
+
const selection = buildColumnSelection(
|
|
57
|
+
rootTable,
|
|
58
|
+
[pkName, relation.foreignKey],
|
|
59
|
+
column => `Column '${column}' not found on table '${rootTable.name}'`
|
|
60
|
+
);
|
|
61
|
+
const keyRows = await fetchRowsForKeys(ctx, rootTable, pkColumn, missingKeys, selection);
|
|
62
|
+
for (const row of keyRows) {
|
|
63
|
+
const pkValue = row[pkName];
|
|
64
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
65
|
+
const entity = entityByPk.get(pkValue);
|
|
66
|
+
if (!entity) continue;
|
|
67
|
+
const fkValue = row[relation.foreignKey];
|
|
68
|
+
if (fkValue !== undefined && fkValue !== null) {
|
|
69
|
+
entity[relation.foreignKey] = fkValue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
foreignKeys = getForeignKeys();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!foreignKeys.size) {
|
|
78
|
+
return new Map();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
82
|
+
const pkColumn = relation.target.columns[targetKey];
|
|
83
|
+
if (!pkColumn) return new Map();
|
|
84
|
+
|
|
85
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
86
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
87
|
+
if (!selectedColumns.includes(targetKey)) {
|
|
88
|
+
selectedColumns.push(targetKey);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const selection = buildColumnSelection(
|
|
92
|
+
relation.target,
|
|
93
|
+
selectedColumns,
|
|
94
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys, selection, options?.filter);
|
|
98
|
+
const grouped = groupRowsByUnique(rows, targetKey);
|
|
99
|
+
|
|
100
|
+
if (!requestedColumns) return grouped;
|
|
101
|
+
|
|
102
|
+
const visibleColumns = new Set(selectedColumns);
|
|
103
|
+
const filtered = new Map<string, Record<string, unknown>>();
|
|
104
|
+
for (const [key, row] of grouped.entries()) {
|
|
105
|
+
filtered.set(key, filterRow(row, visibleColumns));
|
|
106
|
+
}
|
|
107
|
+
return filtered;
|
|
108
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { HasManyRelation } from '../../schema/relation.js';
|
|
3
|
+
import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
|
|
4
|
+
import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
|
|
5
|
+
import { EntityContext } from '../entity-context.js';
|
|
6
|
+
import {
|
|
7
|
+
buildColumnSelection,
|
|
8
|
+
collectKeysFromRoots,
|
|
9
|
+
fetchRowsForKeys,
|
|
10
|
+
filterRows,
|
|
11
|
+
groupRowsByMany,
|
|
12
|
+
hasColumns,
|
|
13
|
+
Rows
|
|
14
|
+
} from './shared.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads related entities for a has-many relation in batch.
|
|
18
|
+
* @param ctx - The entity context.
|
|
19
|
+
* @param rootTable - The root table of the relation.
|
|
20
|
+
* @param _relationName - The name of the relation (unused).
|
|
21
|
+
* @param relation - The has-many relation definition.
|
|
22
|
+
* @returns A promise resolving to a map of root keys to arrays of related rows.
|
|
23
|
+
*/
|
|
24
|
+
export const loadHasManyRelation = async (
|
|
25
|
+
ctx: EntityContext,
|
|
26
|
+
rootTable: TableDef,
|
|
27
|
+
relationName: string,
|
|
28
|
+
relation: HasManyRelation,
|
|
29
|
+
options?: RelationIncludeOptions
|
|
30
|
+
): Promise<Map<string, Rows>> => {
|
|
31
|
+
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
32
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
33
|
+
const keys = collectKeysFromRoots(roots, localKey);
|
|
34
|
+
|
|
35
|
+
if (!keys.size) {
|
|
36
|
+
return new Map();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
40
|
+
if (!fkColumn) return new Map();
|
|
41
|
+
|
|
42
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
43
|
+
const targetPrimaryKey = findPrimaryKey(relation.target);
|
|
44
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
45
|
+
if (!selectedColumns.includes(targetPrimaryKey)) {
|
|
46
|
+
selectedColumns.push(targetPrimaryKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const queryColumns = new Set(selectedColumns);
|
|
50
|
+
queryColumns.add(relation.foreignKey);
|
|
51
|
+
|
|
52
|
+
const selection = buildColumnSelection(
|
|
53
|
+
relation.target,
|
|
54
|
+
Array.from(queryColumns),
|
|
55
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, options?.filter);
|
|
59
|
+
const grouped = groupRowsByMany(rows, relation.foreignKey);
|
|
60
|
+
|
|
61
|
+
if (!requestedColumns) return grouped;
|
|
62
|
+
|
|
63
|
+
const visibleColumns = new Set(selectedColumns);
|
|
64
|
+
const filtered = new Map<string, Rows>();
|
|
65
|
+
for (const [key, bucket] of grouped.entries()) {
|
|
66
|
+
filtered.set(key, filterRows(bucket, visibleColumns));
|
|
67
|
+
}
|
|
68
|
+
return filtered;
|
|
69
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { HasOneRelation } from '../../schema/relation.js';
|
|
3
|
+
import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
|
|
4
|
+
import { RelationIncludeOptions } from '../../query-builder/relation-types.js';
|
|
5
|
+
import { EntityContext } from '../entity-context.js';
|
|
6
|
+
import {
|
|
7
|
+
buildColumnSelection,
|
|
8
|
+
collectKeysFromRoots,
|
|
9
|
+
fetchRowsForKeys,
|
|
10
|
+
filterRow,
|
|
11
|
+
groupRowsByUnique,
|
|
12
|
+
hasColumns
|
|
13
|
+
} from './shared.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads related entities for a has-one relation in batch.
|
|
17
|
+
* @param ctx - The entity context.
|
|
18
|
+
* @param rootTable - The root table of the relation.
|
|
19
|
+
* @param _relationName - The name of the relation (unused).
|
|
20
|
+
* @param relation - The has-one relation definition.
|
|
21
|
+
* @returns A promise resolving to a map of root keys to single related rows.
|
|
22
|
+
*/
|
|
23
|
+
export const loadHasOneRelation = async (
|
|
24
|
+
ctx: EntityContext,
|
|
25
|
+
rootTable: TableDef,
|
|
26
|
+
relationName: string,
|
|
27
|
+
relation: HasOneRelation,
|
|
28
|
+
options?: RelationIncludeOptions
|
|
29
|
+
): Promise<Map<string, Record<string, unknown>>> => {
|
|
30
|
+
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
31
|
+
const roots = ctx.getEntitiesForTable(rootTable);
|
|
32
|
+
const keys = collectKeysFromRoots(roots, localKey);
|
|
33
|
+
|
|
34
|
+
if (!keys.size) {
|
|
35
|
+
return new Map();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
39
|
+
if (!fkColumn) return new Map();
|
|
40
|
+
|
|
41
|
+
const requestedColumns = hasColumns(options?.columns) ? [...options!.columns] : undefined;
|
|
42
|
+
const targetPrimaryKey = findPrimaryKey(relation.target);
|
|
43
|
+
const selectedColumns = requestedColumns ? [...requestedColumns] : Object.keys(relation.target.columns);
|
|
44
|
+
if (!selectedColumns.includes(targetPrimaryKey)) {
|
|
45
|
+
selectedColumns.push(targetPrimaryKey);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const queryColumns = new Set(selectedColumns);
|
|
49
|
+
queryColumns.add(relation.foreignKey);
|
|
50
|
+
|
|
51
|
+
const selection = buildColumnSelection(
|
|
52
|
+
relation.target,
|
|
53
|
+
Array.from(queryColumns),
|
|
54
|
+
column => `Column '${column}' not found on relation '${relationName}'`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys, selection, options?.filter);
|
|
58
|
+
const grouped = groupRowsByUnique(rows, relation.foreignKey);
|
|
59
|
+
|
|
60
|
+
if (!requestedColumns) return grouped;
|
|
61
|
+
|
|
62
|
+
const visibleColumns = new Set(selectedColumns);
|
|
63
|
+
const filtered = new Map<string, Record<string, unknown>>();
|
|
64
|
+
for (const [key, row] of grouped.entries()) {
|
|
65
|
+
filtered.set(key, filterRow(row, visibleColumns));
|
|
66
|
+
}
|
|
67
|
+
return filtered;
|
|
68
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { SelectQueryBuilder } from '../../query-builder/select.js';
|
|
3
|
+
import { ExpressionNode, inList, LiteralNode } from '../../core/ast/expression.js';
|
|
4
|
+
import type { QueryResult } from '../../core/execution/db-executor.js';
|
|
5
|
+
import { ColumnDef } from '../../schema/column-types.js';
|
|
6
|
+
import { EntityContext } from '../entity-context.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An array of database rows, each represented as a record of string keys to unknown values.
|
|
10
|
+
*/
|
|
11
|
+
export type Rows = Record<string, unknown>[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents a single tracked entity from the EntityContext for a table.
|
|
15
|
+
*/
|
|
16
|
+
export type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
|
|
17
|
+
|
|
18
|
+
export const hasColumns = (columns?: readonly string[]): columns is readonly string[] =>
|
|
19
|
+
Boolean(columns && columns.length > 0);
|
|
20
|
+
|
|
21
|
+
export const buildColumnSelection = (
|
|
22
|
+
table: TableDef,
|
|
23
|
+
columns: string[],
|
|
24
|
+
missingMsg: (col: string) => string
|
|
25
|
+
): Record<string, ColumnDef> => {
|
|
26
|
+
return columns.reduce((acc, column) => {
|
|
27
|
+
const def = table.columns[column];
|
|
28
|
+
if (!def) {
|
|
29
|
+
throw new Error(missingMsg(column));
|
|
30
|
+
}
|
|
31
|
+
acc[column] = def;
|
|
32
|
+
return acc;
|
|
33
|
+
}, {} as Record<string, ColumnDef>);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const filterRow = (row: Record<string, unknown>, columns: Set<string>): Record<string, unknown> => {
|
|
37
|
+
const filtered: Record<string, unknown> = {};
|
|
38
|
+
for (const column of columns) {
|
|
39
|
+
if (column in row) {
|
|
40
|
+
filtered[column] = row[column];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filtered;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const filterRows = (rows: Rows, columns: Set<string>): Rows => rows.map(row => filterRow(row, columns));
|
|
47
|
+
|
|
48
|
+
const rowsFromResults = (results: QueryResult[]): Rows => {
|
|
49
|
+
const rows: Rows = [];
|
|
50
|
+
for (const result of results) {
|
|
51
|
+
const { columns, values } = result;
|
|
52
|
+
for (const valueRow of values) {
|
|
53
|
+
const row: Record<string, unknown> = {};
|
|
54
|
+
columns.forEach((column, idx) => {
|
|
55
|
+
row[column] = valueRow[idx];
|
|
56
|
+
});
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return rows;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
|
|
64
|
+
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
65
|
+
const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
66
|
+
return rowsFromResults(results);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
70
|
+
|
|
71
|
+
export const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
|
|
72
|
+
const collected = new Set<unknown>();
|
|
73
|
+
for (const tracked of roots) {
|
|
74
|
+
const value = tracked.entity[key];
|
|
75
|
+
if (value !== null && value !== undefined) {
|
|
76
|
+
collected.add(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return collected;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
|
|
83
|
+
Array.from(keys) as (string | number | LiteralNode)[];
|
|
84
|
+
|
|
85
|
+
export const fetchRowsForKeys = async (
|
|
86
|
+
ctx: EntityContext,
|
|
87
|
+
table: TableDef,
|
|
88
|
+
column: ColumnDef,
|
|
89
|
+
keys: Set<unknown>,
|
|
90
|
+
selection: Record<string, ColumnDef>,
|
|
91
|
+
filter?: ExpressionNode
|
|
92
|
+
): Promise<Rows> => {
|
|
93
|
+
let qb = new SelectQueryBuilder(table).select(selection);
|
|
94
|
+
qb = qb.where(inList(column, buildInListValues(keys)));
|
|
95
|
+
if (filter) {
|
|
96
|
+
qb = qb.where(filter);
|
|
97
|
+
}
|
|
98
|
+
return executeQuery(ctx, qb);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
|
|
102
|
+
const grouped = new Map<string, Rows>();
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
const value = row[keyColumn];
|
|
105
|
+
if (value === null || value === undefined) continue;
|
|
106
|
+
const key = toKey(value);
|
|
107
|
+
const bucket = grouped.get(key) ?? [];
|
|
108
|
+
bucket.push(row);
|
|
109
|
+
grouped.set(key, bucket);
|
|
110
|
+
}
|
|
111
|
+
return grouped;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
|
|
115
|
+
const lookup = new Map<string, Record<string, unknown>>();
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
const value = row[keyColumn];
|
|
118
|
+
if (value === null || value === undefined) continue;
|
|
119
|
+
const key = toKey(value);
|
|
120
|
+
if (!lookup.has(key)) {
|
|
121
|
+
lookup.set(key, row);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return lookup;
|
|
125
|
+
};
|