metal-orm 1.0.43 → 1.0.45
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 +700 -557
- package/dist/index.cjs +896 -476
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1146 -275
- package/dist/index.d.ts +1146 -275
- package/dist/index.js +896 -474
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/adapters.ts +8 -2
- package/src/core/ast/builders.ts +105 -81
- package/src/core/ast/expression-builders.ts +430 -390
- package/src/core/ast/expression-visitor.ts +47 -8
- package/src/core/ast/helpers.ts +23 -0
- package/src/core/ast/join-node.ts +17 -1
- package/src/core/ddl/dialects/base-schema-dialect.ts +7 -1
- package/src/core/ddl/dialects/index.ts +1 -0
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -0
- package/src/core/ddl/introspect/catalogs/index.ts +1 -0
- package/src/core/ddl/introspect/catalogs/postgres.ts +2 -0
- package/src/core/ddl/introspect/context.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +13 -0
- package/src/core/ddl/introspect/mssql.ts +11 -0
- package/src/core/ddl/introspect/mysql.ts +2 -0
- package/src/core/ddl/introspect/postgres.ts +14 -0
- package/src/core/ddl/introspect/registry.ts +14 -0
- package/src/core/ddl/introspect/run-select.ts +13 -0
- package/src/core/ddl/introspect/sqlite.ts +22 -0
- package/src/core/ddl/introspect/utils.ts +18 -0
- package/src/core/ddl/naming-strategy.ts +6 -0
- package/src/core/ddl/schema-dialect.ts +19 -6
- package/src/core/ddl/schema-diff.ts +22 -0
- package/src/core/ddl/schema-generator.ts +22 -0
- package/src/core/ddl/schema-plan-executor.ts +6 -0
- package/src/core/ddl/schema-types.ts +6 -0
- package/src/core/dialect/abstract.ts +2 -2
- package/src/core/execution/pooling/pool.ts +12 -7
- package/src/core/functions/datetime.ts +57 -33
- package/src/core/functions/numeric.ts +95 -30
- package/src/core/functions/standard-strategy.ts +35 -0
- package/src/core/functions/text.ts +83 -22
- package/src/core/functions/types.ts +23 -8
- package/src/decorators/bootstrap.ts +16 -4
- package/src/decorators/column.ts +17 -0
- package/src/decorators/decorator-metadata.ts +27 -0
- package/src/decorators/entity.ts +8 -0
- package/src/decorators/index.ts +3 -0
- package/src/decorators/relations.ts +32 -0
- package/src/orm/als.ts +34 -9
- package/src/orm/entity-context.ts +54 -0
- package/src/orm/entity-metadata.ts +122 -9
- package/src/orm/execute.ts +15 -0
- package/src/orm/lazy-batch.ts +158 -98
- package/src/orm/relations/has-many.ts +44 -0
- package/src/orm/save-graph.ts +45 -0
- package/src/query/index.ts +74 -0
- package/src/query/target.ts +46 -0
- package/src/query-builder/delete-query-state.ts +30 -0
- package/src/query-builder/delete.ts +64 -19
- package/src/query-builder/hydration-manager.ts +46 -0
- package/src/query-builder/insert-query-state.ts +30 -0
- package/src/query-builder/insert.ts +46 -2
- package/src/query-builder/query-ast-service.ts +5 -0
- package/src/query-builder/query-resolution.ts +78 -0
- package/src/query-builder/raw-column-parser.ts +5 -0
- package/src/query-builder/relation-alias.ts +7 -0
- package/src/query-builder/relation-conditions.ts +61 -48
- package/src/query-builder/relation-service.ts +68 -63
- package/src/query-builder/relation-utils.ts +3 -0
- package/src/query-builder/select/cte-facet.ts +40 -0
- package/src/query-builder/select/from-facet.ts +80 -0
- package/src/query-builder/select/join-facet.ts +62 -0
- package/src/query-builder/select/predicate-facet.ts +103 -0
- package/src/query-builder/select/projection-facet.ts +69 -0
- package/src/query-builder/select/relation-facet.ts +81 -0
- package/src/query-builder/select/setop-facet.ts +36 -0
- package/src/query-builder/select-helpers.ts +13 -0
- package/src/query-builder/select-query-builder-deps.ts +19 -1
- package/src/query-builder/select-query-state.ts +2 -1
- package/src/query-builder/select.ts +795 -1163
- package/src/query-builder/update-query-state.ts +52 -0
- package/src/query-builder/update.ts +69 -19
- package/src/schema/table-guards.ts +31 -0
package/src/orm/lazy-batch.ts
CHANGED
|
@@ -7,14 +7,32 @@ import type { QueryResult } from '../core/execution/db-executor.js';
|
|
|
7
7
|
import { ColumnDef } from '../schema/column.js';
|
|
8
8
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* An array of database rows, each represented as a record of string keys to unknown values.
|
|
12
|
+
*/
|
|
10
13
|
type Rows = Record<string, unknown>[];
|
|
11
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Represents a single tracked entity from the EntityContext for a table.
|
|
17
|
+
*/
|
|
18
|
+
type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a record of all columns from the given table definition.
|
|
22
|
+
* @param table - The table definition to select columns from.
|
|
23
|
+
* @returns A record mapping column names to their definitions.
|
|
24
|
+
*/
|
|
12
25
|
const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
|
|
13
26
|
Object.entries(table.columns).reduce((acc, [name, def]) => {
|
|
14
27
|
acc[name] = def;
|
|
15
28
|
return acc;
|
|
16
29
|
}, {} as Record<string, ColumnDef>);
|
|
17
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Extracts rows from query results into a standardized format.
|
|
33
|
+
* @param results - The query results to process.
|
|
34
|
+
* @returns An array of rows as records.
|
|
35
|
+
*/
|
|
18
36
|
const rowsFromResults = (results: QueryResult[]): Rows => {
|
|
19
37
|
const rows: Rows = [];
|
|
20
38
|
for (const result of results) {
|
|
@@ -30,14 +48,115 @@ const rowsFromResults = (results: QueryResult[]): Rows => {
|
|
|
30
48
|
return rows;
|
|
31
49
|
};
|
|
32
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Executes a select query and returns the resulting rows.
|
|
53
|
+
* @param ctx - The entity context for execution.
|
|
54
|
+
* @param qb - The select query builder.
|
|
55
|
+
* @returns A promise resolving to the rows from the query.
|
|
56
|
+
*/
|
|
33
57
|
const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
|
|
34
58
|
const compiled = ctx.dialect.compileSelect(qb.getAST());
|
|
35
59
|
const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
|
|
36
60
|
return rowsFromResults(results);
|
|
37
61
|
};
|
|
38
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Converts a value to a string key, handling null and undefined as empty string.
|
|
65
|
+
* @param value - The value to convert.
|
|
66
|
+
* @returns The string representation of the value.
|
|
67
|
+
*/
|
|
39
68
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
40
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Collects unique keys from the root entities based on the specified key property.
|
|
72
|
+
* @param roots - The tracked entities to collect keys from.
|
|
73
|
+
* @param key - The property name to use as the key.
|
|
74
|
+
* @returns A set of unique key values.
|
|
75
|
+
*/
|
|
76
|
+
const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
|
|
77
|
+
const collected = new Set<unknown>();
|
|
78
|
+
for (const tracked of roots) {
|
|
79
|
+
const value = tracked.entity[key];
|
|
80
|
+
if (value !== null && value !== undefined) {
|
|
81
|
+
collected.add(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return collected;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Builds an array of values suitable for an IN list expression from a set of keys.
|
|
89
|
+
* @param keys - The set of keys to convert.
|
|
90
|
+
* @returns An array of string, number, or LiteralNode values.
|
|
91
|
+
*/
|
|
92
|
+
const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
|
|
93
|
+
Array.from(keys) as (string | number | LiteralNode)[];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetches rows from a table where the specified column matches any of the given keys.
|
|
97
|
+
* @param ctx - The entity context.
|
|
98
|
+
* @param table - The target table.
|
|
99
|
+
* @param column - The column to match against.
|
|
100
|
+
* @param keys - The set of keys to match.
|
|
101
|
+
* @returns A promise resolving to the matching rows.
|
|
102
|
+
*/
|
|
103
|
+
const fetchRowsForKeys = async (
|
|
104
|
+
ctx: EntityContext,
|
|
105
|
+
table: TableDef,
|
|
106
|
+
column: ColumnDef,
|
|
107
|
+
keys: Set<unknown>
|
|
108
|
+
): Promise<Rows> => {
|
|
109
|
+
const qb = new SelectQueryBuilder(table).select(selectAllColumns(table));
|
|
110
|
+
qb.where(inList(column, buildInListValues(keys)));
|
|
111
|
+
return executeQuery(ctx, qb);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Groups rows by the value of a key column, allowing multiple rows per key.
|
|
116
|
+
* @param rows - The rows to group.
|
|
117
|
+
* @param keyColumn - The column name to group by.
|
|
118
|
+
* @returns A map from key strings to arrays of rows.
|
|
119
|
+
*/
|
|
120
|
+
const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
|
|
121
|
+
const grouped = new Map<string, Rows>();
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const value = row[keyColumn];
|
|
124
|
+
if (value === null || value === undefined) continue;
|
|
125
|
+
const key = toKey(value);
|
|
126
|
+
const bucket = grouped.get(key) ?? [];
|
|
127
|
+
bucket.push(row);
|
|
128
|
+
grouped.set(key, bucket);
|
|
129
|
+
}
|
|
130
|
+
return grouped;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Groups rows by the value of a key column, keeping only one row per key.
|
|
135
|
+
* @param rows - The rows to group.
|
|
136
|
+
* @param keyColumn - The column name to group by.
|
|
137
|
+
* @returns A map from key strings to single rows.
|
|
138
|
+
*/
|
|
139
|
+
const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
|
|
140
|
+
const lookup = new Map<string, Record<string, unknown>>();
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
const value = row[keyColumn];
|
|
143
|
+
if (value === null || value === undefined) continue;
|
|
144
|
+
const key = toKey(value);
|
|
145
|
+
if (!lookup.has(key)) {
|
|
146
|
+
lookup.set(key, row);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return lookup;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Loads related entities for a has-many relation in batch.
|
|
154
|
+
* @param ctx - The entity context.
|
|
155
|
+
* @param rootTable - The root table of the relation.
|
|
156
|
+
* @param _relationName - The name of the relation (unused).
|
|
157
|
+
* @param relation - The has-many relation definition.
|
|
158
|
+
* @returns A promise resolving to a map of root keys to arrays of related rows.
|
|
159
|
+
*/
|
|
41
160
|
export const loadHasManyRelation = async (
|
|
42
161
|
ctx: EntityContext,
|
|
43
162
|
rootTable: TableDef,
|
|
@@ -46,41 +165,27 @@ export const loadHasManyRelation = async (
|
|
|
46
165
|
): Promise<Map<string, Rows>> => {
|
|
47
166
|
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
48
167
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
49
|
-
const keys =
|
|
50
|
-
|
|
51
|
-
for (const tracked of roots) {
|
|
52
|
-
const value = tracked.entity[localKey];
|
|
53
|
-
if (value !== null && value !== undefined) {
|
|
54
|
-
keys.add(value);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
168
|
+
const keys = collectKeysFromRoots(roots, localKey);
|
|
57
169
|
|
|
58
170
|
if (!keys.size) {
|
|
59
171
|
return new Map();
|
|
60
172
|
}
|
|
61
173
|
|
|
62
|
-
const selectMap = selectAllColumns(relation.target);
|
|
63
|
-
const fb = new SelectQueryBuilder(relation.target).select(selectMap);
|
|
64
174
|
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
65
175
|
if (!fkColumn) return new Map();
|
|
66
176
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const rows = await executeQuery(ctx, fb);
|
|
70
|
-
const grouped = new Map<string, Rows>();
|
|
71
|
-
|
|
72
|
-
for (const row of rows) {
|
|
73
|
-
const fkValue = row[relation.foreignKey];
|
|
74
|
-
if (fkValue === null || fkValue === undefined) continue;
|
|
75
|
-
const key = toKey(fkValue);
|
|
76
|
-
const bucket = grouped.get(key) ?? [];
|
|
77
|
-
bucket.push(row);
|
|
78
|
-
grouped.set(key, bucket);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return grouped;
|
|
177
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
|
|
178
|
+
return groupRowsByMany(rows, relation.foreignKey);
|
|
82
179
|
};
|
|
83
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Loads related entities for a has-one relation in batch.
|
|
183
|
+
* @param ctx - The entity context.
|
|
184
|
+
* @param rootTable - The root table of the relation.
|
|
185
|
+
* @param _relationName - The name of the relation (unused).
|
|
186
|
+
* @param relation - The has-one relation definition.
|
|
187
|
+
* @returns A promise resolving to a map of root keys to single related rows.
|
|
188
|
+
*/
|
|
84
189
|
export const loadHasOneRelation = async (
|
|
85
190
|
ctx: EntityContext,
|
|
86
191
|
rootTable: TableDef,
|
|
@@ -89,41 +194,27 @@ export const loadHasOneRelation = async (
|
|
|
89
194
|
): Promise<Map<string, Record<string, unknown>>> => {
|
|
90
195
|
const localKey = relation.localKey || findPrimaryKey(rootTable);
|
|
91
196
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
92
|
-
const keys =
|
|
93
|
-
|
|
94
|
-
for (const tracked of roots) {
|
|
95
|
-
const value = tracked.entity[localKey];
|
|
96
|
-
if (value !== null && value !== undefined) {
|
|
97
|
-
keys.add(value);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
197
|
+
const keys = collectKeysFromRoots(roots, localKey);
|
|
100
198
|
|
|
101
199
|
if (!keys.size) {
|
|
102
200
|
return new Map();
|
|
103
201
|
}
|
|
104
202
|
|
|
105
|
-
const selectMap = selectAllColumns(relation.target);
|
|
106
|
-
const qb = new SelectQueryBuilder(relation.target).select(selectMap);
|
|
107
203
|
const fkColumn = relation.target.columns[relation.foreignKey];
|
|
108
204
|
if (!fkColumn) return new Map();
|
|
109
205
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const rows = await executeQuery(ctx, qb);
|
|
113
|
-
const lookup = new Map<string, Record<string, unknown>>();
|
|
114
|
-
|
|
115
|
-
for (const row of rows) {
|
|
116
|
-
const fkValue = row[relation.foreignKey];
|
|
117
|
-
if (fkValue === null || fkValue === undefined) continue;
|
|
118
|
-
const key = toKey(fkValue);
|
|
119
|
-
if (!lookup.has(key)) {
|
|
120
|
-
lookup.set(key, row);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return lookup;
|
|
206
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
|
|
207
|
+
return groupRowsByUnique(rows, relation.foreignKey);
|
|
125
208
|
};
|
|
126
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Loads related entities for a belongs-to relation in batch.
|
|
212
|
+
* @param ctx - The entity context.
|
|
213
|
+
* @param rootTable - The root table of the relation.
|
|
214
|
+
* @param _relationName - The name of the relation (unused).
|
|
215
|
+
* @param relation - The belongs-to relation definition.
|
|
216
|
+
* @returns A promise resolving to a map of foreign keys to single related rows.
|
|
217
|
+
*/
|
|
127
218
|
export const loadBelongsToRelation = async (
|
|
128
219
|
ctx: EntityContext,
|
|
129
220
|
rootTable: TableDef,
|
|
@@ -131,38 +222,28 @@ export const loadBelongsToRelation = async (
|
|
|
131
222
|
relation: BelongsToRelation
|
|
132
223
|
): Promise<Map<string, Record<string, unknown>>> => {
|
|
133
224
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
134
|
-
const foreignKeys =
|
|
135
|
-
|
|
136
|
-
for (const tracked of roots) {
|
|
137
|
-
const value = tracked.entity[relation.foreignKey];
|
|
138
|
-
if (value !== null && value !== undefined) {
|
|
139
|
-
foreignKeys.add(value);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
225
|
+
const foreignKeys = collectKeysFromRoots(roots, relation.foreignKey);
|
|
142
226
|
|
|
143
227
|
if (!foreignKeys.size) {
|
|
144
228
|
return new Map();
|
|
145
229
|
}
|
|
146
230
|
|
|
147
|
-
const selectMap = selectAllColumns(relation.target);
|
|
148
|
-
const qb = new SelectQueryBuilder(relation.target).select(selectMap);
|
|
149
231
|
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
150
232
|
const pkColumn = relation.target.columns[targetKey];
|
|
151
233
|
if (!pkColumn) return new Map();
|
|
152
234
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const map = new Map<string, Record<string, unknown>>();
|
|
156
|
-
|
|
157
|
-
for (const row of rows) {
|
|
158
|
-
const keyValue = row[targetKey];
|
|
159
|
-
if (keyValue === null || keyValue === undefined) continue;
|
|
160
|
-
map.set(toKey(keyValue), row);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return map;
|
|
235
|
+
const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys);
|
|
236
|
+
return groupRowsByUnique(rows, targetKey);
|
|
164
237
|
};
|
|
165
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Loads related entities for a belongs-to-many relation in batch, including pivot data.
|
|
241
|
+
* @param ctx - The entity context.
|
|
242
|
+
* @param rootTable - The root table of the relation.
|
|
243
|
+
* @param _relationName - The name of the relation (unused).
|
|
244
|
+
* @param relation - The belongs-to-many relation definition.
|
|
245
|
+
* @returns A promise resolving to a map of root keys to arrays of related rows with pivot data.
|
|
246
|
+
*/
|
|
166
247
|
export const loadBelongsToManyRelation = async (
|
|
167
248
|
ctx: EntityContext,
|
|
168
249
|
rootTable: TableDef,
|
|
@@ -171,27 +252,16 @@ export const loadBelongsToManyRelation = async (
|
|
|
171
252
|
): Promise<Map<string, Rows>> => {
|
|
172
253
|
const rootKey = relation.localKey || findPrimaryKey(rootTable);
|
|
173
254
|
const roots = ctx.getEntitiesForTable(rootTable);
|
|
174
|
-
const rootIds =
|
|
175
|
-
|
|
176
|
-
for (const tracked of roots) {
|
|
177
|
-
const value = tracked.entity[rootKey];
|
|
178
|
-
if (value !== null && value !== undefined) {
|
|
179
|
-
rootIds.add(value);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
255
|
+
const rootIds = collectKeysFromRoots(roots, rootKey);
|
|
182
256
|
|
|
183
257
|
if (!rootIds.size) {
|
|
184
258
|
return new Map();
|
|
185
259
|
}
|
|
186
260
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
const pivotFkCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
190
|
-
if (!pivotFkCol) return new Map();
|
|
191
|
-
|
|
192
|
-
pivotQb.where(inList(pivotFkCol, Array.from(rootIds) as (string | number | LiteralNode)[]));
|
|
193
|
-
const pivotRows = await executeQuery(ctx, pivotQb);
|
|
261
|
+
const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
262
|
+
if (!pivotColumn) return new Map();
|
|
194
263
|
|
|
264
|
+
const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds);
|
|
195
265
|
const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
|
|
196
266
|
const targetIds = new Set<unknown>();
|
|
197
267
|
|
|
@@ -214,22 +284,12 @@ export const loadBelongsToManyRelation = async (
|
|
|
214
284
|
return new Map();
|
|
215
285
|
}
|
|
216
286
|
|
|
217
|
-
const targetSelect = selectAllColumns(relation.target);
|
|
218
287
|
const targetKey = relation.targetKey || findPrimaryKey(relation.target);
|
|
219
288
|
const targetPkColumn = relation.target.columns[targetKey];
|
|
220
289
|
if (!targetPkColumn) return new Map();
|
|
221
290
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const targetRows = await executeQuery(ctx, targetQb);
|
|
225
|
-
const targetMap = new Map<string, Record<string, unknown>>();
|
|
226
|
-
|
|
227
|
-
for (const row of targetRows) {
|
|
228
|
-
const pkValue = row[targetKey];
|
|
229
|
-
if (pkValue === null || pkValue === undefined) continue;
|
|
230
|
-
targetMap.set(toKey(pkValue), row);
|
|
231
|
-
}
|
|
232
|
-
|
|
291
|
+
const targetRows = await fetchRowsForKeys(ctx, relation.target, targetPkColumn, targetIds);
|
|
292
|
+
const targetMap = groupRowsByUnique(targetRows, targetKey);
|
|
233
293
|
const result = new Map<string, Rows>();
|
|
234
294
|
|
|
235
295
|
for (const [rootId, entries] of rootLookup.entries()) {
|
|
@@ -20,12 +20,28 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Default implementation of HasManyCollection for managing one-to-many relationships.
|
|
25
|
+
* @template TChild - The type of child entities in the collection
|
|
26
|
+
*/
|
|
23
27
|
export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
|
|
24
28
|
private loaded = false;
|
|
25
29
|
private items: TChild[] = [];
|
|
26
30
|
private readonly added = new Set<TChild>();
|
|
27
31
|
private readonly removed = new Set<TChild>();
|
|
28
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new DefaultHasManyCollection instance.
|
|
35
|
+
* @param ctx - The entity context
|
|
36
|
+
* @param meta - The entity metadata
|
|
37
|
+
* @param root - The root entity
|
|
38
|
+
* @param relationName - The relation name
|
|
39
|
+
* @param relation - The relation definition
|
|
40
|
+
* @param rootTable - The root table definition
|
|
41
|
+
* @param loader - The loader function for lazy loading
|
|
42
|
+
* @param createEntity - Function to create entities from rows
|
|
43
|
+
* @param localKey - The local key for the relation
|
|
44
|
+
*/
|
|
29
45
|
constructor(
|
|
30
46
|
private readonly ctx: EntityContext,
|
|
31
47
|
private readonly meta: EntityMeta<TableDef>,
|
|
@@ -41,6 +57,10 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
41
57
|
this.hydrateFromCache();
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Loads the related entities if not already loaded.
|
|
62
|
+
* @returns Promise resolving to the array of child entities
|
|
63
|
+
*/
|
|
44
64
|
async load(): Promise<TChild[]> {
|
|
45
65
|
if (this.loaded) return this.items;
|
|
46
66
|
const map = await this.loader();
|
|
@@ -51,10 +71,19 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
51
71
|
return this.items;
|
|
52
72
|
}
|
|
53
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Gets the current items in the collection.
|
|
76
|
+
* @returns Array of child entities
|
|
77
|
+
*/
|
|
54
78
|
getItems(): TChild[] {
|
|
55
79
|
return this.items;
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Adds a new child entity to the collection.
|
|
84
|
+
* @param data - Partial data for the new entity
|
|
85
|
+
* @returns The created entity
|
|
86
|
+
*/
|
|
58
87
|
add(data: Partial<TChild>): TChild {
|
|
59
88
|
const keyValue = (this.root as Record<string, unknown>)[this.localKey];
|
|
60
89
|
const childRow: Record<string, unknown> = {
|
|
@@ -75,6 +104,10 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
75
104
|
return entity;
|
|
76
105
|
}
|
|
77
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Attaches an existing entity to the collection.
|
|
109
|
+
* @param entity - The entity to attach
|
|
110
|
+
*/
|
|
78
111
|
attach(entity: TChild): void {
|
|
79
112
|
const keyValue = this.root[this.localKey];
|
|
80
113
|
(entity as Record<string, unknown>)[this.relation.foreignKey] = keyValue;
|
|
@@ -90,6 +123,10 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
90
123
|
);
|
|
91
124
|
}
|
|
92
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Removes an entity from the collection.
|
|
128
|
+
* @param entity - The entity to remove
|
|
129
|
+
*/
|
|
93
130
|
remove(entity: TChild): void {
|
|
94
131
|
this.items = this.items.filter(item => item !== entity);
|
|
95
132
|
this.removed.add(entity);
|
|
@@ -103,6 +140,9 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
103
140
|
);
|
|
104
141
|
}
|
|
105
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Clears all entities from the collection.
|
|
145
|
+
*/
|
|
106
146
|
clear(): void {
|
|
107
147
|
for (const entity of [...this.items]) {
|
|
108
148
|
this.remove(entity);
|
|
@@ -122,6 +162,10 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
122
162
|
this.loaded = true;
|
|
123
163
|
}
|
|
124
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Returns the items for JSON serialization.
|
|
167
|
+
* @returns Array of child entities
|
|
168
|
+
*/
|
|
125
169
|
toJSON(): TChild[] {
|
|
126
170
|
return this.items;
|
|
127
171
|
}
|
package/src/orm/save-graph.ts
CHANGED
|
@@ -20,13 +20,30 @@ import type { EntityConstructor } from './entity-metadata.js';
|
|
|
20
20
|
import { getTableDefFromEntity } from '../decorators/bootstrap.js';
|
|
21
21
|
import type { OrmSession } from './orm-session.js';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Options for controlling the behavior of save graph operations.
|
|
25
|
+
*/
|
|
23
26
|
export interface SaveGraphOptions {
|
|
24
27
|
/** Remove existing collection members that are not present in the payload */
|
|
25
28
|
pruneMissing?: boolean;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/** Represents an entity object with arbitrary properties. */
|
|
32
|
+
|
|
33
|
+
/** Represents an entity object with arbitrary properties. */
|
|
34
|
+
|
|
28
35
|
type AnyEntity = Record<string, unknown>;
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
|
|
39
|
+
* Converts a value to a string key, returning an empty string for null or undefined.
|
|
40
|
+
|
|
41
|
+
* @param value - The value to convert.
|
|
42
|
+
|
|
43
|
+
* @returns The string representation or empty string.
|
|
44
|
+
|
|
45
|
+
*/
|
|
46
|
+
|
|
30
47
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
31
48
|
|
|
32
49
|
const pickColumns = (table: TableDef, payload: AnyEntity): Record<string, unknown> => {
|
|
@@ -292,18 +309,46 @@ export const saveGraph = async <TTable extends TableDef>(
|
|
|
292
309
|
return root;
|
|
293
310
|
};
|
|
294
311
|
|
|
312
|
+
/**
|
|
313
|
+
|
|
314
|
+
* Internal version of saveGraph with typed return based on the constructor.
|
|
315
|
+
|
|
316
|
+
* @param session - The ORM session.
|
|
317
|
+
|
|
318
|
+
* @param entityClass - The entity constructor.
|
|
319
|
+
|
|
320
|
+
* @param payload - The payload data for the root entity and its relations.
|
|
321
|
+
|
|
322
|
+
* @param options - Options for the save operation.
|
|
323
|
+
|
|
324
|
+
* @returns The root entity instance.
|
|
325
|
+
|
|
326
|
+
*/
|
|
327
|
+
|
|
295
328
|
export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
329
|
+
|
|
296
330
|
session: OrmSession,
|
|
331
|
+
|
|
297
332
|
entityClass: TCtor,
|
|
333
|
+
|
|
298
334
|
payload: AnyEntity,
|
|
335
|
+
|
|
299
336
|
options: SaveGraphOptions = {}
|
|
337
|
+
|
|
300
338
|
): Promise<InstanceType<TCtor>> => {
|
|
339
|
+
|
|
301
340
|
const table = getTableDefFromEntity(entityClass);
|
|
341
|
+
|
|
302
342
|
if (!table) {
|
|
343
|
+
|
|
303
344
|
throw new Error('Entity metadata has not been bootstrapped');
|
|
345
|
+
|
|
304
346
|
}
|
|
305
347
|
|
|
306
348
|
const root = ensureEntity(session, table, payload);
|
|
349
|
+
|
|
307
350
|
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
351
|
+
|
|
308
352
|
return root as unknown as InstanceType<TCtor>;
|
|
353
|
+
|
|
309
354
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
3
|
+
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
4
|
+
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
5
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
6
|
+
import { QueryTarget, resolveTable } from './target.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a SELECT query builder for the specified table or entity.
|
|
10
|
+
*
|
|
11
|
+
* @template TTable - The table definition type
|
|
12
|
+
* @param target - The table definition or entity constructor to query from
|
|
13
|
+
* @returns A new SelectQueryBuilder instance for building SELECT queries
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const query = selectFrom(UserTable).select('id', 'name');
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const selectFrom = <TTable extends TableDef>(target: QueryTarget<TTable>): SelectQueryBuilder<unknown, TTable> => {
|
|
21
|
+
const table = resolveTable(target);
|
|
22
|
+
return new SelectQueryBuilder(table);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an INSERT query builder for the specified table or entity.
|
|
27
|
+
*
|
|
28
|
+
* @template TTable - The table definition type
|
|
29
|
+
* @param target - The table definition or entity constructor to insert into
|
|
30
|
+
* @returns A new InsertQueryBuilder instance for building INSERT queries
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const query = insertInto(UserTable).values({ name: 'John', email: 'john@example.com' });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const insertInto = <TTable extends TableDef>(target: QueryTarget<TTable>): InsertQueryBuilder<unknown> => {
|
|
38
|
+
const table = resolveTable(target);
|
|
39
|
+
return new InsertQueryBuilder(table);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates an UPDATE query builder for the specified table or entity.
|
|
44
|
+
*
|
|
45
|
+
* @template TTable - The table definition type
|
|
46
|
+
* @param target - The table definition or entity constructor to update
|
|
47
|
+
* @returns A new UpdateQueryBuilder instance for building UPDATE queries
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const query = update(UserTable).set({ name: 'Jane' }).where(eq(UserTable.id, 1));
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const update = <TTable extends TableDef>(target: QueryTarget<TTable>): UpdateQueryBuilder<unknown> => {
|
|
55
|
+
const table = resolveTable(target);
|
|
56
|
+
return new UpdateQueryBuilder(table);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a DELETE query builder for the specified table or entity.
|
|
61
|
+
*
|
|
62
|
+
* @template TTable - The table definition type
|
|
63
|
+
* @param target - The table definition or entity constructor to delete from
|
|
64
|
+
* @returns A new DeleteQueryBuilder instance for building DELETE queries
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const query = deleteFrom(UserTable).where(eq(UserTable.id, 1));
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export const deleteFrom = <TTable extends TableDef>(target: QueryTarget<TTable>): DeleteQueryBuilder<unknown> => {
|
|
72
|
+
const table = resolveTable(target);
|
|
73
|
+
return new DeleteQueryBuilder(table);
|
|
74
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { isTableDef } from '../schema/table-guards.js';
|
|
3
|
+
import { EntityConstructor } from '../orm/entity-metadata.js';
|
|
4
|
+
import { getTableDefFromEntity } from '../decorators/bootstrap.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents a target for query operations, which can be either a table definition
|
|
8
|
+
* or an entity constructor. This type allows flexible targeting of database tables
|
|
9
|
+
* through either direct table definitions or entity classes decorated with ORM metadata.
|
|
10
|
+
*
|
|
11
|
+
* @template TTable - The table definition type, defaults to TableDef
|
|
12
|
+
*/
|
|
13
|
+
export type QueryTarget<TTable extends TableDef = TableDef> = TTable | EntityConstructor;
|
|
14
|
+
|
|
15
|
+
const resolveEntityTarget = <TTable extends TableDef>(ctor: EntityConstructor): TTable => {
|
|
16
|
+
const table = getTableDefFromEntity(ctor);
|
|
17
|
+
if (!table) {
|
|
18
|
+
throw new Error(`Entity '${ctor.name}' is not registered with decorators`);
|
|
19
|
+
}
|
|
20
|
+
return table as TTable;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolves a QueryTarget to its corresponding table definition.
|
|
25
|
+
*
|
|
26
|
+
* If the target is already a TableDef, it returns it directly.
|
|
27
|
+
* If the target is an EntityConstructor, it retrieves the associated table definition
|
|
28
|
+
* from the entity's metadata.
|
|
29
|
+
*
|
|
30
|
+
* @template TTable - The table definition type
|
|
31
|
+
* @param target - The query target to resolve
|
|
32
|
+
* @returns The resolved table definition
|
|
33
|
+
* @throws Error if the entity constructor is not registered with decorators
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const table = resolveTable(UserTable); // Returns UserTable directly
|
|
38
|
+
* const table2 = resolveTable(UserEntity); // Returns table def from UserEntity metadata
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export const resolveTable = <TTable extends TableDef>(target: QueryTarget<TTable>): TTable => {
|
|
42
|
+
if (isTableDef(target)) {
|
|
43
|
+
return target as TTable;
|
|
44
|
+
}
|
|
45
|
+
return resolveEntityTarget(target as EntityConstructor);
|
|
46
|
+
};
|