metal-orm 1.0.12 → 1.0.13

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.
@@ -1,4 +1,4 @@
1
- import { K as TableHooks, Q as ColumnType, C as ColumnDef, T as TableDef, a0 as CascadeMode, z as SelectQueryBuilder } from '../select-BKlr2ivY.cjs';
1
+ import { K as TableHooks, Q as ColumnType, C as ColumnDef, T as TableDef, a0 as CascadeMode, z as SelectQueryBuilder } from '../select-CCp1oz9p.cjs';
2
2
 
3
3
  interface EntityOptions {
4
4
  tableName?: string;
@@ -1,4 +1,4 @@
1
- import { K as TableHooks, Q as ColumnType, C as ColumnDef, T as TableDef, a0 as CascadeMode, z as SelectQueryBuilder } from '../select-BKlr2ivY.js';
1
+ import { K as TableHooks, Q as ColumnType, C as ColumnDef, T as TableDef, a0 as CascadeMode, z as SelectQueryBuilder } from '../select-CCp1oz9p.js';
2
2
 
3
3
  interface EntityOptions {
4
4
  tableName?: string;
@@ -467,6 +467,44 @@ var SelectQueryState = class _SelectQueryState {
467
467
  ctes: [...this.ast.ctes ?? [], cte]
468
468
  });
469
469
  }
470
+ /**
471
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
472
+ * @param op - Set operation node to add
473
+ * @returns New SelectQueryState with set operation
474
+ */
475
+ withSetOperation(op) {
476
+ return this.clone({
477
+ ...this.ast,
478
+ setOps: [...this.ast.setOps ?? [], op]
479
+ });
480
+ }
481
+ };
482
+
483
+ // src/core/ast/join-node.ts
484
+ var createJoinNode = (kind, tableName, condition, relationName) => ({
485
+ type: "Join",
486
+ kind,
487
+ table: { type: "Table", name: tableName },
488
+ condition,
489
+ relationName
490
+ });
491
+
492
+ // src/core/sql/sql.ts
493
+ var JOIN_KINDS = {
494
+ /** INNER JOIN type */
495
+ INNER: "INNER",
496
+ /** LEFT JOIN type */
497
+ LEFT: "LEFT",
498
+ /** RIGHT JOIN type */
499
+ RIGHT: "RIGHT",
500
+ /** CROSS JOIN type */
501
+ CROSS: "CROSS"
502
+ };
503
+ var ORDER_DIRECTIONS = {
504
+ /** Ascending order */
505
+ ASC: "ASC",
506
+ /** Descending order */
507
+ DESC: "DESC"
470
508
  };
471
509
 
472
510
  // src/query-builder/hydration-manager.ts
