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.
@@ -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.createMockRow();
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
- createMockRow() {
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow();
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.createMockRow());
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.createMockRow();
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
- createMockRow() {
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.createMockRow();
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.createMockRow();
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
  */