linkgress-orm 0.2.7 → 0.2.9
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/dist/entity/db-context.d.ts +12 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +189 -35
- package/dist/entity/db-context.js.map +1 -1
- package/dist/query/query-builder.d.ts +12 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +229 -66
- package/dist/query/query-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -1768,11 +1768,11 @@ class SelectQueryBuilder {
|
|
|
1768
1768
|
deleteSql += ` ${usingClause}`;
|
|
1769
1769
|
}
|
|
1770
1770
|
deleteSql += ` WHERE ${fullWhereClause}`;
|
|
1771
|
-
const { sql, params } = queryBuilder.buildReturningWithNavigation(deleteSql, whereParams, returning, navigationInfo);
|
|
1771
|
+
const { sql, params, nestedPaths } = queryBuilder.buildReturningWithNavigation(deleteSql, whereParams, returning, navigationInfo);
|
|
1772
1772
|
const result = queryBuilder.executor
|
|
1773
1773
|
? await queryBuilder.executor.query(sql, params)
|
|
1774
1774
|
: await queryBuilder.client.query(sql, params);
|
|
1775
|
-
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
|
|
1775
|
+
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields, nestedPaths);
|
|
1776
1776
|
}
|
|
1777
1777
|
// Standard RETURNING (no navigation properties)
|
|
1778
1778
|
// Qualify columns with table name when using USING clause to avoid ambiguity
|
|
@@ -1907,11 +1907,11 @@ class SelectQueryBuilder {
|
|
|
1907
1907
|
updateSql += ` ${fromClause}`;
|
|
1908
1908
|
}
|
|
1909
1909
|
updateSql += ` WHERE ${fullWhereClause}`;
|
|
1910
|
-
const { sql, params } = queryBuilder.buildReturningWithNavigation(updateSql, values, returning, navigationInfo);
|
|
1910
|
+
const { sql, params, nestedPaths } = queryBuilder.buildReturningWithNavigation(updateSql, values, returning, navigationInfo);
|
|
1911
1911
|
const result = queryBuilder.executor
|
|
1912
1912
|
? await queryBuilder.executor.query(sql, params)
|
|
1913
1913
|
: await queryBuilder.client.query(sql, params);
|
|
1914
|
-
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
|
|
1914
|
+
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields, nestedPaths);
|
|
1915
1915
|
}
|
|
1916
1916
|
// Standard RETURNING (no navigation properties)
|
|
1917
1917
|
// Qualify columns with table name when using FROM clause to avoid ambiguity
|
|
@@ -2054,35 +2054,68 @@ class SelectQueryBuilder {
|
|
|
2054
2054
|
}
|
|
2055
2055
|
const navigationFields = new Map();
|
|
2056
2056
|
const allTableAliases = new Set();
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2057
|
+
const nestedObjects = new Map();
|
|
2058
|
+
const collectionFields = new Map();
|
|
2059
|
+
// Recursively collect field refs and table aliases from selection
|
|
2060
|
+
const collectFieldRefs = (obj, path = '') => {
|
|
2061
|
+
for (const [key, field] of Object.entries(obj)) {
|
|
2062
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
2063
|
+
if (field && typeof field === 'object') {
|
|
2064
|
+
if ('__dbColumnName' in field) {
|
|
2065
|
+
// Direct field reference (either main table or navigation)
|
|
2066
|
+
const tableAlias = field.__tableAlias;
|
|
2067
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
2068
|
+
// Navigation field
|
|
2069
|
+
allTableAliases.add(tableAlias);
|
|
2070
|
+
navigationFields.set(fieldPath, {
|
|
2071
|
+
tableAlias,
|
|
2072
|
+
dbColumnName: field.__dbColumnName,
|
|
2073
|
+
schemaTable: field.__sourceTable,
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
// Also collect intermediate navigation aliases for multi-level navigation
|
|
2077
|
+
if ('__navigationAliases' in field && Array.isArray(field.__navigationAliases)) {
|
|
2078
|
+
for (const navAlias of field.__navigationAliases) {
|
|
2079
|
+
if (navAlias && navAlias !== this.schema.name) {
|
|
2080
|
+
allTableAliases.add(navAlias);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2074
2083
|
}
|
|
2075
2084
|
}
|
|
2085
|
+
else if (field instanceof CollectionQueryBuilder) {
|
|
2086
|
+
// CollectionQueryBuilder (.toList(), .firstOrDefault())
|
|
2087
|
+
collectionFields.set(fieldPath, field);
|
|
2088
|
+
// Also extract the navigation path from the collection builder
|
|
2089
|
+
// so we can add the necessary joins to reach the collection's source table
|
|
2090
|
+
const collectionBuilder = field;
|
|
2091
|
+
if (collectionBuilder.navigationPath && Array.isArray(collectionBuilder.navigationPath)) {
|
|
2092
|
+
for (const navJoin of collectionBuilder.navigationPath) {
|
|
2093
|
+
if (navJoin.alias && navJoin.alias !== this.schema.name) {
|
|
2094
|
+
allTableAliases.add(navJoin.alias);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
// Also add the source table alias
|
|
2099
|
+
if (collectionBuilder.sourceTable && collectionBuilder.sourceTable !== this.schema.name) {
|
|
2100
|
+
allTableAliases.add(collectionBuilder.sourceTable);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
else if (!Array.isArray(field)) {
|
|
2104
|
+
// Nested plain object - recurse into it
|
|
2105
|
+
nestedObjects.set(fieldPath, field);
|
|
2106
|
+
collectFieldRefs(field, fieldPath);
|
|
2107
|
+
}
|
|
2076
2108
|
}
|
|
2077
2109
|
}
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2110
|
+
};
|
|
2111
|
+
collectFieldRefs(selection);
|
|
2112
|
+
if (navigationFields.size === 0 && nestedObjects.size === 0 && collectionFields.size === 0) {
|
|
2080
2113
|
return null;
|
|
2081
2114
|
}
|
|
2082
2115
|
// Resolve navigation joins
|
|
2083
2116
|
const joins = [];
|
|
2084
2117
|
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
2085
|
-
return { hasNavigation: true, selection, joins, navigationFields };
|
|
2118
|
+
return { hasNavigation: true, selection, joins, navigationFields, nestedObjects, collectionFields };
|
|
2086
2119
|
}
|
|
2087
2120
|
/**
|
|
2088
2121
|
* Build RETURNING clause with navigation property support using CTE
|
|
@@ -2093,6 +2126,7 @@ class SelectQueryBuilder {
|
|
|
2093
2126
|
// First, collect all columns needed from the main table in the mutation's RETURNING
|
|
2094
2127
|
const mainTableColumns = new Set();
|
|
2095
2128
|
const selectParts = [];
|
|
2129
|
+
const nestedPaths = new Set();
|
|
2096
2130
|
const mockRow = this._createMockRow();
|
|
2097
2131
|
const selectedMock = this.selector(mockRow);
|
|
2098
2132
|
const selection = returning(selectedMock);
|
|
@@ -2111,22 +2145,82 @@ class SelectQueryBuilder {
|
|
|
2111
2145
|
for (const join of navigationInfo.joins) {
|
|
2112
2146
|
aliasToSourceTable.set(join.alias, join.targetTable);
|
|
2113
2147
|
}
|
|
2114
|
-
//
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2148
|
+
// Build a map of table name -> alias for collection subquery rewriting
|
|
2149
|
+
// This allows us to rewrite collection subqueries to use the correct joined aliases
|
|
2150
|
+
const tableToAlias = new Map();
|
|
2151
|
+
tableToAlias.set(this.schema.name, '__mutation__'); // Main table uses mutation CTE
|
|
2152
|
+
for (const join of navigationInfo.joins) {
|
|
2153
|
+
tableToAlias.set(join.targetTable, join.alias);
|
|
2154
|
+
}
|
|
2155
|
+
// Track collection subqueries for LATERAL JOINs
|
|
2156
|
+
const collectionSubqueries = [];
|
|
2157
|
+
let lateralCounter = 0;
|
|
2158
|
+
// Track all params including those from collection subqueries
|
|
2159
|
+
const allParams = [...mutationParams];
|
|
2160
|
+
let currentParamCounter = mutationParams.length + 1;
|
|
2161
|
+
// Build a QueryContext for collection subquery building
|
|
2162
|
+
const buildCollectionContext = () => ({
|
|
2163
|
+
ctes: new Map(),
|
|
2164
|
+
cteCounter: lateralCounter,
|
|
2165
|
+
paramCounter: currentParamCounter,
|
|
2166
|
+
allParams: allParams,
|
|
2167
|
+
collectionStrategy: 'lateral',
|
|
2168
|
+
});
|
|
2169
|
+
// Recursively process selection to build SELECT parts
|
|
2170
|
+
const processSelection = (obj, path = '') => {
|
|
2171
|
+
for (const [key, field] of Object.entries(obj)) {
|
|
2172
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
2173
|
+
if (field && typeof field === 'object') {
|
|
2174
|
+
if ('__dbColumnName' in field) {
|
|
2175
|
+
// Direct field reference
|
|
2176
|
+
const tableAlias = field.__tableAlias;
|
|
2177
|
+
const dbColumnName = field.__dbColumnName;
|
|
2178
|
+
if (!tableAlias || tableAlias === this.schema.name) {
|
|
2179
|
+
mainTableColumns.add(dbColumnName);
|
|
2180
|
+
selectParts.push(`"__mutation__"."${dbColumnName}" AS "${fieldPath}"`);
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
selectParts.push(`"${tableAlias}"."${dbColumnName}" AS "${fieldPath}"`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
else if (field instanceof CollectionQueryBuilder) {
|
|
2187
|
+
// CollectionQueryBuilder (.toList(), .firstOrDefault())
|
|
2188
|
+
// Build a correlated subquery that references joined tables from the main query
|
|
2189
|
+
const collectionBuilder = field;
|
|
2190
|
+
const context = buildCollectionContext();
|
|
2191
|
+
// Build the CTE/subquery using lateral strategy
|
|
2192
|
+
const cteResult = collectionBuilder.buildCTE(context);
|
|
2193
|
+
lateralCounter = context.cteCounter;
|
|
2194
|
+
currentParamCounter = context.paramCounter; // Track new param index after collection subquery
|
|
2195
|
+
// The lateral strategy returns either:
|
|
2196
|
+
// 1. A correlated subquery in selectExpression (no join needed)
|
|
2197
|
+
// 2. A LATERAL JOIN with joinClause and selectExpression
|
|
2198
|
+
if (cteResult.joinClause && cteResult.joinClause.trim()) {
|
|
2199
|
+
// LATERAL JOIN needed - rewrite all table references to use correct aliases
|
|
2200
|
+
const rewrittenJoinClause = this.rewriteCollectionTableReferences(cteResult.joinClause, tableToAlias);
|
|
2201
|
+
collectionSubqueries.push({
|
|
2202
|
+
fieldPath,
|
|
2203
|
+
lateralAlias: cteResult.tableName || `lateral_${lateralCounter - 1}`,
|
|
2204
|
+
joinClause: rewrittenJoinClause,
|
|
2205
|
+
selectExpression: cteResult.selectExpression || `"${cteResult.tableName}".data`,
|
|
2206
|
+
});
|
|
2207
|
+
selectParts.push(`${cteResult.selectExpression || `"${cteResult.tableName}".data`} AS "${fieldPath}"`);
|
|
2208
|
+
}
|
|
2209
|
+
else if (cteResult.selectExpression) {
|
|
2210
|
+
// Correlated subquery in SELECT - rewrite all table references
|
|
2211
|
+
const rewrittenExpr = this.rewriteCollectionTableReferences(cteResult.selectExpression, tableToAlias);
|
|
2212
|
+
selectParts.push(`${rewrittenExpr} AS "${fieldPath}"`);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
else if (!Array.isArray(field)) {
|
|
2216
|
+
// Nested plain object - recurse into it and mark as nested path
|
|
2217
|
+
nestedPaths.add(fieldPath);
|
|
2218
|
+
processSelection(field, fieldPath);
|
|
2219
|
+
}
|
|
2127
2220
|
}
|
|
2128
2221
|
}
|
|
2129
|
-
}
|
|
2222
|
+
};
|
|
2223
|
+
processSelection(selection);
|
|
2130
2224
|
// Include foreign keys needed for joins - only for joins from main table
|
|
2131
2225
|
for (const join of navigationInfo.joins) {
|
|
2132
2226
|
// Only add FK to mainTableColumns if the join source is the main table
|
|
@@ -2137,6 +2231,19 @@ class SelectQueryBuilder {
|
|
|
2137
2231
|
}
|
|
2138
2232
|
}
|
|
2139
2233
|
}
|
|
2234
|
+
// Include 'id' column if there are collection subqueries (needed for correlation)
|
|
2235
|
+
if (collectionSubqueries.length > 0 || (navigationInfo.collectionFields && navigationInfo.collectionFields.size > 0)) {
|
|
2236
|
+
// Find the 'id' column db name
|
|
2237
|
+
const idColEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === 'id');
|
|
2238
|
+
if (idColEntry) {
|
|
2239
|
+
const idDbCol = idColEntry[1].build().name;
|
|
2240
|
+
mainTableColumns.add(idDbCol);
|
|
2241
|
+
}
|
|
2242
|
+
else {
|
|
2243
|
+
// Fallback to 'id' if not found in schema
|
|
2244
|
+
mainTableColumns.add('id');
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2140
2247
|
// Build RETURNING clause for CTE with all needed columns from main table
|
|
2141
2248
|
const cteReturningCols = Array.from(mainTableColumns).map(col => `"${col}"`).join(', ');
|
|
2142
2249
|
// Add RETURNING to the mutation SQL for CTE
|
|
@@ -2166,6 +2273,10 @@ class SelectQueryBuilder {
|
|
|
2166
2273
|
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2167
2274
|
joinClauses.push(`${joinType} ${qualifiedJoinTable} AS "${join.alias}" ON ${joinConditions.join(' AND ')}`);
|
|
2168
2275
|
}
|
|
2276
|
+
// Add LATERAL JOINs for collections
|
|
2277
|
+
for (const collection of collectionSubqueries) {
|
|
2278
|
+
joinClauses.push(collection.joinClause);
|
|
2279
|
+
}
|
|
2169
2280
|
// Build the final CTE query
|
|
2170
2281
|
const sql = `WITH "__mutation__" AS (
|
|
2171
2282
|
${mutationWithReturning}
|
|
@@ -2173,47 +2284,99 @@ class SelectQueryBuilder {
|
|
|
2173
2284
|
SELECT ${selectParts.join(', ')}
|
|
2174
2285
|
FROM "__mutation__"
|
|
2175
2286
|
${joinClauses.join('\n')}`;
|
|
2176
|
-
return { sql, params:
|
|
2287
|
+
return { sql, params: allParams, nestedPaths };
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Rewrite table references in a collection subquery to use the correct aliases
|
|
2291
|
+
* from the main query's JOINs. This handles multi-level navigation where the
|
|
2292
|
+
* collection is accessed through intermediate joined tables.
|
|
2293
|
+
*
|
|
2294
|
+
* @param expression - The SQL expression (join clause or select expression) to rewrite
|
|
2295
|
+
* @param tableToAlias - Map of table names to their aliases in the main query
|
|
2296
|
+
* @returns The rewritten expression with all table references updated
|
|
2297
|
+
* @internal
|
|
2298
|
+
*/
|
|
2299
|
+
rewriteCollectionTableReferences(expression, tableToAlias) {
|
|
2300
|
+
let result = expression;
|
|
2301
|
+
// Rewrite each table reference to use the correct alias
|
|
2302
|
+
// Pattern: "tableName"."columnName" -> "alias"."columnName"
|
|
2303
|
+
for (const [tableName, alias] of tableToAlias) {
|
|
2304
|
+
const pattern = new RegExp(`"${tableName}"\\."`, 'g');
|
|
2305
|
+
result = result.replace(pattern, `"${alias}"."`);
|
|
2306
|
+
}
|
|
2307
|
+
return result;
|
|
2177
2308
|
}
|
|
2178
2309
|
/**
|
|
2179
2310
|
* Map RETURNING results with navigation properties
|
|
2180
2311
|
* Applies type mappers based on source table schemas
|
|
2312
|
+
* Reconstructs nested objects from flat column paths
|
|
2181
2313
|
* @internal
|
|
2182
2314
|
*/
|
|
2183
|
-
mapReturningResultsWithNavigation(rows, returning, navigationFields) {
|
|
2315
|
+
mapReturningResultsWithNavigation(rows, returning, navigationFields, nestedPaths) {
|
|
2184
2316
|
return rows.map(row => {
|
|
2185
2317
|
const mapped = {};
|
|
2186
2318
|
for (const [key, value] of Object.entries(row)) {
|
|
2187
|
-
//
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2195
|
-
const config = col.build();
|
|
2196
|
-
return config.name === navInfo.dbColumnName;
|
|
2197
|
-
});
|
|
2198
|
-
if (colEntry) {
|
|
2199
|
-
const config = colEntry[1].build();
|
|
2200
|
-
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2201
|
-
continue;
|
|
2319
|
+
// Handle nested paths (e.g., "invoicingPartner.id" -> { invoicingPartner: { id: value } })
|
|
2320
|
+
if (key.includes('.')) {
|
|
2321
|
+
const parts = key.split('.');
|
|
2322
|
+
let current = mapped;
|
|
2323
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2324
|
+
if (!current[parts[i]]) {
|
|
2325
|
+
current[parts[i]] = {};
|
|
2202
2326
|
}
|
|
2327
|
+
current = current[parts[i]];
|
|
2203
2328
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2329
|
+
const finalKey = parts[parts.length - 1];
|
|
2330
|
+
// Check if this is a navigation field
|
|
2331
|
+
const navInfo = navigationFields.get(key);
|
|
2332
|
+
if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
|
|
2333
|
+
const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
|
|
2334
|
+
if (targetSchema) {
|
|
2335
|
+
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2336
|
+
const config = col.build();
|
|
2337
|
+
return config.name === navInfo.dbColumnName;
|
|
2338
|
+
});
|
|
2339
|
+
if (colEntry) {
|
|
2340
|
+
const config = colEntry[1].build();
|
|
2341
|
+
current[finalKey] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
current[finalKey] = value;
|
|
2214
2347
|
}
|
|
2215
2348
|
else {
|
|
2216
|
-
|
|
2349
|
+
// Check if this is a navigation field
|
|
2350
|
+
const navInfo = navigationFields.get(key);
|
|
2351
|
+
if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
|
|
2352
|
+
// Try to get mapper from the navigation target's schema
|
|
2353
|
+
const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
|
|
2354
|
+
if (targetSchema) {
|
|
2355
|
+
// Find the column and its mapper
|
|
2356
|
+
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2357
|
+
const config = col.build();
|
|
2358
|
+
return config.name === navInfo.dbColumnName;
|
|
2359
|
+
});
|
|
2360
|
+
if (colEntry) {
|
|
2361
|
+
const config = colEntry[1].build();
|
|
2362
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
// Try to find column by alias or name in main schema
|
|
2368
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
|
|
2369
|
+
const config = col.build();
|
|
2370
|
+
return propName === key || config.name === key;
|
|
2371
|
+
});
|
|
2372
|
+
if (colEntry) {
|
|
2373
|
+
const [, col] = colEntry;
|
|
2374
|
+
const config = col.build();
|
|
2375
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2376
|
+
}
|
|
2377
|
+
else {
|
|
2378
|
+
mapped[key] = value;
|
|
2379
|
+
}
|
|
2217
2380
|
}
|
|
2218
2381
|
}
|
|
2219
2382
|
return mapped;
|