@@ -518,8 +556,26 @@ var HydrationManager = class _HydrationManager {
518
556
  * @returns AST with hydration metadata
519
557
  */
520
558
  applyToAst(ast) {
559
+ if (ast.setOps && ast.setOps.length > 0) {
560
+ return ast;
561
+ }
521
562
  const plan = this.planner.getPlan();
522
563
  if (!plan) return ast;
564
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
565
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
566
+ return this.attachHydrationMeta(rewritten, plan);
567
+ }
568
+ /**
569
+ * Gets the current hydration plan
570
+ * @returns Hydration plan or undefined if none exists
571
+ */
572
+ getPlan() {
573
+ return this.planner.getPlan();
574
+ }
575
+ /**
576
+ * Attaches hydration metadata to a query AST node.
577
+ */
578
+ attachHydrationMeta(ast, plan) {
523
579
  return {
524
580
  ...ast,
525
581
  meta: {
@@ -529,11 +585,154 @@ var HydrationManager = class _HydrationManager {
529
585
  };
530
586
  }
531
587
  /**
532
- * Gets the current hydration plan
533
- * @returns Hydration plan or undefined if none exists
588
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
589
+ * applied to parent rows when eager-loading multiplicative relations.
534
590
  */
535
- getPlan() {
536
- return this.planner.getPlan();
591
+ requiresParentPagination(ast, plan) {
592
+ const hasPagination = ast.limit !== void 0 || ast.offset !== void 0;
593
+ return hasPagination && this.hasMultiplyingRelations(plan);
594
+ }
595
+ hasMultiplyingRelations(plan) {
596
+ return plan.relations.some(
597
+ (rel) => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
598
+ );
599
+ }
600
+ /**
601
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
602
+ * instead of the joined result set.
603
+ *
604
+ * The strategy:
605
+ * - Hoist the original query (minus limit/offset) into a base CTE.
606
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
607
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
608
+ */
609
+ wrapForParentPagination(ast, plan) {
610
+ const projectionNames = this.getProjectionNames(ast.columns);
611
+ if (!projectionNames) {
612
+ return ast;
613
+ }
614
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
615
+ const projectionSet = new Set(projectionNames);
616
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
617
+ const baseCteName = this.nextCteName(ast.ctes, "__metal_pagination_base");
618
+ const baseQuery = {
619
+ ...ast,
620
+ ctes: void 0,
621
+ limit: void 0,
622
+ offset: void 0,
623
+ orderBy: void 0,
624
+ meta: void 0
625
+ };
626
+ const baseCte = {
627
+ type: "CommonTableExpression",
628
+ name: baseCteName,
629
+ query: baseQuery,
630
+ recursive: false
631
+ };
632
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
633
+ if (orderBy === null) {
634
+ return ast;
635
+ }
636
+ const pageCteName = this.nextCteName([...ast.ctes ?? [], baseCte], "__metal_pagination_page");
637
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
638
+ const pageCte = {
639
+ type: "CommonTableExpression",
640
+ name: pageCteName,
641
+ query: {
642
+ type: "SelectQuery",
643
+ from: { type: "Table", name: baseCteName },
644
+ columns: pagingColumns,
645
+ joins: [],
646
+ distinct: [{ type: "Column", table: baseCteName, name: rootPkAlias }],
647
+ orderBy,
648
+ limit: ast.limit,
649
+ offset: ast.offset
650
+ },
651
+ recursive: false
652
+ };
653
+ const joinCondition = eq(
654
+ { type: "Column", table: baseCteName, name: rootPkAlias },
655
+ { type: "Column", table: pageCteName, name: rootPkAlias }
656
+ );
657
+ const outerColumns = projectionNames.map((name) => ({
658
+ type: "Column",
659
+ table: baseCteName,
660
+ name,
661
+ alias: name
662
+ }));
663
+ return {
664
+ type: "SelectQuery",
665
+ from: { type: "Table", name: baseCteName },
666
+ columns: outerColumns,
667
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
668
+ orderBy,
669
+ ctes: [...ast.ctes ?? [], baseCte, pageCte]
670
+ };
671
+ }
672
+ nextCteName(existing, baseName) {
673
+ const names = new Set((existing ?? []).map((cte) => cte.name));
674
+ let candidate = baseName;
675
+ let suffix = 1;
676
+ while (names.has(candidate)) {
677
+ suffix += 1;
678
+ candidate = `${baseName}_${suffix}`;
679
+ }
680
+ return candidate;
681
+ }
682
+ getProjectionNames(columns) {
683
+ const names = [];
684
+ for (const col of columns) {
685
+ const alias = col.alias ?? col.name;
686
+ if (!alias) return void 0;
687
+ names.push(alias);
688
+ }
689
+ return names;
690
+ }
691
+ buildProjectionAliasMap(columns) {
692
+ const map = /* @__PURE__ */ new Map();
693
+ for (const col of columns) {
694
+ if (col.type !== "Column") continue;
695
+ const node = col;
696
+ const key = `${node.table}.${node.name}`;
697
+ map.set(key, node.alias ?? node.name);
698
+ }
699
+ return map;
700
+ }
701
+ mapOrderBy(orderBy, plan, projectionAliases, baseAlias, availableColumns) {
702
+ if (!orderBy || orderBy.length === 0) {
703
+ return void 0;
704
+ }
705
+ const mapped = [];
706
+ for (const ob of orderBy) {
707
+ if (ob.column.table !== plan.rootTable) {
708
+ return null;
709
+ }
710
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
711
+ if (!availableColumns.has(alias)) {
712
+ return null;
713
+ }
714
+ mapped.push({
715
+ type: "OrderBy",
716
+ column: { type: "Column", table: baseAlias, name: alias },
717
+ direction: ob.direction
718
+ });
719
+ }
720
+ return mapped;
721
+ }
722
+ buildPagingColumns(primaryKey, orderBy, tableAlias) {
723
+ const columns = [{ type: "Column", table: tableAlias, name: primaryKey, alias: primaryKey }];
724
+ if (!orderBy) return columns;
725
+ for (const ob of orderBy) {
726
+ if (!columns.some((col) => col.name === ob.column.name)) {
727
+ columns.push({
728
+ type: "Column",
729
+ table: tableAlias,
730
+ name: ob.column.name,
731
+ alias: ob.column.name
732
+ });
733
+ }
734
+ }
735
+ return columns;
537
736
  }
538
737
  };
539
738
 
@@ -787,6 +986,20 @@ var QueryAstService = class {
787
986
  };
788
987
  return this.state.withCte(cte);
789
988
  }
989
+ /**
990
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
991
+ * @param operator - Set operator
992
+ * @param query - Right-hand side query
993
+ * @returns Updated query state with set operation
994
+ */
995
+ withSetOperation(operator, query) {
996
+ const op = {
997
+ type: "SetOperation",
998
+ operator,
999
+ query
1000
+ };
1001
+ return this.state.withSetOperation(op);
1002
+ }
790
1003
  /**
791
1004
  * Selects a subquery as a column
792
1005
  * @param alias - Alias for the subquery
@@ -939,15 +1152,6 @@ var RelationProjectionHelper = class {
939
1152
  }
940
1153
  };
941
1154
 
942
- // src/core/ast/join-node.ts
943
- var createJoinNode = (kind, tableName, condition, relationName) => ({
944
- type: "Join",
945
- kind,
946
- table: { type: "Table", name: tableName },
947
- condition,
948
- relationName
949
- });
950
-
951
1155
  // src/query-builder/relation-conditions.ts
952
1156
  var assertNever = (value) => {
953
1157
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
@@ -1003,24 +1207,6 @@ var buildRelationCorrelation = (root, relation) => {
1003
1207
  return baseRelationCondition(root, relation);
1004
1208
  };
1005
1209
 
1006
- // src/core/sql/sql.ts
1007
- var JOIN_KINDS = {
1008
- /** INNER JOIN type */
1009
- INNER: "INNER",
1010
- /** LEFT JOIN type */
1011
- LEFT: "LEFT",
1012
- /** RIGHT JOIN type */
1013
- RIGHT: "RIGHT",
1014
- /** CROSS JOIN type */
1015
- CROSS: "CROSS"
1016
- };
1017
- var ORDER_DIRECTIONS = {
1018
- /** Ascending order */
1019
- ASC: "ASC",
1020
- /** Descending order */
1021
- DESC: "DESC"
1022
- };
1023
-
1024
1210
  // src/query-builder/relation-service.ts
1025
1211
  var RelationService = class {
1026
1212
  /**
@@ -1466,6 +1652,16 @@ var hasEntityMeta = (entity) => {
1466
1652
 
1467
1653
  // src/orm/relations/has-many.ts
1468
1654
  var toKey2 = (value) => value === null || value === void 0 ? "" : String(value);
1655
+ var hideInternal = (obj, keys) => {
1656
+ for (const key of keys) {
1657
+ Object.defineProperty(obj, key, {
1658
+ value: obj[key],
1659
+ writable: false,
1660
+ configurable: false,
1661
+ enumerable: false
1662
+ });
1663
+ }
1664
+ };
1469
1665
  var DefaultHasManyCollection = class {
1470
1666
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1471
1667
  this.ctx = ctx;
@@ -1481,6 +1677,7 @@ var DefaultHasManyCollection = class {
1481
1677
  this.items = [];
1482
1678
  this.added = /* @__PURE__ */ new Set();
1483
1679
  this.removed = /* @__PURE__ */ new Set();
1680
+ hideInternal(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1484
1681
  this.hydrateFromCache();
1485
1682
  }
1486
1683
  async load() {
@@ -1556,10 +1753,23 @@ var DefaultHasManyCollection = class {
1556
1753
  this.items = rows.map((row) => this.createEntity(row));
1557
1754
  this.loaded = true;
1558
1755
  }
1756
+ toJSON() {
1757
+ return this.items;
1758
+ }
1559
1759
  };
1560
1760
 
1561
1761
  // src/orm/relations/belongs-to.ts
1562
1762
  var toKey3 = (value) => value === null || value === void 0 ? "" : String(value);
1763
+ var hideInternal2 = (obj, keys) => {
1764
+ for (const key of keys) {
1765
+ Object.defineProperty(obj, key, {
1766
+ value: obj[key],
1767
+ writable: false,
1768
+ configurable: false,
1769
+ enumerable: false
1770
+ });
1771
+ }
1772
+ };
1563
1773
  var DefaultBelongsToReference = class {
1564
1774
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, targetKey) {
1565
1775
  this.ctx = ctx;
@@ -1573,6 +1783,7 @@ var DefaultBelongsToReference = class {
1573
1783
  this.targetKey = targetKey;
1574
1784
  this.loaded = false;
1575
1785
  this.current = null;
1786
+ hideInternal2(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "targetKey"]);
1576
1787
  this.populateFromHydrationCache();
1577
1788
  }
1578
1789
  async load() {
@@ -1633,10 +1844,23 @@ var DefaultBelongsToReference = class {
1633
1844
  this.current = this.createEntity(row);
1634
1845
  this.loaded = true;
1635
1846
  }
1847
+ toJSON() {
1848
+ return this.current;
1849
+ }
1636
1850
  };
1637
1851
 
1638
1852
  // src/orm/relations/many-to-many.ts
1639
1853
  var toKey4 = (value) => value === null || value === void 0 ? "" : String(value);
1854
+ var hideInternal3 = (obj, keys) => {
1855
+ for (const key of keys) {
1856
+ Object.defineProperty(obj, key, {
1857
+ value: obj[key],
1858
+ writable: false,
1859
+ configurable: false,
1860
+ enumerable: false
1861
+ });
1862
+ }
1863
+ };
1640
1864
  var DefaultManyToManyCollection = class {
1641
1865
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1642
1866
  this.ctx = ctx;
@@ -1650,6 +1874,7 @@ var DefaultManyToManyCollection = class {
1650
1874
  this.localKey = localKey;
1651
1875
  this.loaded = false;
1652
1876
  this.items = [];
1877
+ hideInternal3(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1653
1878
  this.hydrateFromCache();
1654
1879
  }
1655
1880
  async load() {
@@ -1755,6 +1980,9 @@ var DefaultManyToManyCollection = class {
1755
1980
  });
1756
1981
  this.loaded = true;
1757
1982
  }
1983
+ toJSON() {
1984
+ return this.items;
1985
+ }
1758
1986
  };
1759
1987
 
1760
1988
  // src/orm/lazy-batch.ts
@@ -2115,9 +2343,15 @@ var flattenResults = (results) => {
2115
2343
  return rows;
2116
2344
  };
2117
2345
  async function executeHydrated(ctx, qb) {
2118
- const compiled = ctx.dialect.compileSelect(qb.getAST());
2346
+ const ast = qb.getAST();
2347
+ const compiled = ctx.dialect.compileSelect(ast);
2119
2348
  const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
2120
2349
  const rows = flattenResults(executed);
2350
+ if (ast.setOps && ast.setOps.length > 0) {
2351
+ return rows.map(
2352
+ (row) => createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
2353
+ );
2354
+ }
2121
2355
  const hydrated = hydrateRows(rows, qb.getHydrationPlan());
2122
2356
  return hydrated.map(
2123
2357
  (row) => createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
@@ -2164,6 +2398,10 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2164
2398
  const joinNode = createJoinNode(kind, table.name, condition);
2165
2399
  return this.applyAst(context, (service) => service.withJoin(joinNode));
2166
2400
  }
2401
+ applySetOperation(operator, query) {
2402
+ const subAst = this.resolveQueryNode(query);
2403
+ return this.applyAst(this.context, (service) => service.withSetOperation(operator, subAst));
2404
+ }
2167
2405
  /**
2168
2406
  * Selects specific columns for the query
2169
2407
  * @param columns - Record of column definitions, function nodes, case expressions, or window functions
@@ -2352,6 +2590,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2352
2590
  const nextContext = this.applyAst(this.context, (service) => service.withOffset(n));
2353
2591
  return this.clone(nextContext);
2354
2592
  }
2593
+ /**
2594
+ * Combines this query with another using UNION
2595
+ * @param query - Query to union with
2596
+ * @returns New query builder instance with the set operation
2597
+ */
2598
+ union(query) {
2599
+ return this.clone(this.applySetOperation("UNION", query));
2600
+ }
2601
+ /**
2602
+ * Combines this query with another using UNION ALL
2603
+ * @param query - Query to union with
2604
+ * @returns New query builder instance with the set operation
2605
+ */
2606
+ unionAll(query) {
2607
+ return this.clone(this.applySetOperation("UNION ALL", query));
2608
+ }
2609
+ /**
2610
+ * Combines this query with another using INTERSECT
2611
+ * @param query - Query to intersect with
2612
+ * @returns New query builder instance with the set operation
2613
+ */
2614
+ intersect(query) {
2615
+ return this.clone(this.applySetOperation("INTERSECT", query));
2616
+ }
2617
+ /**
2618
+ * Combines this query with another using EXCEPT
2619
+ * @param query - Query to subtract
2620
+ * @returns New query builder instance with the set operation
2621
+ */
2622
+ except(query) {
2623
+ return this.clone(this.applySetOperation("EXCEPT", query));
2624
+ }
2355
2625
  /**
2356
2626
  * Adds a WHERE EXISTS condition to the query
2357
2627
  * @param subquery - Subquery to check for existence