linkgress-orm 0.2.7 → 0.2.8
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 +11 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +170 -35
- package/dist/entity/db-context.js.map +1 -1
- package/dist/query/query-builder.d.ts +11 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +204 -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,54 @@ 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
|
+
}
|
|
2089
|
+
else if (!Array.isArray(field)) {
|
|
2090
|
+
// Nested plain object - recurse into it
|
|
2091
|
+
nestedObjects.set(fieldPath, field);
|
|
2092
|
+
collectFieldRefs(field, fieldPath);
|
|
2093
|
+
}
|
|
2076
2094
|
}
|
|
2077
2095
|
}
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2096
|
+
};
|
|
2097
|
+
collectFieldRefs(selection);
|
|
2098
|
+
if (navigationFields.size === 0 && nestedObjects.size === 0 && collectionFields.size === 0) {
|
|
2080
2099
|
return null;
|
|
2081
2100
|
}
|
|
2082
2101
|
// Resolve navigation joins
|
|
2083
2102
|
const joins = [];
|
|
2084
2103
|
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
2085
|
-
return { hasNavigation: true, selection, joins, navigationFields };
|
|
2104
|
+
return { hasNavigation: true, selection, joins, navigationFields, nestedObjects, collectionFields };
|
|
2086
2105
|
}
|
|
2087
2106
|
/**
|
|
2088
2107
|
* Build RETURNING clause with navigation property support using CTE
|
|
@@ -2093,6 +2112,7 @@ class SelectQueryBuilder {
|
|
|
2093
2112
|
// First, collect all columns needed from the main table in the mutation's RETURNING
|
|
2094
2113
|
const mainTableColumns = new Set();
|
|
2095
2114
|
const selectParts = [];
|
|
2115
|
+
const nestedPaths = new Set();
|
|
2096
2116
|
const mockRow = this._createMockRow();
|
|
2097
2117
|
const selectedMock = this.selector(mockRow);
|
|
2098
2118
|
const selection = returning(selectedMock);
|
|
@@ -2111,22 +2131,75 @@ class SelectQueryBuilder {
|
|
|
2111
2131
|
for (const join of navigationInfo.joins) {
|
|
2112
2132
|
aliasToSourceTable.set(join.alias, join.targetTable);
|
|
2113
2133
|
}
|
|
2114
|
-
//
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2134
|
+
// Track collection subqueries for LATERAL JOINs
|
|
2135
|
+
const collectionSubqueries = [];
|
|
2136
|
+
let lateralCounter = 0;
|
|
2137
|
+
// Track all params including those from collection subqueries
|
|
2138
|
+
const allParams = [...mutationParams];
|
|
2139
|
+
let currentParamCounter = mutationParams.length + 1;
|
|
2140
|
+
// Build a QueryContext for collection subquery building
|
|
2141
|
+
const buildCollectionContext = () => ({
|
|
2142
|
+
ctes: new Map(),
|
|
2143
|
+
cteCounter: lateralCounter,
|
|
2144
|
+
paramCounter: currentParamCounter,
|
|
2145
|
+
allParams: allParams,
|
|
2146
|
+
collectionStrategy: 'lateral',
|
|
2147
|
+
});
|
|
2148
|
+
// Recursively process selection to build SELECT parts
|
|
2149
|
+
const processSelection = (obj, path = '') => {
|
|
2150
|
+
for (const [key, field] of Object.entries(obj)) {
|
|
2151
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
2152
|
+
if (field && typeof field === 'object') {
|
|
2153
|
+
if ('__dbColumnName' in field) {
|
|
2154
|
+
// Direct field reference
|
|
2155
|
+
const tableAlias = field.__tableAlias;
|
|
2156
|
+
const dbColumnName = field.__dbColumnName;
|
|
2157
|
+
if (!tableAlias || tableAlias === this.schema.name) {
|
|
2158
|
+
mainTableColumns.add(dbColumnName);
|
|
2159
|
+
selectParts.push(`"__mutation__"."${dbColumnName}" AS "${fieldPath}"`);
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
selectParts.push(`"${tableAlias}"."${dbColumnName}" AS "${fieldPath}"`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
else if (field instanceof CollectionQueryBuilder) {
|
|
2166
|
+
// CollectionQueryBuilder (.toList(), .firstOrDefault())
|
|
2167
|
+
// Build a correlated subquery that references __mutation__ instead of the source table
|
|
2168
|
+
const collectionBuilder = field;
|
|
2169
|
+
const context = buildCollectionContext();
|
|
2170
|
+
// Build the CTE/subquery using lateral strategy
|
|
2171
|
+
const cteResult = collectionBuilder.buildCTE(context);
|
|
2172
|
+
lateralCounter = context.cteCounter;
|
|
2173
|
+
currentParamCounter = context.paramCounter; // Track new param index after collection subquery
|
|
2174
|
+
// The lateral strategy returns either:
|
|
2175
|
+
// 1. A correlated subquery in selectExpression (no join needed)
|
|
2176
|
+
// 2. A LATERAL JOIN with joinClause and selectExpression
|
|
2177
|
+
if (cteResult.joinClause && cteResult.joinClause.trim()) {
|
|
2178
|
+
// LATERAL JOIN needed - rewrite to reference __mutation__ instead of source table
|
|
2179
|
+
const rewrittenJoinClause = this.rewriteCollectionJoinForMutation(cteResult.joinClause, this.schema.name, '__mutation__');
|
|
2180
|
+
collectionSubqueries.push({
|
|
2181
|
+
fieldPath,
|
|
2182
|
+
lateralAlias: cteResult.tableName || `lateral_${lateralCounter - 1}`,
|
|
2183
|
+
joinClause: rewrittenJoinClause,
|
|
2184
|
+
selectExpression: cteResult.selectExpression || `"${cteResult.tableName}".data`,
|
|
2185
|
+
});
|
|
2186
|
+
selectParts.push(`${cteResult.selectExpression || `"${cteResult.tableName}".data`} AS "${fieldPath}"`);
|
|
2187
|
+
}
|
|
2188
|
+
else if (cteResult.selectExpression) {
|
|
2189
|
+
// Correlated subquery in SELECT - rewrite source table references
|
|
2190
|
+
const rewrittenExpr = this.rewriteCollectionExprForMutation(cteResult.selectExpression, this.schema.name, '__mutation__');
|
|
2191
|
+
selectParts.push(`${rewrittenExpr} AS "${fieldPath}"`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
else if (!Array.isArray(field)) {
|
|
2195
|
+
// Nested plain object - recurse into it and mark as nested path
|
|
2196
|
+
nestedPaths.add(fieldPath);
|
|
2197
|
+
processSelection(field, fieldPath);
|
|
2198
|
+
}
|
|
2127
2199
|
}
|
|
2128
2200
|
}
|
|
2129
|
-
}
|
|
2201
|
+
};
|
|
2202
|
+
processSelection(selection);
|
|
2130
2203
|
// Include foreign keys needed for joins - only for joins from main table
|
|
2131
2204
|
for (const join of navigationInfo.joins) {
|
|
2132
2205
|
// Only add FK to mainTableColumns if the join source is the main table
|
|
@@ -2137,6 +2210,19 @@ class SelectQueryBuilder {
|
|
|
2137
2210
|
}
|
|
2138
2211
|
}
|
|
2139
2212
|
}
|
|
2213
|
+
// Include 'id' column if there are collection subqueries (needed for correlation)
|
|
2214
|
+
if (collectionSubqueries.length > 0 || (navigationInfo.collectionFields && navigationInfo.collectionFields.size > 0)) {
|
|
2215
|
+
// Find the 'id' column db name
|
|
2216
|
+
const idColEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === 'id');
|
|
2217
|
+
if (idColEntry) {
|
|
2218
|
+
const idDbCol = idColEntry[1].build().name;
|
|
2219
|
+
mainTableColumns.add(idDbCol);
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
// Fallback to 'id' if not found in schema
|
|
2223
|
+
mainTableColumns.add('id');
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2140
2226
|
// Build RETURNING clause for CTE with all needed columns from main table
|
|
2141
2227
|
const cteReturningCols = Array.from(mainTableColumns).map(col => `"${col}"`).join(', ');
|
|
2142
2228
|
// Add RETURNING to the mutation SQL for CTE
|
|
@@ -2166,6 +2252,10 @@ class SelectQueryBuilder {
|
|
|
2166
2252
|
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2167
2253
|
joinClauses.push(`${joinType} ${qualifiedJoinTable} AS "${join.alias}" ON ${joinConditions.join(' AND ')}`);
|
|
2168
2254
|
}
|
|
2255
|
+
// Add LATERAL JOINs for collections
|
|
2256
|
+
for (const collection of collectionSubqueries) {
|
|
2257
|
+
joinClauses.push(collection.joinClause);
|
|
2258
|
+
}
|
|
2169
2259
|
// Build the final CTE query
|
|
2170
2260
|
const sql = `WITH "__mutation__" AS (
|
|
2171
2261
|
${mutationWithReturning}
|
|
@@ -2173,47 +2263,95 @@ class SelectQueryBuilder {
|
|
|
2173
2263
|
SELECT ${selectParts.join(', ')}
|
|
2174
2264
|
FROM "__mutation__"
|
|
2175
2265
|
${joinClauses.join('\n')}`;
|
|
2176
|
-
return { sql, params:
|
|
2266
|
+
return { sql, params: allParams, nestedPaths };
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Rewrite a LATERAL JOIN clause to reference __mutation__ instead of the source table
|
|
2270
|
+
* @internal
|
|
2271
|
+
*/
|
|
2272
|
+
rewriteCollectionJoinForMutation(joinClause, sourceTable, mutationAlias) {
|
|
2273
|
+
const sourcePattern = new RegExp(`"${sourceTable}"\\."`, 'g');
|
|
2274
|
+
return joinClause.replace(sourcePattern, `"${mutationAlias}"."`);
|
|
2275
|
+
}
|
|
2276
|
+
/**
|
|
2277
|
+
* Rewrite a correlated subquery expression to reference __mutation__ instead of the source table
|
|
2278
|
+
* @internal
|
|
2279
|
+
*/
|
|
2280
|
+
rewriteCollectionExprForMutation(expression, sourceTable, mutationAlias) {
|
|
2281
|
+
const sourcePattern = new RegExp(`"${sourceTable}"\\."`, 'g');
|
|
2282
|
+
return expression.replace(sourcePattern, `"${mutationAlias}"."`);
|
|
2177
2283
|
}
|
|
2178
2284
|
/**
|
|
2179
2285
|
* Map RETURNING results with navigation properties
|
|
2180
2286
|
* Applies type mappers based on source table schemas
|
|
2287
|
+
* Reconstructs nested objects from flat column paths
|
|
2181
2288
|
* @internal
|
|
2182
2289
|
*/
|
|
2183
|
-
mapReturningResultsWithNavigation(rows, returning, navigationFields) {
|
|
2290
|
+
mapReturningResultsWithNavigation(rows, returning, navigationFields, nestedPaths) {
|
|
2184
2291
|
return rows.map(row => {
|
|
2185
2292
|
const mapped = {};
|
|
2186
2293
|
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;
|
|
2294
|
+
// Handle nested paths (e.g., "invoicingPartner.id" -> { invoicingPartner: { id: value } })
|
|
2295
|
+
if (key.includes('.')) {
|
|
2296
|
+
const parts = key.split('.');
|
|
2297
|
+
let current = mapped;
|
|
2298
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2299
|
+
if (!current[parts[i]]) {
|
|
2300
|
+
current[parts[i]] = {};
|
|
2202
2301
|
}
|
|
2302
|
+
current = current[parts[i]];
|
|
2203
2303
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2304
|
+
const finalKey = parts[parts.length - 1];
|
|
2305
|
+
// Check if this is a navigation field
|
|
2306
|
+
const navInfo = navigationFields.get(key);
|
|
2307
|
+
if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
|
|
2308
|
+
const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
|
|
2309
|
+
if (targetSchema) {
|
|
2310
|
+
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2311
|
+
const config = col.build();
|
|
2312
|
+
return config.name === navInfo.dbColumnName;
|
|
2313
|
+
});
|
|
2314
|
+
if (colEntry) {
|
|
2315
|
+
const config = colEntry[1].build();
|
|
2316
|
+
current[finalKey] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
current[finalKey] = value;
|
|
2214
2322
|
}
|
|
2215
2323
|
else {
|
|
2216
|
-
|
|
2324
|
+
// Check if this is a navigation field
|
|
2325
|
+
const navInfo = navigationFields.get(key);
|
|
2326
|
+
if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
|
|
2327
|
+
// Try to get mapper from the navigation target's schema
|
|
2328
|
+
const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
|
|
2329
|
+
if (targetSchema) {
|
|
2330
|
+
// Find the column and its mapper
|
|
2331
|
+
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2332
|
+
const config = col.build();
|
|
2333
|
+
return config.name === navInfo.dbColumnName;
|
|
2334
|
+
});
|
|
2335
|
+
if (colEntry) {
|
|
2336
|
+
const config = colEntry[1].build();
|
|
2337
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
// Try to find column by alias or name in main schema
|
|
2343
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
|
|
2344
|
+
const config = col.build();
|
|
2345
|
+
return propName === key || config.name === key;
|
|
2346
|
+
});
|
|
2347
|
+
if (colEntry) {
|
|
2348
|
+
const [, col] = colEntry;
|
|
2349
|
+
const config = col.build();
|
|
2350
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2351
|
+
}
|
|
2352
|
+
else {
|
|
2353
|
+
mapped[key] = value;
|
|
2354
|
+
}
|
|
2217
2355
|
}
|
|
2218
2356
|
}
|
|
2219
2357
|
return mapped;
|