linkgress-orm 0.1.16 → 0.1.18
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 +12 -0
- package/dist/entity/db-context.d.ts +146 -45
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +170 -160
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/query/conditions.d.ts +108 -0
- package/dist/query/conditions.d.ts.map +1 -1
- package/dist/query/conditions.js +131 -0
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/cte-builder.d.ts +1 -1
- package/dist/query/cte-builder.js +8 -8
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +8 -0
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +12 -0
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts +62 -3
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +275 -25
- package/dist/query/query-builder.js.map +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ var FieldType;
|
|
|
27
27
|
FieldType[FieldType["FIELD_REF_MAPPER"] = 6] = "FIELD_REF_MAPPER";
|
|
28
28
|
FieldType[FieldType["FIELD_REF_NO_MAPPER"] = 7] = "FIELD_REF_NO_MAPPER";
|
|
29
29
|
FieldType[FieldType["SIMPLE"] = 8] = "SIMPLE";
|
|
30
|
+
FieldType[FieldType["COLLECTION_SINGLE"] = 9] = "COLLECTION_SINGLE";
|
|
30
31
|
})(FieldType || (FieldType = {}));
|
|
31
32
|
/**
|
|
32
33
|
* Performance utility: Get column name map from schema, using cached version if available
|
|
@@ -175,7 +176,7 @@ class QueryBuilder {
|
|
|
175
176
|
* Multiple where() calls are chained with AND logic
|
|
176
177
|
*/
|
|
177
178
|
where(condition) {
|
|
178
|
-
const mockRow = this.
|
|
179
|
+
const mockRow = this._createMockRow();
|
|
179
180
|
const newCondition = condition(mockRow);
|
|
180
181
|
if (this.whereCond) {
|
|
181
182
|
this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
|
|
@@ -194,8 +195,9 @@ class QueryBuilder {
|
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
196
197
|
* Create mock row for analysis
|
|
198
|
+
* @internal - Also used by DbEntityTable.props() to avoid code duplication
|
|
197
199
|
*/
|
|
198
|
-
|
|
200
|
+
_createMockRow() {
|
|
199
201
|
// Performance: Return cached mock if available
|
|
200
202
|
if (this._cachedMockRow) {
|
|
201
203
|
return this._cachedMockRow;
|
|
@@ -293,7 +295,7 @@ class QueryBuilder {
|
|
|
293
295
|
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
294
296
|
const newJoinCounter = this.joinCounter + 1;
|
|
295
297
|
// Create mock rows for condition evaluation
|
|
296
|
-
const mockLeft = this.
|
|
298
|
+
const mockLeft = this._createMockRow();
|
|
297
299
|
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
298
300
|
const joinCondition = condition(mockLeft, mockRight);
|
|
299
301
|
// Add the join to the list
|
|
@@ -306,7 +308,7 @@ class QueryBuilder {
|
|
|
306
308
|
}];
|
|
307
309
|
// Store schemas for creating fresh mocks in the selector
|
|
308
310
|
const leftSchema = this.schema;
|
|
309
|
-
const createLeftMock = () => this.
|
|
311
|
+
const createLeftMock = () => this._createMockRow();
|
|
310
312
|
const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
|
|
311
313
|
// Create a selector wrapper that generates fresh mocks and calls the user's selector
|
|
312
314
|
const wrappedSelector = (row) => {
|
|
@@ -342,7 +344,7 @@ class QueryBuilder {
|
|
|
342
344
|
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
343
345
|
const newJoinCounter = this.joinCounter + 1;
|
|
344
346
|
// Create mock rows for condition evaluation
|
|
345
|
-
const mockLeft = this.
|
|
347
|
+
const mockLeft = this._createMockRow();
|
|
346
348
|
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
347
349
|
const joinCondition = condition(mockLeft, mockRight);
|
|
348
350
|
// Add the join to the list
|
|
@@ -355,7 +357,7 @@ class QueryBuilder {
|
|
|
355
357
|
}];
|
|
356
358
|
// Store schemas for creating fresh mocks in the selector
|
|
357
359
|
const leftSchema = this.schema;
|
|
358
|
-
const createLeftMock = () => this.
|
|
360
|
+
const createLeftMock = () => this._createMockRow();
|
|
359
361
|
const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
|
|
360
362
|
// Create a selector wrapper that generates fresh mocks and calls the user's selector
|
|
361
363
|
const wrappedSelector = (row) => {
|
|
@@ -441,7 +443,7 @@ class QueryBuilder {
|
|
|
441
443
|
return this;
|
|
442
444
|
}
|
|
443
445
|
orderBy(selector) {
|
|
444
|
-
const mockRow = this.
|
|
446
|
+
const mockRow = this._createMockRow();
|
|
445
447
|
const result = selector(mockRow);
|
|
446
448
|
(0, query_utils_1.parseOrderBy)(result, this.orderByFields);
|
|
447
449
|
return this;
|
|
@@ -497,7 +499,7 @@ class SelectQueryBuilder {
|
|
|
497
499
|
* Note: The row parameter represents the selected shape (after select())
|
|
498
500
|
*/
|
|
499
501
|
where(condition) {
|
|
500
|
-
const mockRow = this.
|
|
502
|
+
const mockRow = this._createMockRow();
|
|
501
503
|
// Apply the selector to get the selected shape that the user sees in the WHERE condition
|
|
502
504
|
const selectedMock = this.selector(mockRow);
|
|
503
505
|
// Wrap in proxy - for WHERE, we preserve original column names
|
|
@@ -545,7 +547,7 @@ class SelectQueryBuilder {
|
|
|
545
547
|
return this;
|
|
546
548
|
}
|
|
547
549
|
orderBy(selector) {
|
|
548
|
-
const mockRow = this.
|
|
550
|
+
const mockRow = this._createMockRow();
|
|
549
551
|
const selectedMock = this.selector(mockRow);
|
|
550
552
|
// Wrap selectedMock in a proxy that returns FieldRefs for property access
|
|
551
553
|
const fieldRefProxy = this.createFieldRefProxy(selectedMock);
|
|
@@ -577,7 +579,7 @@ class SelectQueryBuilder {
|
|
|
577
579
|
leftJoinSubquery(subquery, alias, condition, selector) {
|
|
578
580
|
const newJoinCounter = this.joinCounter + 1;
|
|
579
581
|
// Create mock for the current selection (left side)
|
|
580
|
-
const mockRow = this.
|
|
582
|
+
const mockRow = this._createMockRow();
|
|
581
583
|
const mockLeftSelection = this.selector(mockRow);
|
|
582
584
|
// Create mock for the subquery result (right side)
|
|
583
585
|
// For subqueries, we create a mock based on the result type
|
|
@@ -621,7 +623,7 @@ class SelectQueryBuilder {
|
|
|
621
623
|
const newJoinCounter = this.joinCounter + 1;
|
|
622
624
|
// Create mock for the current selection (left side)
|
|
623
625
|
// IMPORTANT: We call the selector with the mock row to get a result that contains FieldRef objects
|
|
624
|
-
const mockRow = this.
|
|
626
|
+
const mockRow = this._createMockRow();
|
|
625
627
|
const mockLeftSelection = this.selector(mockRow);
|
|
626
628
|
// The mockLeftSelection now contains FieldRef objects (with __fieldName, __dbColumnName, __tableAlias)
|
|
627
629
|
// These FieldRef objects preserve the table context
|
|
@@ -652,7 +654,7 @@ class SelectQueryBuilder {
|
|
|
652
654
|
leftJoinCte(cte, condition, selector) {
|
|
653
655
|
const newJoinCounter = this.joinCounter + 1;
|
|
654
656
|
// Create mock for the current selection (left side)
|
|
655
|
-
const mockRow = this.
|
|
657
|
+
const mockRow = this._createMockRow();
|
|
656
658
|
const mockLeftSelection = this.selector(mockRow);
|
|
657
659
|
// Create mock for the CTE columns (right side)
|
|
658
660
|
const mockRight = this.createMockRowForCte(cte);
|
|
@@ -685,7 +687,7 @@ class SelectQueryBuilder {
|
|
|
685
687
|
innerJoinSubquery(subquery, alias, condition, selector) {
|
|
686
688
|
const newJoinCounter = this.joinCounter + 1;
|
|
687
689
|
// Create mock for the current selection (left side)
|
|
688
|
-
const mockRow = this.
|
|
690
|
+
const mockRow = this._createMockRow();
|
|
689
691
|
const mockLeftSelection = this.selector(mockRow);
|
|
690
692
|
// Create mock for the subquery result (right side)
|
|
691
693
|
const mockRight = this.createMockRowForSubquery(alias, subquery);
|
|
@@ -727,7 +729,7 @@ class SelectQueryBuilder {
|
|
|
727
729
|
const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
|
|
728
730
|
const newJoinCounter = this.joinCounter + 1;
|
|
729
731
|
// Create mock for the current selection (left side)
|
|
730
|
-
const mockRow = this.
|
|
732
|
+
const mockRow = this._createMockRow();
|
|
731
733
|
const mockLeftSelection = this.selector(mockRow);
|
|
732
734
|
// Create mock for the right table
|
|
733
735
|
const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
|
|
@@ -936,13 +938,13 @@ class SelectQueryBuilder {
|
|
|
936
938
|
// If selector is provided, apply it to determine the field
|
|
937
939
|
let fieldToAggregate;
|
|
938
940
|
if (selector) {
|
|
939
|
-
const mockRow = this.
|
|
941
|
+
const mockRow = this._createMockRow();
|
|
940
942
|
const mockSelection = this.selector(mockRow);
|
|
941
943
|
fieldToAggregate = selector(mockSelection);
|
|
942
944
|
}
|
|
943
945
|
else {
|
|
944
946
|
// No selector - use the current selection
|
|
945
|
-
const mockRow = this.
|
|
947
|
+
const mockRow = this._createMockRow();
|
|
946
948
|
fieldToAggregate = this.selector(mockRow);
|
|
947
949
|
}
|
|
948
950
|
// Build aggregation query
|
|
@@ -967,13 +969,13 @@ class SelectQueryBuilder {
|
|
|
967
969
|
// If selector is provided, apply it to determine the field
|
|
968
970
|
let fieldToAggregate;
|
|
969
971
|
if (selector) {
|
|
970
|
-
const mockRow = this.
|
|
972
|
+
const mockRow = this._createMockRow();
|
|
971
973
|
const mockSelection = this.selector(mockRow);
|
|
972
974
|
fieldToAggregate = selector(mockSelection);
|
|
973
975
|
}
|
|
974
976
|
else {
|
|
975
977
|
// No selector - use the current selection
|
|
976
|
-
const mockRow = this.
|
|
978
|
+
const mockRow = this._createMockRow();
|
|
977
979
|
fieldToAggregate = this.selector(mockRow);
|
|
978
980
|
}
|
|
979
981
|
// Build aggregation query
|
|
@@ -998,13 +1000,13 @@ class SelectQueryBuilder {
|
|
|
998
1000
|
// If selector is provided, apply it to determine the field
|
|
999
1001
|
let fieldToAggregate;
|
|
1000
1002
|
if (selector) {
|
|
1001
|
-
const mockRow = this.
|
|
1003
|
+
const mockRow = this._createMockRow();
|
|
1002
1004
|
const mockSelection = this.selector(mockRow);
|
|
1003
1005
|
fieldToAggregate = selector(mockSelection);
|
|
1004
1006
|
}
|
|
1005
1007
|
else {
|
|
1006
1008
|
// No selector - use the current selection
|
|
1007
|
-
const mockRow = this.
|
|
1009
|
+
const mockRow = this._createMockRow();
|
|
1008
1010
|
fieldToAggregate = this.selector(mockRow);
|
|
1009
1011
|
}
|
|
1010
1012
|
// Build aggregation query
|
|
@@ -1052,7 +1054,7 @@ class SelectQueryBuilder {
|
|
|
1052
1054
|
executor: this.executor,
|
|
1053
1055
|
}));
|
|
1054
1056
|
// Analyze the selector to extract nested queries
|
|
1055
|
-
const mockRow = tracer.trace('createMockRow', () => this.
|
|
1057
|
+
const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
|
|
1056
1058
|
const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
|
|
1057
1059
|
// Check if we're using temp table strategy and have collections
|
|
1058
1060
|
const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
|
|
@@ -1319,7 +1321,7 @@ class SelectQueryBuilder {
|
|
|
1319
1321
|
const baseSelection = {};
|
|
1320
1322
|
const collectionNames = new Set(collections.map(c => c.name));
|
|
1321
1323
|
// Always ensure we have the primary key in the base selection with a known alias
|
|
1322
|
-
const mockRow = this.
|
|
1324
|
+
const mockRow = this._createMockRow();
|
|
1323
1325
|
baseSelection['__pk_id'] = mockRow.id; // Add primary key with a known alias
|
|
1324
1326
|
for (const [key, value] of Object.entries(selection)) {
|
|
1325
1327
|
if (!collectionNames.has(key)) {
|
|
@@ -1413,6 +1415,10 @@ class SelectQueryBuilder {
|
|
|
1413
1415
|
// Array aggregation
|
|
1414
1416
|
return [];
|
|
1415
1417
|
}
|
|
1418
|
+
else if (builder.isSingleResult()) {
|
|
1419
|
+
// firstOrDefault() - single item
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1416
1422
|
else {
|
|
1417
1423
|
// JSONB aggregation (object array)
|
|
1418
1424
|
return [];
|
|
@@ -1441,10 +1447,215 @@ class SelectQueryBuilder {
|
|
|
1441
1447
|
}
|
|
1442
1448
|
return result;
|
|
1443
1449
|
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Delete records matching the current WHERE condition
|
|
1452
|
+
* Returns a fluent builder that can be awaited directly or chained with .returning()
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* ```typescript
|
|
1456
|
+
* // No returning (default) - returns void
|
|
1457
|
+
* await db.users.where(u => eq(u.id, 1)).delete();
|
|
1458
|
+
*
|
|
1459
|
+
* // With returning() - returns full entities
|
|
1460
|
+
* const deleted = await db.users.where(u => eq(u.id, 1)).delete().returning();
|
|
1461
|
+
*
|
|
1462
|
+
* // With returning(selector) - returns selected columns
|
|
1463
|
+
* const results = await db.users.where(u => eq(u.isActive, false)).delete()
|
|
1464
|
+
* .returning(u => ({ id: u.id, username: u.username }));
|
|
1465
|
+
* ```
|
|
1466
|
+
*/
|
|
1467
|
+
delete() {
|
|
1468
|
+
const queryBuilder = this;
|
|
1469
|
+
const executeDelete = async (returning) => {
|
|
1470
|
+
// Build WHERE clause
|
|
1471
|
+
if (!queryBuilder.whereCond) {
|
|
1472
|
+
throw new Error('Delete requires a WHERE condition. Use where() before delete().');
|
|
1473
|
+
}
|
|
1474
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1475
|
+
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
|
|
1476
|
+
// Build RETURNING clause
|
|
1477
|
+
const returningClause = queryBuilder.buildDeleteReturningClause(returning);
|
|
1478
|
+
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1479
|
+
let sql = `DELETE FROM ${qualifiedTableName} WHERE ${whereSql}`;
|
|
1480
|
+
if (returningClause) {
|
|
1481
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
1482
|
+
}
|
|
1483
|
+
const result = queryBuilder.executor
|
|
1484
|
+
? await queryBuilder.executor.query(sql, whereParams)
|
|
1485
|
+
: await queryBuilder.client.query(sql, whereParams);
|
|
1486
|
+
if (!returningClause) {
|
|
1487
|
+
return undefined;
|
|
1488
|
+
}
|
|
1489
|
+
return queryBuilder.mapDeleteReturningResults(result.rows, returning);
|
|
1490
|
+
};
|
|
1491
|
+
return {
|
|
1492
|
+
then(onfulfilled, onrejected) {
|
|
1493
|
+
return executeDelete(undefined).then(onfulfilled, onrejected);
|
|
1494
|
+
},
|
|
1495
|
+
returning(selector) {
|
|
1496
|
+
const returningConfig = selector ?? true;
|
|
1497
|
+
return {
|
|
1498
|
+
then(onfulfilled, onrejected) {
|
|
1499
|
+
return executeDelete(returningConfig).then(onfulfilled, onrejected);
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Update records matching the current WHERE condition
|
|
1507
|
+
* Returns a fluent builder that can be awaited directly or chained with .returning()
|
|
1508
|
+
*
|
|
1509
|
+
* @param data Partial data to update
|
|
1510
|
+
*
|
|
1511
|
+
* @example
|
|
1512
|
+
* ```typescript
|
|
1513
|
+
* // No returning (default) - returns void
|
|
1514
|
+
* await db.users.where(u => eq(u.id, 1)).update({ age: 30 });
|
|
1515
|
+
*
|
|
1516
|
+
* // With returning() - returns full entities
|
|
1517
|
+
* const updated = await db.users.where(u => eq(u.id, 1)).update({ age: 30 }).returning();
|
|
1518
|
+
*
|
|
1519
|
+
* // With returning(selector) - returns selected columns
|
|
1520
|
+
* const results = await db.users.where(u => eq(u.isActive, true)).update({ lastLogin: new Date() })
|
|
1521
|
+
* .returning(u => ({ id: u.id, lastLogin: u.lastLogin }));
|
|
1522
|
+
* ```
|
|
1523
|
+
*/
|
|
1524
|
+
update(data) {
|
|
1525
|
+
const queryBuilder = this;
|
|
1526
|
+
const executeUpdate = async (returning) => {
|
|
1527
|
+
// Build WHERE clause
|
|
1528
|
+
if (!queryBuilder.whereCond) {
|
|
1529
|
+
throw new Error('Update requires a WHERE condition. Use where() before update().');
|
|
1530
|
+
}
|
|
1531
|
+
// Build SET clause
|
|
1532
|
+
const setClauses = [];
|
|
1533
|
+
const values = [];
|
|
1534
|
+
let paramIndex = 1;
|
|
1535
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1536
|
+
const column = queryBuilder.schema.columns[key];
|
|
1537
|
+
if (column) {
|
|
1538
|
+
const config = column.build();
|
|
1539
|
+
setClauses.push(`"${config.name}" = $${paramIndex++}`);
|
|
1540
|
+
// Apply toDriver mapper if present
|
|
1541
|
+
const mappedValue = config.mapper
|
|
1542
|
+
? config.mapper.toDriver(value)
|
|
1543
|
+
: value;
|
|
1544
|
+
values.push(mappedValue);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (setClauses.length === 0) {
|
|
1548
|
+
throw new Error('No valid columns to update');
|
|
1549
|
+
}
|
|
1550
|
+
const condBuilder = new conditions_1.ConditionBuilder();
|
|
1551
|
+
const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
|
|
1552
|
+
values.push(...whereParams);
|
|
1553
|
+
// Build RETURNING clause
|
|
1554
|
+
const returningClause = queryBuilder.buildDeleteReturningClause(returning);
|
|
1555
|
+
const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
|
|
1556
|
+
let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
|
|
1557
|
+
if (returningClause) {
|
|
1558
|
+
sql += ` RETURNING ${returningClause.sql}`;
|
|
1559
|
+
}
|
|
1560
|
+
const result = queryBuilder.executor
|
|
1561
|
+
? await queryBuilder.executor.query(sql, values)
|
|
1562
|
+
: await queryBuilder.client.query(sql, values);
|
|
1563
|
+
if (!returningClause) {
|
|
1564
|
+
return undefined;
|
|
1565
|
+
}
|
|
1566
|
+
return queryBuilder.mapDeleteReturningResults(result.rows, returning);
|
|
1567
|
+
};
|
|
1568
|
+
return {
|
|
1569
|
+
then(onfulfilled, onrejected) {
|
|
1570
|
+
return executeUpdate(undefined).then(onfulfilled, onrejected);
|
|
1571
|
+
},
|
|
1572
|
+
returning(selector) {
|
|
1573
|
+
const returningConfig = selector ?? true;
|
|
1574
|
+
return {
|
|
1575
|
+
then(onfulfilled, onrejected) {
|
|
1576
|
+
return executeUpdate(returningConfig).then(onfulfilled, onrejected);
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Build RETURNING clause for delete/update operations
|
|
1584
|
+
* @internal
|
|
1585
|
+
*/
|
|
1586
|
+
buildDeleteReturningClause(returning) {
|
|
1587
|
+
if (returning === undefined) {
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
if (returning === true) {
|
|
1591
|
+
// Return all columns
|
|
1592
|
+
const columns = Object.values(this.schema.columns).map(col => col.build().name);
|
|
1593
|
+
const sql = columns.map(name => `"${name}"`).join(', ');
|
|
1594
|
+
return { sql, columns };
|
|
1595
|
+
}
|
|
1596
|
+
// Selector function - extract selected columns
|
|
1597
|
+
const mockRow = this._createMockRow();
|
|
1598
|
+
const selectedMock = this.selector(mockRow);
|
|
1599
|
+
const selection = returning(selectedMock);
|
|
1600
|
+
if (typeof selection === 'object' && selection !== null) {
|
|
1601
|
+
const columns = [];
|
|
1602
|
+
const sqlParts = [];
|
|
1603
|
+
for (const [alias, field] of Object.entries(selection)) {
|
|
1604
|
+
if (field && typeof field === 'object' && '__dbColumnName' in field) {
|
|
1605
|
+
const dbName = field.__dbColumnName;
|
|
1606
|
+
columns.push(alias);
|
|
1607
|
+
sqlParts.push(`"${dbName}" AS "${alias}"`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return { sql: sqlParts.join(', '), columns };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Map row results for delete/update RETURNING clause
|
|
1616
|
+
* @internal
|
|
1617
|
+
*/
|
|
1618
|
+
mapDeleteReturningResults(rows, returning) {
|
|
1619
|
+
if (returning === true) {
|
|
1620
|
+
// Full entity mapping - apply fromDriver mappers
|
|
1621
|
+
return rows.map(row => {
|
|
1622
|
+
const mapped = {};
|
|
1623
|
+
for (const [propName, colBuilder] of Object.entries(this.schema.columns)) {
|
|
1624
|
+
const config = colBuilder.build();
|
|
1625
|
+
const dbValue = row[config.name];
|
|
1626
|
+
mapped[propName] = config.mapper ? config.mapper.fromDriver(dbValue) : dbValue;
|
|
1627
|
+
}
|
|
1628
|
+
return mapped;
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
// For selector functions, rows are already in the correct shape
|
|
1632
|
+
// Just apply any type mappers needed
|
|
1633
|
+
return rows.map(row => {
|
|
1634
|
+
const mapped = {};
|
|
1635
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1636
|
+
// Try to find column by alias or name
|
|
1637
|
+
const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
|
|
1638
|
+
const config = col.build();
|
|
1639
|
+
return propName === key || config.name === key;
|
|
1640
|
+
});
|
|
1641
|
+
if (colEntry) {
|
|
1642
|
+
const [, col] = colEntry;
|
|
1643
|
+
const config = col.build();
|
|
1644
|
+
// Apply fromDriver mapper if present
|
|
1645
|
+
mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
|
|
1646
|
+
}
|
|
1647
|
+
else {
|
|
1648
|
+
mapped[key] = value;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return mapped;
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1444
1654
|
/**
|
|
1445
1655
|
* Create mock row for analysis
|
|
1656
|
+
* @internal
|
|
1446
1657
|
*/
|
|
1447
|
-
|
|
1658
|
+
_createMockRow() {
|
|
1448
1659
|
const mock = {};
|
|
1449
1660
|
const tableAlias = this.schema.name;
|
|
1450
1661
|
// Performance: Use pre-computed column name map if available
|
|
@@ -2232,9 +2443,18 @@ class SelectQueryBuilder {
|
|
|
2232
2443
|
}
|
|
2233
2444
|
else {
|
|
2234
2445
|
const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
|
|
2446
|
+
const isSingleResult = value instanceof CollectionQueryBuilder && value.isSingleResult();
|
|
2235
2447
|
if (isArrayAgg) {
|
|
2236
2448
|
fieldConfigs.push({ key, type: 2 /* FieldType.COLLECTION_ARRAY */, value });
|
|
2237
2449
|
}
|
|
2450
|
+
else if (isSingleResult) {
|
|
2451
|
+
fieldConfigs.push({
|
|
2452
|
+
key,
|
|
2453
|
+
type: 9 /* FieldType.COLLECTION_SINGLE */,
|
|
2454
|
+
value,
|
|
2455
|
+
collectionBuilder: value
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2238
2458
|
else {
|
|
2239
2459
|
fieldConfigs.push({
|
|
2240
2460
|
key,
|
|
@@ -2345,6 +2565,18 @@ class SelectQueryBuilder {
|
|
|
2345
2565
|
}
|
|
2346
2566
|
break;
|
|
2347
2567
|
}
|
|
2568
|
+
case 9 /* FieldType.COLLECTION_SINGLE */: {
|
|
2569
|
+
// firstOrDefault() - return first item or null
|
|
2570
|
+
const items = rawValue || [];
|
|
2571
|
+
if (config.collectionBuilder && items.length > 0) {
|
|
2572
|
+
const transformedItems = this.transformCollectionItems(items, config.collectionBuilder);
|
|
2573
|
+
result[key] = transformedItems[0] ?? null;
|
|
2574
|
+
}
|
|
2575
|
+
else {
|
|
2576
|
+
result[key] = items[0] ?? null;
|
|
2577
|
+
}
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2348
2580
|
case 4 /* FieldType.CTE_AGGREGATION */: {
|
|
2349
2581
|
const items = rawValue || [];
|
|
2350
2582
|
if (config.innerMetadata && !disableMappers) {
|
|
@@ -2647,7 +2879,7 @@ class SelectQueryBuilder {
|
|
|
2647
2879
|
executor: this.executor,
|
|
2648
2880
|
};
|
|
2649
2881
|
// Analyze the selector to extract nested queries
|
|
2650
|
-
const mockRow = this.
|
|
2882
|
+
const mockRow = this._createMockRow();
|
|
2651
2883
|
const selectionResult = this.selector(mockRow);
|
|
2652
2884
|
// Build the query
|
|
2653
2885
|
const { sql } = this.buildQuery(selectionResult, context);
|
|
@@ -2658,7 +2890,7 @@ class SelectQueryBuilder {
|
|
|
2658
2890
|
// For table subqueries, preserve the selection metadata (includes SqlFragments with mappers)
|
|
2659
2891
|
let selectionMetadata;
|
|
2660
2892
|
if (mode === 'table') {
|
|
2661
|
-
const mockRow = this.
|
|
2893
|
+
const mockRow = this._createMockRow();
|
|
2662
2894
|
selectionMetadata = this.selector(mockRow);
|
|
2663
2895
|
}
|
|
2664
2896
|
// Extract outer field refs from the WHERE condition
|
|
@@ -3078,6 +3310,18 @@ class CollectionQueryBuilder {
|
|
|
3078
3310
|
// At runtime, this is still a CollectionQueryBuilder, but TypeScript sees it as CollectionResult
|
|
3079
3311
|
return this;
|
|
3080
3312
|
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Get first item from collection or null if empty
|
|
3315
|
+
* Automatically applies LIMIT 1 and returns a single item instead of array
|
|
3316
|
+
*/
|
|
3317
|
+
firstOrDefault(name) {
|
|
3318
|
+
if (name) {
|
|
3319
|
+
this.asName = name;
|
|
3320
|
+
}
|
|
3321
|
+
this.limitValue = 1;
|
|
3322
|
+
this.isMarkedAsList = false; // Single item, not a list
|
|
3323
|
+
return this;
|
|
3324
|
+
}
|
|
3081
3325
|
/**
|
|
3082
3326
|
* Get target table schema
|
|
3083
3327
|
*/
|
|
@@ -3096,6 +3340,12 @@ class CollectionQueryBuilder {
|
|
|
3096
3340
|
isScalarAggregation() {
|
|
3097
3341
|
return this.aggregationType !== undefined;
|
|
3098
3342
|
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Check if this is a single item result (firstOrDefault)
|
|
3345
|
+
*/
|
|
3346
|
+
isSingleResult() {
|
|
3347
|
+
return !this.isMarkedAsList && this.limitValue === 1;
|
|
3348
|
+
}
|
|
3099
3349
|
/**
|
|
3100
3350
|
* Get the aggregation type
|
|
3101
3351
|
*/
|