@rebasepro/server-postgres 0.10.1-canary.3d09338 → 0.10.1-canary.60301c3

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.
Files changed (39) hide show
  1. package/dist/auth/schema-version.d.ts +100 -0
  2. package/dist/collections/validate-relations.d.ts +53 -0
  3. package/dist/data-transformer.d.ts +3 -3
  4. package/dist/{ensure-collection-tables-BVvtkkRm.js → ensure-collection-tables-DGMYK0fr.js} +5 -5
  5. package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
  6. package/dist/index.es.js +507 -309
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/services/RelationService.d.ts +9 -8
  9. package/dist/services/nested-path.d.ts +3 -3
  10. package/dist/services/row-pipeline.d.ts +2 -2
  11. package/dist/{src-lcfUP4xg.js → src-3VmUJ8Xn.js} +135 -247
  12. package/dist/src-3VmUJ8Xn.js.map +1 -0
  13. package/dist/{src-CBgtrPhJ.js → src-D5xBTl32.js} +12 -2
  14. package/dist/src-D5xBTl32.js.map +1 -0
  15. package/dist/utils/drizzle-conditions.d.ts +35 -23
  16. package/package.json +6 -6
  17. package/src/PostgresBootstrapper.ts +5 -1
  18. package/src/auth/ensure-tables.ts +23 -0
  19. package/src/auth/schema-version.ts +254 -0
  20. package/src/cli-errors.ts +1 -1
  21. package/src/cli-helpers.ts +4 -3
  22. package/src/collections/PostgresCollectionRegistry.ts +9 -4
  23. package/src/collections/buildRegistry.ts +7 -0
  24. package/src/collections/validate-relations.ts +280 -0
  25. package/src/data-transformer.ts +28 -38
  26. package/src/schema/doctor.ts +8 -8
  27. package/src/schema/generate-drizzle-schema-logic.ts +49 -101
  28. package/src/schema/generate-postgres-ddl-logic.ts +9 -9
  29. package/src/schema/introspect-db-logic.ts +19 -23
  30. package/src/services/FetchService.ts +12 -12
  31. package/src/services/PersistService.ts +23 -27
  32. package/src/services/RelationService.ts +65 -83
  33. package/src/services/cdc/junction-tables.ts +4 -4
  34. package/src/services/nested-path.ts +24 -9
  35. package/src/services/row-pipeline.ts +5 -6
  36. package/src/utils/drizzle-conditions.ts +156 -372
  37. package/dist/ensure-collection-tables-BVvtkkRm.js.map +0 -1
  38. package/dist/src-CBgtrPhJ.js.map +0 -1
  39. package/dist/src-lcfUP4xg.js.map +0 -1
@@ -1,6 +1,9 @@
1
1
  import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
2
2
  import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
3
- import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
3
+ import {
4
+ FilterValues, WhereFilterOp, JoinStep, LogicalCondition, FilterCondition,
5
+ ResolvedRelation, ResolvedBelongsTo, ResolvedHasOne, ResolvedHasMany
6
+ } from "@rebasepro/types";
4
7
  import { getColumnName, normalizeToEntityRelation, resolveCollectionRelations } from "@rebasepro/common";
5
8
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
9
  import { ConditionBuilderStatic } from "../interfaces";
