@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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 (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,1000 @@
1
+ import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
2
+ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
3
+ import { FilterValues, WhereFilterOp, Relation, JoinStep } from "@rebasepro/types";
4
+ import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
5
+ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
+ import { ConditionBuilderStatic } from "../interfaces";
7
+
8
+ /** Drizzle dynamic query builder — accepts innerJoin + where chaining */
9
+
10
+ export interface DrizzleDynamicQuery {
11
+ innerJoin(table: PgTable<any>, condition: SQL): this;
12
+ where(condition: SQL | undefined): this;
13
+ limit(limit: number): this;
14
+ }
15
+
16
+ /**
17
+ * Unified condition builder for Drizzle/PostgreSQL queries.
18
+ *
19
+ * This class uses static methods and satisfies the ConditionBuilderStatic<SQL> type.
20
+ * It translates Rebase filter conditions to Drizzle SQL conditions.
21
+ *
22
+ * @example
23
+ * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
24
+ */
25
+ export class DrizzleConditionBuilder {
26
+
27
+ /**
28
+ * Build filter conditions from FilterValues
29
+ */
30
+ static buildFilterConditions<M extends Record<string, unknown>>(
31
+ filter: FilterValues<Extract<keyof M, string>>,
32
+ table: PgTable<any>,
33
+ collectionPath: string
34
+ ): SQL[] {
35
+ const conditions: SQL[] = [];
36
+
37
+ for (const [field, filterParam] of Object.entries(filter)) {
38
+ if (!filterParam) continue;
39
+
40
+ const [op, value] = filterParam as [WhereFilterOp, any];
41
+ const fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
42
+
43
+ if (!fieldColumn) {
44
+ console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
45
+ continue;
46
+ }
47
+
48
+ const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
49
+ if (condition) {
50
+ conditions.push(condition);
51
+ }
52
+ }
53
+
54
+ return conditions;
55
+ }
56
+
57
+ /**
58
+ * Build a single filter condition for a specific operator and value
59
+ */
60
+ static buildSingleFilterCondition(
61
+ column: AnyPgColumn,
62
+ op: WhereFilterOp,
63
+ value: unknown
64
+ ): SQL | null {
65
+ switch (op) {
66
+ case "==":
67
+ return eq(column, value);
68
+ case "!=":
69
+ return sql`${column} != ${value}`;
70
+ case ">":
71
+ return sql`${column} > ${value}`;
72
+ case ">=":
73
+ return sql`${column} >= ${value}`;
74
+ case "<":
75
+ return sql`${column} < ${value}`;
76
+ case "<=":
77
+ return sql`${column} <= ${value}`;
78
+ case "in":
79
+ if (Array.isArray(value) && value.length > 0) {
80
+ return inArray(column, value);
81
+ }
82
+ return null;
83
+ case "array-contains":
84
+ // For JSONB arrays
85
+ return sql`${column} @> ${JSON.stringify([value])}`;
86
+ default:
87
+ console.warn(`Unsupported filter operation: ${op}`);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Build relation-based conditions for different relation types
94
+ */
95
+ static buildRelationConditions(
96
+ relation: Relation,
97
+ parentEntityId: string | number | (string | number)[],
98
+ targetTable: PgTable<any>,
99
+ parentTable: PgTable<any>,
100
+ parentIdColumn: AnyPgColumn,
101
+ targetIdColumn: AnyPgColumn,
102
+ registry: PostgresCollectionRegistry
103
+ ): {
104
+ joinConditions: { table: PgTable<any>; condition: SQL }[];
105
+ whereConditions: SQL[];
106
+ } {
107
+ console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
108
+ relationName: relation.relationName,
109
+ cardinality: relation.cardinality,
110
+ direction: relation.direction,
111
+ hasThrough: !!relation.through,
112
+ hasForeignKeyOnTarget: !!relation.foreignKeyOnTarget,
113
+ inverseRelationName: relation.inverseRelationName,
114
+ parentEntityId: parentEntityId
115
+ });
116
+
117
+ const joinConditions: { table: PgTable<any>; condition: SQL }[] = [];
118
+ const whereConditions: SQL[] = [];
119
+
120
+ if (relation.joinPath && relation.joinPath.length > 0) {
121
+ console.debug("🔍 [buildRelationConditions] Using joinPath logic");
122
+ // Handle join path relations
123
+ const {
124
+ joins,
125
+ finalCondition
126
+ } = this.buildJoinPathConditions(
127
+ relation.joinPath,
128
+ targetTable,
129
+ parentTable,
130
+ parentIdColumn,
131
+ parentEntityId,
132
+ registry
133
+ );
134
+ joinConditions.push(...joins);
135
+ whereConditions.push(finalCondition);
136
+
137
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
138
+ console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
139
+ // Handle many-to-many relations with junction table
140
+ const junctionResult = this.buildJunctionTableConditions(
141
+ relation.through,
142
+ targetIdColumn,
143
+ parentEntityId,
144
+ registry
145
+ );
146
+ joinConditions.push(junctionResult.join);
147
+ whereConditions.push(junctionResult.condition);
148
+
149
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
150
+ console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
151
+ // Handle inverse many-to-many relations with junction table
152
+ const junctionResult = this.buildInverseJunctionTableConditions(
153
+ relation.through,
154
+ targetIdColumn,
155
+ parentEntityId,
156
+ registry
157
+ );
158
+ joinConditions.push(junctionResult.join);
159
+ whereConditions.push(junctionResult.condition);
160
+
161
+ } else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
162
+ console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
163
+
164
+ // First, try to find a junction table (for many-to-many relationships)
165
+ const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
166
+ if (junctionInfo) {
167
+ console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
168
+ const junctionResult = this.buildInverseJunctionTableConditions(
169
+ junctionInfo,
170
+ targetIdColumn,
171
+ parentEntityId,
172
+ registry
173
+ );
174
+ joinConditions.push(junctionResult.join);
175
+ whereConditions.push(junctionResult.condition);
176
+ } else if (relation.foreignKeyOnTarget) {
177
+ console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
178
+ // This is a true inverse one-to-many relationship
179
+ const simpleCondition = this.buildSimpleRelationCondition(
180
+ relation,
181
+ targetTable,
182
+ parentTable,
183
+ parentEntityId
184
+ );
185
+ whereConditions.push(simpleCondition);
186
+ } else {
187
+ console.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
188
+ 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.`);
189
+ }
190
+ } else {
191
+ console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
192
+ // Handle simple relations
193
+ const simpleCondition = this.buildSimpleRelationCondition(
194
+ relation,
195
+ targetTable,
196
+ parentTable,
197
+ parentEntityId
198
+ );
199
+ whereConditions.push(simpleCondition);
200
+ }
201
+
202
+ console.debug("🔍 [buildRelationConditions] Final result:", {
203
+ joinConditionsCount: joinConditions.length,
204
+ whereConditionsCount: whereConditions.length
205
+ });
206
+
207
+ return {
208
+ joinConditions,
209
+ whereConditions
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Build conditions for join path relations
215
+ */
216
+ private static buildJoinPathConditions(
217
+ joinPath: JoinStep[],
218
+ targetTable: PgTable<any>,
219
+ parentTable: PgTable<any>,
220
+ parentIdColumn: AnyPgColumn,
221
+ parentEntityId: string | number | (string | number)[],
222
+ registry: PostgresCollectionRegistry
223
+ ): {
224
+ joins: { table: PgTable<any>; condition: SQL }[];
225
+ finalCondition: SQL;
226
+ } {
227
+ const joins: { table: PgTable<any>; condition: SQL }[] = [];
228
+ let currentTable = targetTable;
229
+
230
+ // Process join steps in reverse order to build path back to parent
231
+ for (const joinStep of [...joinPath].reverse()) {
232
+ const fromTableName = this.getTableNamesFromColumns(joinStep.on.from)[0];
233
+ const toTableName = this.getTableNamesFromColumns(joinStep.on.to)[0];
234
+ const fromColName = this.getColumnNamesFromColumns(joinStep.on.from)[0];
235
+ const toColName = this.getColumnNamesFromColumns(joinStep.on.to)[0];
236
+
237
+ const fromTable = registry.getTable(fromTableName);
238
+ const toTable = registry.getTable(toTableName);
239
+
240
+ if (!fromTable || !toTable) {
241
+ throw new Error(`Join tables not found for step: from ${fromTableName} to ${toTableName}`);
242
+ }
243
+
244
+ const {
245
+ joinTable,
246
+ condition,
247
+ additionalJoins
248
+ } = this.buildSingleJoinCondition(
249
+ currentTable,
250
+ fromTable,
251
+ toTable,
252
+ fromColName,
253
+ toColName,
254
+ fromTableName,
255
+ toTableName,
256
+ registry
257
+ );
258
+
259
+ joins.push({
260
+ table: joinTable,
261
+ condition
262
+ });
263
+ currentTable = joinTable;
264
+
265
+ // Add any additional joins needed for many-to-many relationships
266
+ if (additionalJoins && additionalJoins.length > 0) {
267
+ joins.push(...additionalJoins);
268
+ }
269
+ }
270
+
271
+ // Ensure we've connected back to the parent table
272
+ // For junction tables, we might end up at the junction table instead of the parent table
273
+ if (currentTable !== parentTable) {
274
+ // Try to get table names from the Drizzle table objects
275
+ let currentTableName = "unknown";
276
+ let parentTableName = "unknown";
277
+
278
+ // Try multiple ways to extract table names from Drizzle objects
279
+ if (currentTable && typeof currentTable === "object") {
280
+ // Check common Drizzle table name properties
281
+ currentTableName = (currentTable as unknown as Record<string | symbol, unknown>)[Symbol.for("drizzle:Name")] as string ||
282
+ ((currentTable as unknown as Record<string, unknown>)._ as Record<string, unknown>)?.name as string ||
283
+ (currentTable as unknown as Record<string, unknown>).tableName as string ||
284
+ (currentTable as unknown as Record<string, unknown>).name as string ||
285
+ "unknown";
286
+ }
287
+
288
+ if (parentTable && typeof parentTable === "object") {
289
+ parentTableName = (parentTable as unknown as Record<string | symbol, unknown>)[Symbol.for("drizzle:Name")] as string ||
290
+ ((parentTable as unknown as Record<string, unknown>)._ as Record<string, unknown>)?.name as string ||
291
+ (parentTable as unknown as Record<string, unknown>).tableName as string ||
292
+ (parentTable as unknown as Record<string, unknown>).name as string ||
293
+ "unknown";
294
+ }
295
+
296
+ // For junction table scenarios, be more lenient with validation
297
+ // If we can't determine table names reliably, or if this looks like a junction table scenario,
298
+ // we'll allow it and let the SQL execution validate the correctness
299
+ const couldBeJunctionScenario = currentTableName.includes("_") ||
300
+ currentTableName === "unknown" ||
301
+ parentTableName === "unknown";
302
+
303
+ if (!couldBeJunctionScenario) {
304
+ throw new Error(`Join path did not result in connecting to parent table. Current: ${currentTableName}, Parent: ${parentTableName}`);
305
+ }
306
+ }
307
+
308
+ // Handle both single ID and array of IDs
309
+ const finalCondition = Array.isArray(parentEntityId)
310
+ ? inArray(parentIdColumn, parentEntityId)
311
+ : eq(parentIdColumn, parentEntityId);
312
+
313
+ return {
314
+ joins,
315
+ finalCondition
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Build a single join condition between tables
321
+ */
322
+ private static buildSingleJoinCondition(
323
+ currentTable: PgTable<any>,
324
+ fromTable: PgTable<any>,
325
+ toTable: PgTable<any>,
326
+ fromColName: string,
327
+ toColName: string,
328
+ fromTableName: string,
329
+ toTableName: string,
330
+ registry?: PostgresCollectionRegistry
331
+ ): { joinTable: PgTable<any>; condition: SQL; additionalJoins?: { table: PgTable<any>; condition: SQL }[] } {
332
+ let joinTable: PgTable<any>;
333
+ let condition: SQL;
334
+ const additionalJoins: { table: PgTable<any>; condition: SQL }[] = [];
335
+
336
+ if (currentTable === toTable) {
337
+ // current -> toTable, so join the fromTable
338
+ const left = fromTable[fromColName as keyof typeof fromTable] as AnyPgColumn;
339
+ const right = (currentTable as unknown as Record<string, unknown>)[toColName] as AnyPgColumn;
340
+
341
+ if (!left || !right) {
342
+ // Check if this might be a many-to-many relationship requiring a junction table
343
+ if (registry) {
344
+ const junctionResult = this.tryBuildJunctionJoin(
345
+ currentTable,
346
+ fromTable,
347
+ fromColName,
348
+ toColName,
349
+ fromTableName,
350
+ toTableName,
351
+ registry
352
+ );
353
+ if (junctionResult) {
354
+ return junctionResult;
355
+ }
356
+ }
357
+ throw new Error(`Join columns not found: ${fromTableName}.${fromColName} = ${toTableName}.${toColName}`);
358
+ }
359
+
360
+ joinTable = fromTable;
361
+ condition = eq(left, right);
362
+ } else if (currentTable === fromTable) {
363
+ // current -> fromTable, so join the toTable
364
+ const left = toTable[toColName as keyof typeof toTable] as AnyPgColumn;
365
+ const right = (currentTable as unknown as Record<string, unknown>)[fromColName] as AnyPgColumn;
366
+
367
+ if (!left || !right) {
368
+ // Check if this might be a many-to-many relationship requiring a junction table
369
+ if (registry) {
370
+ const junctionResult = this.tryBuildJunctionJoin(
371
+ currentTable,
372
+ toTable,
373
+ fromColName,
374
+ toColName,
375
+ fromTableName,
376
+ toTableName,
377
+ registry
378
+ );
379
+ if (junctionResult) {
380
+ return junctionResult;
381
+ }
382
+ }
383
+ throw new Error(`Join columns not found: ${toTableName}.${toColName} = ${fromTableName}.${fromColName}`);
384
+ }
385
+
386
+ joinTable = toTable;
387
+ condition = eq(left, right);
388
+ } else {
389
+ throw new Error(`Join step does not match current table. Current table does not match from: ${fromTableName} or to: ${toTableName}`);
390
+ }
391
+
392
+ return {
393
+ joinTable,
394
+ condition,
395
+ additionalJoins
396
+ };
397
+ }
398
+
399
+ /**
400
+ * Try to build a junction table join when direct foreign key relationship is not found
401
+ */
402
+ private static tryBuildJunctionJoin(
403
+ currentTable: PgTable<any>,
404
+ targetTable: PgTable<any>,
405
+ fromColName: string,
406
+ toColName: string,
407
+ fromTableName: string,
408
+ toTableName: string,
409
+ registry: PostgresCollectionRegistry
410
+ ): { joinTable: PgTable<any>; condition: SQL; additionalJoins: { table: PgTable<any>; condition: SQL }[] } | null {
411
+ // Try to find a junction table that connects these two tables
412
+ // Common naming patterns: table1_table2, table1Table2, etc.
413
+ const possibleJunctionNames = [
414
+ `${fromTableName}_${toTableName}`,
415
+ `${toTableName}_${fromTableName}`,
416
+ `${fromTableName}${toTableName.charAt(0).toUpperCase() + toTableName.slice(1)}`,
417
+ `${toTableName}${fromTableName.charAt(0).toUpperCase() + fromTableName.slice(1)}`
418
+ ];
419
+
420
+ for (const junctionName of possibleJunctionNames) {
421
+ const junctionTable = registry.getTable(junctionName);
422
+ if (junctionTable) {
423
+ // Try to find the appropriate columns in the junction table
424
+ const sourceColName = `${fromTableName.slice(0, -1)}_id`; // Remove 's' and add '_id'
425
+ const targetColName = `${toTableName.slice(0, -1)}_id`;
426
+
427
+ const junctionSourceCol = junctionTable[sourceColName as keyof typeof junctionTable] as AnyPgColumn;
428
+ const junctionTargetCol = junctionTable[targetColName as keyof typeof junctionTable] as AnyPgColumn;
429
+
430
+ if (junctionSourceCol && junctionTargetCol) {
431
+ // Found a valid junction table setup
432
+ const currentTableIdCol = Object.values(currentTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
433
+ const targetTableIdCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
434
+
435
+ if (!currentTableIdCol || !targetTableIdCol) {
436
+ continue; // Skip if we can't find primary keys
437
+ }
438
+
439
+ // Determine which direction to join
440
+ if (currentTable === targetTable) {
441
+ // We're joining through junction to reach the other table
442
+ return {
443
+ joinTable: targetTable,
444
+ condition: eq(targetTableIdCol, junctionTargetCol),
445
+ additionalJoins: [
446
+ {
447
+ table: junctionTable,
448
+ condition: eq(currentTableIdCol, junctionSourceCol)
449
+ }
450
+ ]
451
+ };
452
+ } else {
453
+ // Standard junction join
454
+ return {
455
+ joinTable: junctionTable,
456
+ condition: eq(currentTableIdCol, junctionSourceCol),
457
+ additionalJoins: [
458
+ {
459
+ table: targetTable,
460
+ condition: eq(targetTableIdCol, junctionTargetCol)
461
+ }
462
+ ]
463
+ };
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ return null; // No junction table found
470
+ }
471
+
472
+ /**
473
+ * Build conditions for junction table (many-to-many) relations
474
+ */
475
+ private static buildJunctionTableConditions(
476
+ through: { table: string; sourceColumn: string; targetColumn: string },
477
+ targetIdColumn: AnyPgColumn,
478
+ parentEntityId: string | number | (string | number)[],
479
+ registry: PostgresCollectionRegistry
480
+ ): { join: { table: PgTable<any>; condition: SQL }; condition: SQL } {
481
+ const junctionTable = registry.getTable(through.table);
482
+ if (!junctionTable) {
483
+ throw new Error(`Junction table not found: ${through.table}`);
484
+ }
485
+
486
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
487
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
488
+
489
+ if (!junctionSourceCol) {
490
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
491
+ }
492
+ if (!junctionTargetCol) {
493
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
494
+ }
495
+
496
+ // Handle both single ID and array of IDs
497
+ const condition = Array.isArray(parentEntityId)
498
+ ? inArray(junctionSourceCol, parentEntityId)
499
+ : eq(junctionSourceCol, parentEntityId);
500
+
501
+ return {
502
+ join: {
503
+ table: junctionTable,
504
+ condition: eq(targetIdColumn, junctionTargetCol)
505
+ },
506
+ condition
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Build conditions for inverse junction table (many-to-many) relations
512
+ */
513
+ private static buildInverseJunctionTableConditions(
514
+ through: { table: string; sourceColumn: string; targetColumn: string },
515
+ targetIdColumn: AnyPgColumn,
516
+ parentEntityId: string | number | (string | number)[],
517
+ registry: PostgresCollectionRegistry
518
+ ): { join: { table: PgTable<any>; condition: SQL }; condition: SQL } {
519
+ const junctionTable = registry.getTable(through.table);
520
+ if (!junctionTable) {
521
+ throw new Error(`Junction table not found: ${through.table}`);
522
+ }
523
+
524
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
525
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
526
+
527
+ if (!junctionSourceCol) {
528
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
529
+ }
530
+ if (!junctionTargetCol) {
531
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
532
+ }
533
+
534
+ // For inverse relations, the parentEntityId (tag ID) should match the sourceColumn (tag_id)
535
+ // and we want to find target entities (posts) through the targetColumn (post_id)
536
+ const condition = Array.isArray(parentEntityId)
537
+ ? inArray(junctionSourceCol, parentEntityId)
538
+ : eq(junctionSourceCol, parentEntityId);
539
+
540
+ return {
541
+ join: {
542
+ table: junctionTable,
543
+ condition: eq(targetIdColumn, junctionTargetCol)
544
+ },
545
+ condition
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Build conditions for simple relations (owning/inverse without join paths)
551
+ */
552
+ private static buildSimpleRelationCondition(
553
+ relation: Relation,
554
+ targetTable: PgTable<any>,
555
+ parentTable: PgTable<any>,
556
+ parentEntityId: string | number | (string | number)[]
557
+ ): SQL {
558
+ if (relation.direction === "owning" && relation.localKey) {
559
+ // For owning relations, the parentEntityId is actually the foreign key value
560
+ // that should match the target table's primary key
561
+ const targetIdCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
562
+ if (!targetIdCol) {
563
+ // Fallback to looking for an "id" column by name
564
+ const idCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.name === "id") as AnyPgColumn;
565
+ if (!idCol) {
566
+ throw new Error("No primary key or \"id\" column found in target table");
567
+ }
568
+ return Array.isArray(parentEntityId)
569
+ ? inArray(idCol, parentEntityId)
570
+ : eq(idCol, parentEntityId);
571
+ }
572
+ return Array.isArray(parentEntityId)
573
+ ? inArray(targetIdCol, parentEntityId)
574
+ : eq(targetIdCol, parentEntityId);
575
+
576
+ } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
577
+ // Inverse relation: use foreign key on target table
578
+ const foreignKeyCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
579
+ if (!foreignKeyCol) {
580
+ // This could be a many-to-many relationship where foreignKeyOnTarget was set by sanitizeRelation
581
+ // but the column doesn't actually exist. In this case, we should suggest using junction tables.
582
+ 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.`);
583
+ }
584
+ return Array.isArray(parentEntityId)
585
+ ? inArray(foreignKeyCol, parentEntityId)
586
+ : eq(foreignKeyCol, parentEntityId);
587
+
588
+ } else if (relation.direction === "inverse" && relation.cardinality === "many" && relation.inverseRelationName) {
589
+ // For inverse many-to-many relations, this should not be called directly
590
+ // The buildRelationConditions method should handle finding the junction table
591
+ // If we reach here, it means the junction table lookup failed
592
+ 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.`);
593
+
594
+ } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
595
+ // Auto-infer foreign key column for inverse one-to-one relations
596
+ // Pattern: {inverseRelationName}_id (e.g., "author" -> "author_id")
597
+ const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
598
+ const foreignKeyCol = targetTable[inferredForeignKeyName as keyof typeof targetTable] as AnyPgColumn;
599
+
600
+ if (!foreignKeyCol) {
601
+ throw new Error(`Auto-inferred foreign key column '${inferredForeignKeyName}' not found in target table for inverse relation '${relation.relationName}'. Please specify 'foreignKeyOnTarget' explicitly.`);
602
+ }
603
+
604
+ console.debug(`🔍 [DrizzleConditionBuilder] Auto-inferred foreign key '${inferredForeignKeyName}' for inverse relation '${relation.relationName}'`);
605
+
606
+ return Array.isArray(parentEntityId)
607
+ ? inArray(foreignKeyCol, parentEntityId)
608
+ : eq(foreignKeyCol, parentEntityId);
609
+
610
+ } else {
611
+ throw new Error(`Relation '${relation.relationName}' lacks proper configuration. For many-to-many relations, use 'through' property. For simple relations, use 'localKey' or 'foreignKeyOnTarget'.`);
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Combine multiple conditions with AND operator
617
+ */
618
+ static combineConditionsWithAnd(conditions: SQL[]): SQL | undefined {
619
+ if (conditions.length === 0) return undefined;
620
+ if (conditions.length === 1) return conditions[0];
621
+ return and(...conditions);
622
+ }
623
+
624
+ /**
625
+ * Combine multiple conditions with OR operator
626
+ */
627
+ static combineConditionsWithOr(conditions: SQL[]): SQL | undefined {
628
+ if (conditions.length === 0) return undefined;
629
+ if (conditions.length === 1) return conditions[0];
630
+ return or(...conditions);
631
+ }
632
+
633
+ /**
634
+ * Build search conditions for text fields
635
+ */
636
+ static buildSearchConditions(
637
+ searchString: string,
638
+ properties: Record<string, unknown>,
639
+ table: PgTable<any>
640
+ ): SQL[] {
641
+ const searchConditions: SQL[] = [];
642
+
643
+ for (const [key, prop] of Object.entries(properties)) {
644
+ const p = prop as Record<string, unknown>;
645
+ // Only include string properties that don't have enum defined
646
+ // PostgreSQL enum and uuid columns don't support ILIKE, so we skip them
647
+ if (p.type === "string" && !p.enum && p.isId !== "uuid") {
648
+ const fieldColumn = table[key as keyof typeof table] as AnyPgColumn;
649
+ if (fieldColumn) {
650
+ searchConditions.push(ilike(fieldColumn, `%${searchString}%`));
651
+ }
652
+ }
653
+ }
654
+
655
+ return searchConditions;
656
+ }
657
+
658
+ /**
659
+ * Build a unique field check condition
660
+ */
661
+ static buildUniqueFieldCondition(
662
+ fieldColumn: AnyPgColumn,
663
+ value: unknown,
664
+ idColumn?: AnyPgColumn,
665
+ excludeId?: string | number
666
+ ): SQL[] {
667
+ const conditions: SQL[] = [eq(fieldColumn, value)];
668
+
669
+ if (excludeId && idColumn) {
670
+ conditions.push(sql`${idColumn} != ${excludeId}`);
671
+ }
672
+
673
+ return conditions;
674
+ }
675
+
676
+ /**
677
+ * Build relation-based query with joins and conditions
678
+ */
679
+ static buildRelationQuery<T extends DrizzleDynamicQuery>(
680
+ baseQuery: T,
681
+ relation: Relation,
682
+ parentEntityId: string | number | (string | number)[],
683
+ targetTable: PgTable<any>,
684
+ parentTable: PgTable<any>,
685
+ parentIdColumn: AnyPgColumn,
686
+ targetIdColumn: AnyPgColumn,
687
+ registry: PostgresCollectionRegistry,
688
+ additionalFilters?: SQL[]
689
+ ): T {
690
+ const { joinConditions, whereConditions } = this.buildRelationConditions(
691
+ relation,
692
+ parentEntityId,
693
+ targetTable,
694
+ parentTable,
695
+ parentIdColumn,
696
+ targetIdColumn,
697
+ registry
698
+ );
699
+
700
+ let query = baseQuery;
701
+
702
+ // Apply joins
703
+ for (const { table, condition } of joinConditions) {
704
+ query = query.innerJoin(table, condition);
705
+ }
706
+
707
+ // Combine all conditions
708
+ const allConditions = [...whereConditions];
709
+ if (additionalFilters) {
710
+ allConditions.push(...additionalFilters);
711
+ }
712
+
713
+ // Apply where conditions
714
+ if (allConditions.length > 0) {
715
+ query = query.where(and(...allConditions));
716
+ }
717
+
718
+ return query;
719
+ }
720
+
721
+ /**
722
+ * Build count query for relations with proper joins and conditions
723
+ */
724
+ static buildRelationCountQuery<T extends DrizzleDynamicQuery>(
725
+ baseCountQuery: T,
726
+ relation: Relation,
727
+ parentEntityId: string | number,
728
+ targetTable: PgTable<any>,
729
+ parentTable: PgTable<any>,
730
+ parentIdColumn: AnyPgColumn,
731
+ targetIdColumn: AnyPgColumn,
732
+ registry: PostgresCollectionRegistry,
733
+ additionalFilters?: SQL[]
734
+ ): T {
735
+ // For count queries, we need to handle joins differently to avoid duplicates
736
+ if (relation.joinPath && relation.joinPath.length > 0) {
737
+ return this.buildJoinPathCountQuery(
738
+ baseCountQuery,
739
+ relation.joinPath,
740
+ targetTable,
741
+ parentTable,
742
+ parentIdColumn,
743
+ parentEntityId,
744
+ registry,
745
+ additionalFilters
746
+ );
747
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
748
+ return this.buildJunctionCountQuery(
749
+ baseCountQuery,
750
+ relation.through,
751
+ targetIdColumn,
752
+ parentEntityId,
753
+ registry,
754
+ additionalFilters
755
+ );
756
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
757
+ return this.buildInverseJunctionCountQuery(
758
+ baseCountQuery,
759
+ relation.through,
760
+ targetIdColumn,
761
+ parentEntityId,
762
+ registry,
763
+ additionalFilters
764
+ );
765
+ } else {
766
+ // Simple relations
767
+ const simpleCondition = this.buildSimpleRelationCondition(
768
+ relation,
769
+ targetTable,
770
+ parentTable,
771
+ parentEntityId
772
+ );
773
+
774
+ const allConditions = [simpleCondition];
775
+ if (additionalFilters) {
776
+ allConditions.push(...additionalFilters);
777
+ }
778
+
779
+ return baseCountQuery.where(and(...allConditions));
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Build join path conditions for count queries
785
+ */
786
+ private static buildJoinPathCountQuery<T extends DrizzleDynamicQuery>(
787
+ baseCountQuery: T,
788
+ joinPath: JoinStep[],
789
+ targetTable: PgTable<any>,
790
+ parentTable: PgTable<any>,
791
+ parentIdColumn: AnyPgColumn,
792
+ parentEntityId: string | number,
793
+ registry: PostgresCollectionRegistry,
794
+ additionalFilters?: SQL[]
795
+ ): T {
796
+ let query = baseCountQuery;
797
+ let currentTable = targetTable;
798
+
799
+ // Process join steps in reverse order
800
+ for (const joinStep of [...joinPath].reverse()) {
801
+ const fromTableName = this.getTableNamesFromColumns(joinStep.on.from)[0];
802
+ const toTableName = this.getTableNamesFromColumns(joinStep.on.to)[0];
803
+ const fromColName = this.getColumnNamesFromColumns(joinStep.on.from)[0];
804
+ const toColName = this.getColumnNamesFromColumns(joinStep.on.to)[0];
805
+
806
+ const fromTable = registry.getTable(fromTableName);
807
+ const toTable = registry.getTable(toTableName);
808
+
809
+ if (!fromTable || !toTable) {
810
+ throw new Error(`Join tables not found for step: from ${fromTableName} to ${toTableName}`);
811
+ }
812
+
813
+ const { joinTable, condition } = this.buildSingleJoinCondition(
814
+ currentTable,
815
+ fromTable,
816
+ toTable,
817
+ fromColName,
818
+ toColName,
819
+ fromTableName,
820
+ toTableName
821
+ );
822
+
823
+ query = query.innerJoin(joinTable, condition);
824
+ currentTable = joinTable;
825
+ }
826
+
827
+ if (currentTable !== parentTable) {
828
+ throw new Error("Join path did not result in connecting to parent table");
829
+ }
830
+
831
+ const allConditions = [eq(parentIdColumn, parentEntityId)];
832
+ if (additionalFilters) {
833
+ allConditions.push(...additionalFilters);
834
+ }
835
+
836
+ return query.where(and(...allConditions));
837
+ }
838
+
839
+ /**
840
+ * Build junction table conditions for count queries
841
+ */
842
+ private static buildJunctionCountQuery<T extends DrizzleDynamicQuery>(
843
+ baseCountQuery: T,
844
+ through: { table: string; sourceColumn: string; targetColumn: string },
845
+ targetIdColumn: AnyPgColumn,
846
+ parentEntityId: string | number,
847
+ registry: PostgresCollectionRegistry,
848
+ additionalFilters?: SQL[]
849
+ ): T {
850
+ const junctionTable = registry.getTable(through.table);
851
+ if (!junctionTable) {
852
+ throw new Error(`Junction table not found: ${through.table}`);
853
+ }
854
+
855
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
856
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
857
+
858
+ if (!junctionSourceCol) {
859
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
860
+ }
861
+ if (!junctionTargetCol) {
862
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
863
+ }
864
+
865
+ const baseConditions = [eq(junctionSourceCol, parentEntityId)];
866
+ if (additionalFilters && additionalFilters.length > 0) {
867
+ baseConditions.push(...additionalFilters);
868
+ }
869
+
870
+ return baseCountQuery
871
+ .innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol))
872
+ .where(and(...baseConditions));
873
+ }
874
+
875
+ /**
876
+ * Build inverse junction table conditions for count queries
877
+ */
878
+ private static buildInverseJunctionCountQuery<T extends DrizzleDynamicQuery>(
879
+ baseCountQuery: T,
880
+ through: { table: string; sourceColumn: string; targetColumn: string },
881
+ targetIdColumn: AnyPgColumn,
882
+ parentEntityId: string | number,
883
+ registry: PostgresCollectionRegistry,
884
+ additionalFilters?: SQL[]
885
+ ): T {
886
+ const junctionTable = registry.getTable(through.table);
887
+ if (!junctionTable) {
888
+ throw new Error(`Junction table not found: ${through.table}`);
889
+ }
890
+
891
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
892
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
893
+
894
+ if (!junctionSourceCol) {
895
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
896
+ }
897
+ if (!junctionTargetCol) {
898
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
899
+ }
900
+
901
+ const baseConditions = [eq(junctionSourceCol, parentEntityId)];
902
+ if (additionalFilters && additionalFilters.length > 0) {
903
+ baseConditions.push(...additionalFilters);
904
+ }
905
+
906
+ return baseCountQuery
907
+ .innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol))
908
+ .where(and(...baseConditions));
909
+ }
910
+
911
+ /**
912
+ * Helper method to extract table names from columns
913
+ */
914
+ static getTableNamesFromColumns(columns: string | string[]): string[] {
915
+ if (Array.isArray(columns)) {
916
+ return columns.map(col => col.includes(".") ? col.split(".")[0] : "");
917
+ }
918
+ return [columns.includes(".") ? columns.split(".")[0] : ""];
919
+ }
920
+
921
+ /**
922
+ * Helper method to extract column names from columns
923
+ */
924
+ static getColumnNamesFromColumns(columns: string | string[]): string[] {
925
+ if (Array.isArray(columns)) {
926
+ return columns.map(col => getColumnName(col));
927
+ }
928
+ return [getColumnName(columns)];
929
+ }
930
+
931
+ /**
932
+ * Find the corresponding junction table for an inverse many-to-many relation
933
+ */
934
+ private static findCorrespondingJunctionTable(
935
+ relation: Relation,
936
+ registry: PostgresCollectionRegistry
937
+ ): { table: string; sourceColumn: string; targetColumn: string } | null {
938
+ try {
939
+ console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
940
+
941
+ if (!relation.inverseRelationName) {
942
+ console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
943
+ return null;
944
+ }
945
+
946
+ // Get the target collection of the inverse relation
947
+ const targetCollection = relation.target();
948
+ console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
949
+
950
+ // Find the corresponding owning relation on the target collection
951
+ const targetCollectionRelations = resolveCollectionRelations(targetCollection);
952
+ console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
953
+
954
+ // Look for the owning many-to-many relation that matches our inverseRelationName
955
+ const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
956
+
957
+ if (!correspondingRelation) {
958
+ console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
959
+ return null;
960
+ }
961
+
962
+ console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
963
+ relationName: correspondingRelation.relationName,
964
+ cardinality: correspondingRelation.cardinality,
965
+ direction: correspondingRelation.direction,
966
+ hasThrough: !!correspondingRelation.through
967
+ });
968
+
969
+ // Verify it's an owning many-to-many relation with junction table
970
+ if (correspondingRelation.cardinality !== "many" ||
971
+ correspondingRelation.direction !== "owning" ||
972
+ !correspondingRelation.through) {
973
+ console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
974
+ return null;
975
+ }
976
+
977
+ console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
978
+
979
+ // For inverse relation, we need to swap source and target columns
980
+ const through = correspondingRelation.through;
981
+ const result = {
982
+ table: through.table,
983
+ sourceColumn: through.targetColumn, // Swapped for inverse relation
984
+ targetColumn: through.sourceColumn // Swapped for inverse relation
985
+ };
986
+
987
+ console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
988
+ return result;
989
+ } catch (error) {
990
+ console.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}':`, error);
991
+ return null;
992
+ }
993
+ }
994
+ }
995
+
996
+ /**
997
+ * Alias for DrizzleConditionBuilder for consistent naming with other database implementations.
998
+ * This allows code to use PostgresConditionBuilder alongside future MongoConditionBuilder, etc.
999
+ */
1000
+ export const PostgresConditionBuilder = DrizzleConditionBuilder;