linkgress-orm 0.2.4 → 0.2.5
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 +21 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +245 -1
- package/dist/entity/db-context.js.map +1 -1
- package/dist/query/query-builder.d.ts +20 -1
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +266 -10
- package/dist/query/query-builder.js.map +1 -1
- package/package.json +3 -2
|
@@ -1728,14 +1728,63 @@ class SelectQueryBuilder {
|
|
|
1728
1728
|
if (!queryBuilder.whereCond) {
|
|
1729
1729
|
throw new Error('Delete requires a WHERE condition. Use where() before delete().');
|
|
1730
1730
|
}
|
|
1731
|
+
// Detect navigation property joins from WHERE condition
|
|
1732
|
+
const whereJoins = [];
|
|
1733
|
+
queryBuilder.detectAndAddJoinsFromCondition(queryBuilder.whereCond, whereJoins);
|
|
1731
1734
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1732
1735
|
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
|
|
1733
|
-
|
|
1736
|
+
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1737
|
+
// Build USING clause for navigation properties (PostgreSQL syntax for DELETE with JOINs)
|
|
1738
|
+
let usingClause = '';
|
|
1739
|
+
let joinConditions = [];
|
|
1740
|
+
for (const join of whereJoins) {
|
|
1741
|
+
const sourceTable = join.sourceAlias || queryBuilder.schema.name;
|
|
1742
|
+
const joinTableName = queryBuilder.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
1743
|
+
if (usingClause) {
|
|
1744
|
+
usingClause += `, ${joinTableName} AS "${join.alias}"`;
|
|
1745
|
+
}
|
|
1746
|
+
else {
|
|
1747
|
+
usingClause = `USING ${joinTableName} AS "${join.alias}"`;
|
|
1748
|
+
}
|
|
1749
|
+
// Build ON conditions as part of WHERE clause
|
|
1750
|
+
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
1751
|
+
const fk = join.foreignKeys[i];
|
|
1752
|
+
const match = join.matches[i];
|
|
1753
|
+
joinConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
// Combine join conditions with the original WHERE clause
|
|
1757
|
+
const fullWhereClause = joinConditions.length > 0
|
|
1758
|
+
? `${joinConditions.join(' AND ')} AND ${whereSql}`
|
|
1759
|
+
: whereSql;
|
|
1760
|
+
// Check if RETURNING uses navigation properties
|
|
1761
|
+
const navigationInfo = returning && returning !== 'count' && returning !== true
|
|
1762
|
+
? queryBuilder.detectNavigationInReturning(returning)
|
|
1763
|
+
: null;
|
|
1764
|
+
if (navigationInfo) {
|
|
1765
|
+
// Use CTE-based approach for navigation properties in RETURNING
|
|
1766
|
+
let deleteSql = `DELETE FROM ${qualifiedTableName}`;
|
|
1767
|
+
if (usingClause) {
|
|
1768
|
+
deleteSql += ` ${usingClause}`;
|
|
1769
|
+
}
|
|
1770
|
+
deleteSql += ` WHERE ${fullWhereClause}`;
|
|
1771
|
+
const { sql, params } = queryBuilder.buildReturningWithNavigation(deleteSql, whereParams, returning, navigationInfo);
|
|
1772
|
+
const result = queryBuilder.executor
|
|
1773
|
+
? await queryBuilder.executor.query(sql, params)
|
|
1774
|
+
: await queryBuilder.client.query(sql, params);
|
|
1775
|
+
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
|
|
1776
|
+
}
|
|
1777
|
+
// Standard RETURNING (no navigation properties)
|
|
1778
|
+
// Qualify columns with table name when using USING clause to avoid ambiguity
|
|
1779
|
+
const hasJoins = whereJoins.length > 0;
|
|
1734
1780
|
const returningClause = returning !== 'count'
|
|
1735
|
-
? queryBuilder.
|
|
1781
|
+
? queryBuilder.buildUpdateDeleteReturningClause(returning, hasJoins)
|
|
1736
1782
|
: undefined;
|
|
1737
|
-
|
|
1738
|
-
|
|
1783
|
+
let sql = `DELETE FROM ${qualifiedTableName}`;
|
|
1784
|
+
if (usingClause) {
|
|
1785
|
+
sql += ` ${usingClause}`;
|
|
1786
|
+
}
|
|
1787
|
+
sql += ` WHERE ${fullWhereClause}`;
|
|
1739
1788
|
if (returningClause) {
|
|
1740
1789
|
sql += ` RETURNING ${returningClause.sql}`;
|
|
1741
1790
|
}
|
|
@@ -1820,11 +1869,24 @@ class SelectQueryBuilder {
|
|
|
1820
1869
|
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1821
1870
|
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
|
|
1822
1871
|
values.push(...whereParams);
|
|
1823
|
-
|
|
1872
|
+
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1873
|
+
// Check if RETURNING uses navigation properties
|
|
1874
|
+
const navigationInfo = returning && returning !== 'count' && returning !== true
|
|
1875
|
+
? queryBuilder.detectNavigationInReturning(returning)
|
|
1876
|
+
: null;
|
|
1877
|
+
if (navigationInfo) {
|
|
1878
|
+
// Use CTE-based approach for navigation properties in RETURNING
|
|
1879
|
+
const updateSql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
|
|
1880
|
+
const { sql, params } = queryBuilder.buildReturningWithNavigation(updateSql, values, returning, navigationInfo);
|
|
1881
|
+
const result = queryBuilder.executor
|
|
1882
|
+
? await queryBuilder.executor.query(sql, params)
|
|
1883
|
+
: await queryBuilder.client.query(sql, params);
|
|
1884
|
+
return queryBuilder.mapReturningResultsWithNavigation(result.rows, returning, navigationInfo.navigationFields);
|
|
1885
|
+
}
|
|
1886
|
+
// Standard RETURNING (no navigation properties)
|
|
1824
1887
|
const returningClause = returning !== 'count'
|
|
1825
|
-
? queryBuilder.
|
|
1888
|
+
? queryBuilder.buildUpdateDeleteReturningClause(returning)
|
|
1826
1889
|
: undefined;
|
|
1827
|
-
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1828
1890
|
let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
|
|
1829
1891
|
if (returningClause) {
|
|
1830
1892
|
sql += ` RETURNING ${returningClause.sql}`;
|
|
@@ -1864,16 +1926,19 @@ class SelectQueryBuilder {
|
|
|
1864
1926
|
}
|
|
1865
1927
|
/**
|
|
1866
1928
|
* Build RETURNING clause for delete/update operations
|
|
1929
|
+
* @param returning - The returning configuration
|
|
1930
|
+
* @param qualifyWithTable - If true, qualify column names with the main table name (needed for DELETE with USING)
|
|
1867
1931
|
* @internal
|
|
1868
1932
|
*/
|
|
1869
|
-
|
|
1933
|
+
buildUpdateDeleteReturningClause(returning, qualifyWithTable = false) {
|
|
1870
1934
|
if (returning === undefined) {
|
|
1871
1935
|
return null;
|
|
1872
1936
|
}
|
|
1937
|
+
const tablePrefix = qualifyWithTable ? `"${this.schema.name}".` : '';
|
|
1873
1938
|
if (returning === true) {
|
|
1874
1939
|
// Return all columns
|
|
1875
1940
|
const columns = Object.values(this.schema.columns).map(col => col.build().name);
|
|
1876
|
-
const sql = columns.map(name =>
|
|
1941
|
+
const sql = columns.map(name => `${tablePrefix}"${name}"`).join(', ');
|
|
1877
1942
|
return { sql, columns };
|
|
1878
1943
|
}
|
|
1879
1944
|
// Selector function - extract selected columns
|
|
@@ -1887,7 +1952,7 @@ class SelectQueryBuilder {
|
|
|
1887
1952
|
if (field && typeof field === 'object' && '__dbColumnName' in field) {
|
|
1888
1953
|
const dbName = field.__dbColumnName;
|
|
1889
1954
|
columns.push(alias);
|
|
1890
|
-
sqlParts.push(
|
|
1955
|
+
sqlParts.push(`${tablePrefix}"${dbName}" AS "${alias}"`);
|
|
1891
1956
|
}
|
|
1892
1957
|
}
|
|
1893
1958
|
return { sql: sqlParts.join(', '), columns };
|
|
@@ -1934,6 +1999,197 @@ class SelectQueryBuilder {
|
|
|
1934
1999
|
return mapped;
|
|
1935
2000
|
});
|
|
1936
2001
|
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Detect if a RETURNING selector uses navigation properties
|
|
2004
|
+
* Returns navigation info if found, null otherwise
|
|
2005
|
+
* @internal
|
|
2006
|
+
*/
|
|
2007
|
+
detectNavigationInReturning(returning) {
|
|
2008
|
+
if (returning === true) {
|
|
2009
|
+
// Full entity returning doesn't need navigation support
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
// Analyze the returning selector
|
|
2013
|
+
const mockRow = this._createMockRow();
|
|
2014
|
+
const selectedMock = this.selector(mockRow);
|
|
2015
|
+
const selection = returning(selectedMock);
|
|
2016
|
+
if (typeof selection !== 'object' || selection === null) {
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
2019
|
+
const navigationFields = new Map();
|
|
2020
|
+
const allTableAliases = new Set();
|
|
2021
|
+
// Check each field in the selection for navigation properties
|
|
2022
|
+
for (const [alias, field] of Object.entries(selection)) {
|
|
2023
|
+
if (field && typeof field === 'object' && '__dbColumnName' in field && '__tableAlias' in field) {
|
|
2024
|
+
const tableAlias = field.__tableAlias;
|
|
2025
|
+
if (tableAlias && tableAlias !== this.schema.name) {
|
|
2026
|
+
allTableAliases.add(tableAlias);
|
|
2027
|
+
navigationFields.set(alias, {
|
|
2028
|
+
tableAlias,
|
|
2029
|
+
dbColumnName: field.__dbColumnName,
|
|
2030
|
+
schemaTable: field.__sourceTable,
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
// Also collect intermediate navigation aliases for multi-level navigation
|
|
2034
|
+
if ('__navigationAliases' in field && Array.isArray(field.__navigationAliases)) {
|
|
2035
|
+
for (const navAlias of field.__navigationAliases) {
|
|
2036
|
+
if (navAlias && navAlias !== this.schema.name) {
|
|
2037
|
+
allTableAliases.add(navAlias);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (navigationFields.size === 0) {
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
// Resolve navigation joins
|
|
2047
|
+
const joins = [];
|
|
2048
|
+
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
2049
|
+
return { hasNavigation: true, selection, joins, navigationFields };
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Build RETURNING clause with navigation property support using CTE
|
|
2053
|
+
* @internal
|
|
2054
|
+
*/
|
|
2055
|
+
buildReturningWithNavigation(mutationSql, mutationParams, returning, navigationInfo) {
|
|
2056
|
+
// Build the CTE wrapping the mutation
|
|
2057
|
+
// First, collect all columns needed from the main table in the mutation's RETURNING
|
|
2058
|
+
const mainTableColumns = new Set();
|
|
2059
|
+
const selectParts = [];
|
|
2060
|
+
const mockRow = this._createMockRow();
|
|
2061
|
+
const selectedMock = this.selector(mockRow);
|
|
2062
|
+
const selection = returning(selectedMock);
|
|
2063
|
+
// For each selected field, determine if it's from main table or navigation
|
|
2064
|
+
for (const [alias, field] of Object.entries(selection)) {
|
|
2065
|
+
if (field && typeof field === 'object' && '__dbColumnName' in field) {
|
|
2066
|
+
const tableAlias = field.__tableAlias;
|
|
2067
|
+
const dbColumnName = field.__dbColumnName;
|
|
2068
|
+
if (!tableAlias || tableAlias === this.schema.name) {
|
|
2069
|
+
// Main table column - add to CTE RETURNING and final SELECT
|
|
2070
|
+
mainTableColumns.add(dbColumnName);
|
|
2071
|
+
selectParts.push(`"__mutation__"."${dbColumnName}" AS "${alias}"`);
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
// Navigation column - will be joined in outer query
|
|
2075
|
+
selectParts.push(`"${tableAlias}"."${dbColumnName}" AS "${alias}"`);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
// We also need to include foreign keys for joins that aren't already in the selection
|
|
2080
|
+
for (const join of navigationInfo.joins) {
|
|
2081
|
+
for (const fk of join.foreignKeys) {
|
|
2082
|
+
// Find the db column name for this foreign key
|
|
2083
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === fk);
|
|
2084
|
+
if (colEntry) {
|
|
2085
|
+
const config = colEntry[1].build();
|
|
2086
|
+
const dbColName = config.name;
|
|
2087
|
+
mainTableColumns.add(dbColName);
|
|
2088
|
+
}
|
|
2089
|
+
else {
|
|
2090
|
+
// FK might be the db column name directly
|
|
2091
|
+
mainTableColumns.add(fk);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
// Also include foreign keys from intermediate joins (join to another join)
|
|
2096
|
+
for (const join of navigationInfo.joins) {
|
|
2097
|
+
if (join.sourceAlias && join.sourceAlias !== this.schema.name) {
|
|
2098
|
+
// This join comes from another join, we need to include those FK columns too
|
|
2099
|
+
// Find the source join
|
|
2100
|
+
const sourceJoin = navigationInfo.joins.find(j => j.alias === join.sourceAlias);
|
|
2101
|
+
if (sourceJoin) {
|
|
2102
|
+
// We need the source table's FK columns in our RETURNING if the source is the main table
|
|
2103
|
+
// But if source is itself a navigation, we handle it in the outer SELECT
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
// Build RETURNING clause for CTE with all needed columns from main table
|
|
2108
|
+
const cteReturningCols = Array.from(mainTableColumns).map(col => `"${col}"`).join(', ');
|
|
2109
|
+
// Add RETURNING to the mutation SQL for CTE
|
|
2110
|
+
const mutationWithReturning = `${mutationSql} RETURNING ${cteReturningCols}`;
|
|
2111
|
+
// Build the JOINs for the outer SELECT
|
|
2112
|
+
const joinClauses = [];
|
|
2113
|
+
for (const join of navigationInfo.joins) {
|
|
2114
|
+
const sourceAlias = join.sourceAlias === this.schema.name ? '__mutation__' : `"${join.sourceAlias}"`;
|
|
2115
|
+
const qualifiedJoinTable = this.getQualifiedTableName(join.targetTable, join.targetSchema);
|
|
2116
|
+
// Find the db column names for the foreign keys
|
|
2117
|
+
const joinConditions = [];
|
|
2118
|
+
for (let i = 0; i < join.foreignKeys.length; i++) {
|
|
2119
|
+
const fk = join.foreignKeys[i];
|
|
2120
|
+
const match = join.matches[i] || 'id';
|
|
2121
|
+
// Convert property name to db column name for FK
|
|
2122
|
+
let fkDbCol = fk;
|
|
2123
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, _]) => propName === fk);
|
|
2124
|
+
if (colEntry) {
|
|
2125
|
+
const config = colEntry[1].build();
|
|
2126
|
+
fkDbCol = config.name;
|
|
2127
|
+
}
|
|
2128
|
+
// For source that's the mutation CTE
|
|
2129
|
+
if (join.sourceAlias === this.schema.name || !join.sourceAlias) {
|
|
2130
|
+
joinConditions.push(`"__mutation__"."${fkDbCol}" = "${join.alias}"."${match}"`);
|
|
2131
|
+
}
|
|
2132
|
+
else {
|
|
2133
|
+
// Source is another joined table
|
|
2134
|
+
joinConditions.push(`"${join.sourceAlias}"."${fk}" = "${join.alias}"."${match}"`);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
|
|
2138
|
+
joinClauses.push(`${joinType} ${qualifiedJoinTable} AS "${join.alias}" ON ${joinConditions.join(' AND ')}`);
|
|
2139
|
+
}
|
|
2140
|
+
// Build the final CTE query
|
|
2141
|
+
const sql = `WITH "__mutation__" AS (
|
|
2142
|
+
${mutationWithReturning}
|
|
2143
|
+
)
|
|
2144
|
+
SELECT ${selectParts.join(', ')}
|
|
2145
|
+
FROM "__mutation__"
|
|
2146
|
+
${joinClauses.join('\n')}`;
|
|
2147
|
+
return { sql, params: mutationParams };
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Map RETURNING results with navigation properties
|
|
2151
|
+
* Applies type mappers based on source table schemas
|
|
2152
|
+
* @internal
|
|
2153
|
+
*/
|
|
2154
|
+
mapReturningResultsWithNavigation(rows, returning, navigationFields) {
|
|
2155
|
+
return rows.map(row => {
|
|
2156
|
+
const mapped = {};
|
|
2157
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2158
|
+
// Check if this is a navigation field
|
|
2159
|
+
const navInfo = navigationFields.get(key);
|
|
2160
|
+
if (navInfo && navInfo.schemaTable && this.schemaRegistry) {
|
|
2161
|
+
// Try to get mapper from the navigation target's schema
|
|
2162
|
+
const targetSchema = this.schemaRegistry.get(navInfo.schemaTable);
|
|
2163
|
+
if (targetSchema) {
|
|
2164
|
+
// Find the column and its mapper
|
|
2165
|
+
const colEntry = Object.entries(targetSchema.columns).find(([_, col]) => {
|
|
2166
|
+
const config = col.build();
|
|
2167
|
+
return config.name === navInfo.dbColumnName;
|
|
2168
|
+
});
|
|
2169
|
+
if (colEntry) {
|
|
2170
|
+
const config = colEntry[1].build();
|
|
2171
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
// Try to find column by alias or name in main schema
|
|
2177
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
|
|
2178
|
+
const config = col.build();
|
|
2179
|
+
return propName === key || config.name === key;
|
|
2180
|
+
});
|
|
2181
|
+
if (colEntry) {
|
|
2182
|
+
const [, col] = colEntry;
|
|
2183
|
+
const config = col.build();
|
|
2184
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
2185
|
+
}
|
|
2186
|
+
else {
|
|
2187
|
+
mapped[key] = value;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return mapped;
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
1937
2193
|
/**
|
|
1938
2194
|
* Create mock row for analysis
|
|
1939
2195
|
* @internal
|