@@ -59,12 +62,12 @@ export class DrizzleConditionBuilder {
59
62
  * it and the final step correlating to the outer row.
60
63
  */
61
64
  static buildRelationScopeCondition(
62
- relation: Relation,
65
+ relation: ResolvedRelation,
63
66
  /**
64
- * Lazy: only `joinPath` and `localKey` relations need the parent's own
65
- * table. An inverse foreign key and a junction are both expressible
66
- * from the parent's *id* alone, and requiring the table for them would
67
- * make a child listing fail on a parent whose table isn't registered.
67
+ * Lazy: only `via` and `belongsTo` need the parent's own table. A
68
+ * foreign key on the target and a junction are both expressible from
69
+ * the parent's *id* alone, and requiring the table for them would make
70
+ * a child listing fail on a parent whose table isn't registered.
68
71
  */
69
72
  parent: () => { table: PgTable<any>; idColumn: AnyPgColumn },
70
73
  parentId: string | number,
@@ -72,50 +75,58 @@ export class DrizzleConditionBuilder {
72
75
  targetIdColumn: AnyPgColumn,
73
76
  registry: PostgresCollectionRegistry
74
77
  ): SQL {
75
- if (relation.joinPath && relation.joinPath.length > 0) {
76
- const { table, idColumn } = parent();
77
- return this.buildJoinPathScopeCondition(
78
- relation.joinPath, table, idColumn, parentId, targetTable, registry
79
- );
80
- }
78
+ switch (relation.kind) {
79
+ case "via": {
80
+ const { table, idColumn } = parent();
81
+ return this.buildJoinPathScopeCondition(
82
+ relation.joinPath, table, idColumn, parentId, targetTable, registry
83
+ );
84
+ }
81
85
 
82
- if (relation.through) {
83
- const junctionTable = registry.getTable(relation.through.table);
84
- if (!junctionTable) {
85
- throw new Error(`Junction table not found: ${relation.through.table}`);
86
+ case "manyToMany": {
87
+ const { table: junctionName, sourceColumn, targetColumn } = relation.through;
88
+ const junctionTable = registry.getTable(junctionName);
89
+ if (!junctionTable) {
90
+ throw new Error(`Junction table not found: ${junctionName}`);
91
+ }
92
+ const sourceCol = junctionTable[sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
93
+ const targetCol = junctionTable[targetColumn as keyof typeof junctionTable] as AnyPgColumn;
94
+ if (!sourceCol || !targetCol) {
95
+ throw new Error(
96
+ `Junction columns '${sourceColumn}'/'${targetColumn}' not found in '${junctionName}'`
97
+ );
98
+ }
99
+ // Correlated, not joined: a join through a junction multiplies
100
+ // the target rows by the number of matching links and silently
101
+ // breaks `limit`/`offset`.
102
+ return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
86
103
  }
87
- const sourceCol = junctionTable[relation.through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
88
- const targetCol = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
89
- if (!sourceCol || !targetCol) {
90
- throw new Error(
91
- `Junction columns '${relation.through.sourceColumn}'/'${relation.through.targetColumn}' not found in '${relation.through.table}'`
92
- );
104
+
105
+ case "hasOne":
106
+ case "hasMany": {
107
+ const fkColumn = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
108
+ if (!fkColumn) {
109
+ throw new Error(
110
+ `Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of ` +
111
+ `relation '${relation.relationName}'.`
112
+ );
113
+ }
114
+ return eq(fkColumn, parentId);
93
115
  }
94
- return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
95
- }
96
116
 
97
- if (relation.foreignKeyOnTarget) {
98
- const fkColumn = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
99
- if (!fkColumn) {
100
- throw new Error(
101
- `Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation ` +
102
- `'${relation.relationName}'. A many-to-many relation needs \`through\` instead.`
103
- );
117
+ case "belongsTo": {
118
+ // The single target row the parent's foreign key points at.
119
+ const { table, idColumn } = parent();
120
+ return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
104
121
  }
105
- return eq(fkColumn, parentId);
106
- }
107
122
 
108
- if (relation.localKey) {
109
- // A to-one owning relation read as a scope: the single target row is
110
- // the one the parent's foreign key points at.
111
- const { table, idColumn } = parent();
112
- return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
123
+ default: {
124
+ // Exhaustive. There is no "declares nothing" case to fall
125
+ // through to any more — every kind names its own link.
126
+ const exhaustive: never = relation;
127
+ throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
128
+ }
113
129
  }
114
-
115
- throw new Error(
116
- `Relation '${relation.relationName}' declares no \`foreignKeyOnTarget\`, \`through\`, \`joinPath\` or ` +
117
- "`localKey`, so there is no way to tell which target rows belong to a parent."
118
- );
119
130
  }
120
131
 
121
132
  /**
@@ -350,8 +361,22 @@ export class DrizzleConditionBuilder {
350
361
  /**
351
362
  * Build relation-based conditions for different relation types
352
363
  */
364
+ /**
365
+ * Joins and where-conditions that reach a relation's target rows.
366
+ *
367
+ * One case per kind. This used to be a chain of six `else if`s over
368
+ * `cardinality`/`direction`/`through`, ending in
369
+ * `findCorrespondingJunctionTable` — a search through the *target's* own
370
+ * relations to work out whether an "inverse many" was a one-to-many or the
371
+ * far side of a junction. That search is gone: the kind says which it is.
372
+ *
373
+ * The owning/inverse split for junctions is gone too. Both variants built
374
+ * the identical condition — `through` is always written from the declaring
375
+ * side's point of view — so the second was a distinction without a
376
+ * difference and one of the places the two could drift apart.
377
+ */
353
378
  static buildRelationConditions(
354
- relation: Relation,
379
+ relation: ResolvedRelation,
355
380
  parentId: string | number | (string | number)[],
356
381
  targetTable: PgTable<any>,
357
382
  parentTable: PgTable<any>,
@@ -362,110 +387,44 @@ export class DrizzleConditionBuilder {
362
387
  joinConditions: { table: PgTable<any>; condition: SQL }[];
363
388
  whereConditions: SQL[];
364
389
  } {
365
- console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
366
- relationName: relation.relationName,
367
- cardinality: relation.cardinality,
368
- direction: relation.direction,
369
- hasThrough: !!relation.through,
370
- hasForeignKeyOnTarget: !!relation.foreignKeyOnTarget,
371
- inverseRelationName: relation.inverseRelationName,
372
- parentId: parentId
373
- });
374
-
375
390
  const joinConditions: { table: PgTable<any>; condition: SQL }[] = [];
376
391
  const whereConditions: SQL[] = [];
377
392
 
378
- if (relation.joinPath && relation.joinPath.length > 0) {
379
- console.debug("🔍 [buildRelationConditions] Using joinPath logic");
380
- // Handle join path relations
381
- const {
382
- joins,
383
- finalCondition
384
- } = this.buildJoinPathConditions(
385
- relation.joinPath,
386
- targetTable,
387
- parentTable,
388
- parentIdColumn,
389
- parentId,
390
- registry
391
- );
392
- joinConditions.push(...joins);
393
- whereConditions.push(finalCondition);
394
-
395
- } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
396
- console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
397
- // Handle many-to-many relations with junction table
398
- const junctionResult = this.buildJunctionTableConditions(
399
- relation.through,
400
- targetIdColumn,
401
- parentId,
402
- registry
403
- );
404
- joinConditions.push(junctionResult.join);
405
- whereConditions.push(junctionResult.condition);
406
-
407
- } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
408
- console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
409
- // Handle inverse many-to-many relations with junction table
410
- const junctionResult = this.buildInverseJunctionTableConditions(
411
- relation.through,
412
- targetIdColumn,
413
- parentId,
414
- registry
415
- );
416
- joinConditions.push(junctionResult.join);
417
- whereConditions.push(junctionResult.condition);
418
-
419
- } else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
420
- console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
421
-
422
- // First, try to find a junction table (for many-to-many relationships)
423
- const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
424
- if (junctionInfo) {
425
- console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
426
- const junctionResult = this.buildInverseJunctionTableConditions(
427
- junctionInfo,
428
- targetIdColumn,
429
- parentId,
430
- registry
393
+ switch (relation.kind) {
394
+ case "via": {
395
+ const { joins, finalCondition } = this.buildJoinPathConditions(
396
+ relation.joinPath, targetTable, parentTable, parentIdColumn, parentId, registry
397
+ );
398
+ joinConditions.push(...joins);
399
+ whereConditions.push(finalCondition);
400
+ break;
401
+ }
402
+
403
+ case "manyToMany": {
404
+ const junctionResult = this.buildJunctionTableConditions(
405
+ relation.through, targetIdColumn, parentId, registry
431
406
  );
432
407
  joinConditions.push(junctionResult.join);
433
408
  whereConditions.push(junctionResult.condition);
434
- } else if (relation.foreignKeyOnTarget) {
435
- console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
436
- // This is a true inverse one-to-many relationship
437
- const simpleCondition = this.buildSimpleRelationCondition(
438
- relation,
439
- targetTable,
440
- parentTable,
441
- parentId
409
+ break;
410
+ }
411
+
412
+ case "hasOne":
413
+ case "hasMany":
414
+ case "belongsTo":
415
+ whereConditions.push(
416
+ this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId)
442
417
  );
443
- whereConditions.push(simpleCondition);
444
- } else {
445
- logger.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
446
- throw new Error(`Cannot resolve inverse many relation '${relation.relationName}'. Either specify 'through' property, ensure corresponding owning relation exists with junction table configuration, or specify 'foreignKeyOnTarget' for one-to-many relationships.`);
418
+ break;
419
+
420
+ default: {
421
+ const exhaustive: never = relation;
422
+ throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
447
423
  }
448
- } else {
449
- console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
450
- // Handle simple relations
451
- const simpleCondition = this.buildSimpleRelationCondition(
452
- relation,
453
- targetTable,
454
- parentTable,
455
- parentId
456
- );
457
- whereConditions.push(simpleCondition);
458
424
  }
459
425
 
460
- console.debug("🔍 [buildRelationConditions] Final result:", {
461
- joinConditionsCount: joinConditions.length,
462
- whereConditionsCount: whereConditions.length
463
- });
464
-
465
- return {
466
- joinConditions,
467
- whereConditions
468
- };
426
+ return { joinConditions,
427
+ whereConditions };
469
428
  }
470
429
 
471
430
  /**
@@ -765,109 +724,47 @@ export class DrizzleConditionBuilder {
765
724
  };
766
725
  }
767
726
 
768
- /**
769
- * Build conditions for inverse junction table (many-to-many) relations
770
- */
771
- private static buildInverseJunctionTableConditions(
772
- through: { table: string; sourceColumn: string; targetColumn: string },
773
- targetIdColumn: AnyPgColumn,
774
- parentId: string | number | (string | number)[],
775
- registry: PostgresCollectionRegistry
776
- ): { join: { table: PgTable<any>; condition: SQL }; condition: SQL } {
777
- const junctionTable = registry.getTable(through.table);
778
- if (!junctionTable) {
779
- throw new Error(`Junction table not found: ${through.table}`);
780
- }
781
-
782
- const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
783
- const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
784
-
785
- if (!junctionSourceCol) {
786
- throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
787
- }
788
- if (!junctionTargetCol) {
789
- throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
790
- }
791
-
792
- // For inverse relations, the parentId (tag ID) should match the sourceColumn (tag_id)
793
- // and we want to find target rows (posts) through the targetColumn (post_id)
794
- const condition = Array.isArray(parentId)
795
- ? inArray(junctionSourceCol, parentId)
796
- : eq(junctionSourceCol, parentId);
797
-
798
- return {
799
- join: {
800
- table: junctionTable,
801
- condition: eq(targetIdColumn, junctionTargetCol)
802
- },
803
- condition
804
- };
805
- }
806
727
 
807
728
  /**
808
- * Build conditions for simple relations (owning/inverse without join paths)
729
+ * The condition for a relation whose link is a single column.
730
+ *
731
+ * Two cases. It had five: two of them existed only to throw ("should not be
732
+ * called directly", "lacks proper configuration"), and one guessed a column
733
+ * name by appending `_id` to `inverseRelationName` when no foreign key had
734
+ * been resolved. All three were reachable only because the old type let a
735
+ * relation arrive here under-specified. It cannot now.
809
736
  */
810
737
  private static buildSimpleRelationCondition(
811
- relation: Relation,
738
+ relation: ResolvedBelongsTo | ResolvedHasOne | ResolvedHasMany,
812
739
  targetTable: PgTable<any>,
813
740
  parentTable: PgTable<any>,
814
741
  parentId: string | number | (string | number)[]
815
742
  ): SQL {
816
- if (relation.direction === "owning" && relation.localKey) {
817
- // For owning relations, the parentId is actually the foreign key value
818
- // that should match the target table's primary key
819
- const targetIdCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
743
+ const match = (column: AnyPgColumn) =>
744
+ Array.isArray(parentId) ? inArray(column, parentId) : eq(column, parentId);
745
+
746
+ if (relation.kind === "belongsTo") {
747
+ // `parentId` is the foreign key's value, matched against the
748
+ // target's own key.
749
+ const targetIdCol =
750
+ (Object.values(targetTable).find((col: Record<string, unknown>) => col.primary)
751
+ ?? Object.values(targetTable).find((col: Record<string, unknown>) => col.name === "id")) as AnyPgColumn | undefined;
820
752
  if (!targetIdCol) {
821
- // Fallback to looking for an "id" column by name
822
- const idCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.name === "id") as AnyPgColumn;
823
- if (!idCol) {
824
- throw new Error("No primary key or \"id\" column found in target table");
825
- }
826
- return Array.isArray(parentId)
827
- ? inArray(idCol, parentId)
828
- : eq(idCol, parentId);
829
- }
830
- return Array.isArray(parentId)
831
- ? inArray(targetIdCol, parentId)
832
- : eq(targetIdCol, parentId);
833
-
834
- } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
835
- // Inverse relation: use foreign key on target table
836
- const foreignKeyCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
837
- if (!foreignKeyCol) {
838
- // This could be a many-to-many relationship where foreignKeyOnTarget was set by sanitizeRelation
839
- // but the column doesn't actually exist. In this case, we should suggest using junction tables.
840
- throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table. This might be a many-to-many relationship that requires a junction table. Consider using 'through' property or ensure the corresponding owning relation exists with junction table configuration.`);
841
- }
842
- return Array.isArray(parentId)
843
- ? inArray(foreignKeyCol, parentId)
844
- : eq(foreignKeyCol, parentId);
845
-
846
- } else if (relation.direction === "inverse" && relation.cardinality === "many" && relation.inverseRelationName) {
847
- // For inverse many-to-many relations, this should not be called directly
848
- // The buildRelationConditions method should handle finding the junction table
849
- // If we reach here, it means the junction table lookup failed
850
- throw new Error(`Inverse many-to-many relation '${relation.relationName}' requires a junction table. Either specify 'through' property or ensure the corresponding owning relation exists with junction table configuration.`);
851
-
852
- } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
853
- // Auto-infer foreign key column for inverse one-to-one relations
854
- // Pattern: {inverseRelationName}_id (e.g., "author" -> "author_id")
855
- const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
856
- const foreignKeyCol = targetTable[inferredForeignKeyName as keyof typeof targetTable] as AnyPgColumn;
857
-
858
- if (!foreignKeyCol) {
859
- throw new Error(`Auto-inferred foreign key column '${inferredForeignKeyName}' not found in target table for inverse relation '${relation.relationName}'. Please specify 'foreignKeyOnTarget' explicitly.`);
753
+ throw new Error(
754
+ `No primary key or "id" column in the target table of relation '${relation.relationName}'.`
755
+ );
860
756
  }
757
+ return match(targetIdCol);
758
+ }
861
759
 
862
- console.debug(`🔍 [DrizzleConditionBuilder] Auto-inferred foreign key '${inferredForeignKeyName}' for inverse relation '${relation.relationName}'`);
863
-
864
- return Array.isArray(parentId)
865
- ? inArray(foreignKeyCol, parentId)
866
- : eq(foreignKeyCol, parentId);
867
-
868
- } else {
869
- throw new Error(`Relation '${relation.relationName}' lacks proper configuration. For many-to-many relations, use 'through' property. For simple relations, use 'localKey' or 'foreignKeyOnTarget'.`);
760
+ const foreignKeyCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
761
+ if (!foreignKeyCol) {
762
+ throw new Error(
763
+ `Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation ` +
764
+ `'${relation.relationName}'. A link through a junction is \`kind: "manyToMany"\`.`
765
+ );
870
766
  }
767
+ return match(foreignKeyCol);
871
768
  }
872
769
 
873
770
  /**
@@ -944,7 +841,7 @@ export class DrizzleConditionBuilder {
944
841
  */
945
842
  static buildRelationQuery<T extends DrizzleDynamicQuery>(
946
843
  baseQuery: T,
947
- relation: Relation,
844
+ relation: ResolvedRelation,
948
845
  parentId: string | number | (string | number)[],
949
846
  targetTable: PgTable<any>,
950
847
  parentTable: PgTable<any>,
@@ -985,11 +882,15 @@ export class DrizzleConditionBuilder {
985
882
  }
986
883
 
987
884
  /**
988
- * Build count query for relations with proper joins and conditions
885
+ * A count over a relation's target rows.
886
+ *
887
+ * The junction case counts `distinct` because the caller's query joins the
888
+ * junction; the owning/inverse pair that used to sit here built the same
889
+ * query twice.
989
890
  */
990
891
  static buildRelationCountQuery<T extends DrizzleDynamicQuery>(
991
892
  baseCountQuery: T,
992
- relation: Relation,
893
+ relation: ResolvedRelation,
993
894
  parentId: string | number,
994
895
  targetTable: PgTable<any>,
995
896
  parentTable: PgTable<any>,
@@ -998,51 +899,32 @@ export class DrizzleConditionBuilder {
998
899
  registry: PostgresCollectionRegistry,
999
900
  additionalFilters?: SQL[]
1000
901
  ): T {
1001
- // For count queries, we need to handle joins differently to avoid duplicates
1002
- if (relation.joinPath && relation.joinPath.length > 0) {
1003
- return this.buildJoinPathCountQuery(
1004
- baseCountQuery,
1005
- relation.joinPath,
1006
- targetTable,
1007
- parentTable,
1008
- parentIdColumn,
1009
- parentId,
1010
- registry,
1011
- additionalFilters
1012
- );
1013
- } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
1014
- return this.buildJunctionCountQuery(
1015
- baseCountQuery,
1016
- relation.through,
1017
- targetIdColumn,
1018
- parentId,
1019
- registry,
1020
- additionalFilters
1021
- );
1022
- } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
1023
- return this.buildInverseJunctionCountQuery(
1024
- baseCountQuery,
1025
- relation.through,
1026
- targetIdColumn,
1027
- parentId,
1028
- registry,
1029
- additionalFilters
1030
- );
1031
- } else {
1032
- // Simple relations
1033
- const simpleCondition = this.buildSimpleRelationCondition(
1034
- relation,
1035
- targetTable,
1036
- parentTable,
1037
- parentId
1038
- );
902
+ switch (relation.kind) {
903
+ case "via":
904
+ return this.buildJoinPathCountQuery(
905
+ baseCountQuery, relation.joinPath, targetTable, parentTable,
906
+ parentIdColumn, parentId, registry, additionalFilters
907
+ );
908
+
909
+ case "manyToMany":
910
+ return this.buildJunctionCountQuery(
911
+ baseCountQuery, relation.through, targetIdColumn, parentId, registry, additionalFilters
912
+ );
1039
913
 
1040
- const allConditions = [simpleCondition];
1041
- if (additionalFilters) {
1042
- allConditions.push(...additionalFilters);
914
+ case "belongsTo":
915
+ case "hasOne":
916
+ case "hasMany": {
917
+ const allConditions = [
918
+ this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId),
919
+ ...(additionalFilters ?? [])
920
+ ];
921
+ return baseCountQuery.where(and(...allConditions));
1043
922
  }
1044
923
 
1045
- return baseCountQuery.where(and(...allConditions));
924
+ default: {
925
+ const exhaustive: never = relation;
926
+ throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
927
+ }
1046
928
  }
1047
929
  }
1048
930
 
@@ -1138,41 +1020,6 @@ export class DrizzleConditionBuilder {
1138
1020
  .where(and(...baseConditions));
1139
1021
  }
1140
1022
 
1141
- /**
1142
- * Build inverse junction table conditions for count queries
1143
- */
1144
- private static buildInverseJunctionCountQuery<T extends DrizzleDynamicQuery>(
1145
- baseCountQuery: T,
1146
- through: { table: string; sourceColumn: string; targetColumn: string },
1147
- targetIdColumn: AnyPgColumn,
1148
- parentId: string | number,
1149
- registry: PostgresCollectionRegistry,
1150
- additionalFilters?: SQL[]
1151
- ): T {
1152
- const junctionTable = registry.getTable(through.table);
1153
- if (!junctionTable) {
1154
- throw new Error(`Junction table not found: ${through.table}`);
1155
- }
1156
-
1157
- const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
1158
- const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
1159
-
1160
- if (!junctionSourceCol) {
1161
- throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
1162
- }
1163
- if (!junctionTargetCol) {
1164
- throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
1165
- }
1166
-
1167
- const baseConditions = [eq(junctionSourceCol, parentId)];
1168
- if (additionalFilters && additionalFilters.length > 0) {
1169
- baseConditions.push(...additionalFilters);
1170
- }
1171
-
1172
- return baseCountQuery
1173
- .innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol))
1174
- .where(and(...baseConditions));
1175
- }
1176
1023
 
1177
1024
  /**
1178
1025
  * Helper method to extract table names from columns
@@ -1194,69 +1041,6 @@ export class DrizzleConditionBuilder {
1194
1041
  return [getColumnName(columns)];
1195
1042
  }
1196
1043
 
1197
- /**
1198
- * Find the corresponding junction table for an inverse many-to-many relation
1199
- */
1200
- private static findCorrespondingJunctionTable(
1201
- relation: Relation,
1202
- registry: PostgresCollectionRegistry
1203
- ): { table: string; sourceColumn: string; targetColumn: string } | null {
1204
- try {
1205
- console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
1206
-
1207
- if (!relation.inverseRelationName) {
1208
- console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
1209
- return null;
1210
- }
1211
-
1212
- // Get the target collection of the inverse relation
1213
- const targetCollection = relation.target();
1214
- console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
1215
-
1216
- // Find the corresponding owning relation on the target collection
1217
- const targetCollectionRelations = resolveCollectionRelations(targetCollection);
1218
- console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
1219
-
1220
- // Look for the owning many-to-many relation that matches our inverseRelationName
1221
- const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
1222
-
1223
- if (!correspondingRelation) {
1224
- console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
1225
- return null;
1226
- }
1227
-
1228
- console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
1229
- relationName: correspondingRelation.relationName,
1230
- cardinality: correspondingRelation.cardinality,
1231
- direction: correspondingRelation.direction,
1232
- hasThrough: !!correspondingRelation.through
1233
- });
1234
-
1235
- // Verify it's an owning many-to-many relation with junction table
1236
- if (correspondingRelation.cardinality !== "many" ||
1237
- correspondingRelation.direction !== "owning" ||
1238
- !correspondingRelation.through) {
1239
- console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
1240
- return null;
1241
- }
1242
-
1243
- console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
1244
-
1245
- // For inverse relation, we need to swap source and target columns
1246
- const through = correspondingRelation.through;
1247
- const result = {
1248
- table: through.table,
1249
- sourceColumn: through.targetColumn, // Swapped for inverse relation
1250
- targetColumn: through.sourceColumn // Swapped for inverse relation
1251
- };
1252
-
1253
- console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
1254
- return result;
1255
- } catch (error) {
1256
- logger.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}'`, { error: error });
1257
- return null;
1258
- }
1259
- }
1260
1044
 
1261
1045
  /**
1262
1046
  * Build vector similarity search expressions for pgvector.