@rebasepro/server-postgres 0.0.1-canary.4829d6e

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 (121) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/PostgresAdapter.d.ts +6 -0
  4. package/dist/PostgresBackendDriver.d.ts +150 -0
  5. package/dist/PostgresBootstrapper.d.ts +51 -0
  6. package/dist/auth/ensure-tables.d.ts +10 -0
  7. package/dist/auth/services.d.ts +250 -0
  8. package/dist/backup/backup-cli.d.ts +3 -0
  9. package/dist/backup/backup-cron.d.ts +53 -0
  10. package/dist/backup/backup-service.d.ts +85 -0
  11. package/dist/backup/index.d.ts +12 -0
  12. package/dist/backup/pg-tools.d.ts +110 -0
  13. package/dist/backup/retention.d.ts +35 -0
  14. package/dist/chunk-DSJWtz9O.js +40 -0
  15. package/dist/cli-errors.d.ts +42 -0
  16. package/dist/cli-helpers.d.ts +7 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
  19. package/dist/connection.d.ts +65 -0
  20. package/dist/data-transformer.d.ts +55 -0
  21. package/dist/databasePoolManager.d.ts +20 -0
  22. package/dist/history/HistoryService.d.ts +71 -0
  23. package/dist/history/ensure-history-table.d.ts +7 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.es.js +23535 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/interfaces.d.ts +18 -0
  28. package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
  29. package/dist/schema/auth-default-policies.d.ts +10 -0
  30. package/dist/schema/auth-schema.d.ts +2376 -0
  31. package/dist/schema/doctor-cli.d.ts +2 -0
  32. package/dist/schema/doctor.d.ts +58 -0
  33. package/dist/schema/dynamic-tables.d.ts +31 -0
  34. package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
  35. package/dist/schema/generate-drizzle-schema.d.ts +1 -0
  36. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
  37. package/dist/schema/generate-postgres-ddl.d.ts +1 -0
  38. package/dist/schema/introspect-db-inference.d.ts +5 -0
  39. package/dist/schema/introspect-db-logic.d.ts +118 -0
  40. package/dist/schema/introspect-db.d.ts +1 -0
  41. package/dist/schema/introspect-runtime.d.ts +57 -0
  42. package/dist/schema/test-schema.d.ts +24 -0
  43. package/dist/security/policy-drift.d.ts +57 -0
  44. package/dist/security/rls-enforcement.d.ts +122 -0
  45. package/dist/services/BranchService.d.ts +47 -0
  46. package/dist/services/FetchService.d.ts +214 -0
  47. package/dist/services/PersistService.d.ts +39 -0
  48. package/dist/services/RelationService.d.ts +109 -0
  49. package/dist/services/cdc/CdcListener.d.ts +54 -0
  50. package/dist/services/cdc/trigger-cdc.d.ts +64 -0
  51. package/dist/services/collection-helpers.d.ts +38 -0
  52. package/dist/services/dataService.d.ts +110 -0
  53. package/dist/services/index.d.ts +4 -0
  54. package/dist/services/realtimeService.d.ts +298 -0
  55. package/dist/src-Eh-CZosp.js +595 -0
  56. package/dist/src-Eh-CZosp.js.map +1 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/utils/drizzle-conditions.d.ts +138 -0
  59. package/dist/utils/pg-array-null-patch.d.ts +16 -0
  60. package/dist/utils/pg-error-utils.d.ts +65 -0
  61. package/dist/utils/table-classification.d.ts +8 -0
  62. package/dist/websocket.d.ts +18 -0
  63. package/package.json +113 -0
  64. package/src/PostgresAdapter.ts +58 -0
  65. package/src/PostgresBackendDriver.ts +1387 -0
  66. package/src/PostgresBootstrapper.ts +581 -0
  67. package/src/auth/ensure-tables.ts +367 -0
  68. package/src/auth/services.ts +1321 -0
  69. package/src/backup/backup-cli.ts +383 -0
  70. package/src/backup/backup-cron.ts +189 -0
  71. package/src/backup/backup-service.ts +299 -0
  72. package/src/backup/index.ts +12 -0
  73. package/src/backup/pg-tools.ts +231 -0
  74. package/src/backup/retention.ts +75 -0
  75. package/src/cli-errors.ts +265 -0
  76. package/src/cli-helpers.ts +196 -0
  77. package/src/cli.ts +786 -0
  78. package/src/collections/PostgresCollectionRegistry.ts +103 -0
  79. package/src/connection.ts +166 -0
  80. package/src/data-transformer.ts +733 -0
  81. package/src/databasePoolManager.ts +87 -0
  82. package/src/history/HistoryService.ts +272 -0
  83. package/src/history/ensure-history-table.ts +46 -0
  84. package/src/index.ts +15 -0
  85. package/src/interfaces.ts +60 -0
  86. package/src/schema/auth-bootstrap-sql.ts +41 -0
  87. package/src/schema/auth-default-policies.ts +125 -0
  88. package/src/schema/auth-schema.ts +233 -0
  89. package/src/schema/doctor-cli.ts +113 -0
  90. package/src/schema/doctor.ts +733 -0
  91. package/src/schema/dynamic-tables.test.ts +302 -0
  92. package/src/schema/dynamic-tables.ts +293 -0
  93. package/src/schema/generate-drizzle-schema-logic.ts +850 -0
  94. package/src/schema/generate-drizzle-schema.ts +131 -0
  95. package/src/schema/generate-postgres-ddl-logic.ts +490 -0
  96. package/src/schema/generate-postgres-ddl.ts +92 -0
  97. package/src/schema/introspect-db-inference.ts +238 -0
  98. package/src/schema/introspect-db-logic.ts +910 -0
  99. package/src/schema/introspect-db.ts +266 -0
  100. package/src/schema/introspect-runtime.test.ts +212 -0
  101. package/src/schema/introspect-runtime.ts +293 -0
  102. package/src/schema/test-schema.ts +11 -0
  103. package/src/security/policy-drift.test.ts +122 -0
  104. package/src/security/policy-drift.ts +159 -0
  105. package/src/security/rls-enforcement.ts +295 -0
  106. package/src/services/BranchService.ts +251 -0
  107. package/src/services/FetchService.ts +1661 -0
  108. package/src/services/PersistService.ts +329 -0
  109. package/src/services/RelationService.ts +1306 -0
  110. package/src/services/cdc/CdcListener.ts +167 -0
  111. package/src/services/cdc/trigger-cdc.ts +169 -0
  112. package/src/services/collection-helpers.ts +151 -0
  113. package/src/services/dataService.ts +246 -0
  114. package/src/services/index.ts +13 -0
  115. package/src/services/realtimeService.ts +1502 -0
  116. package/src/types.ts +4 -0
  117. package/src/utils/drizzle-conditions.ts +1162 -0
  118. package/src/utils/pg-array-null-patch.ts +42 -0
  119. package/src/utils/pg-error-utils.ts +227 -0
  120. package/src/utils/table-classification.ts +16 -0
  121. package/src/websocket.ts +640 -0
@@ -0,0 +1,1162 @@
1
+ import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
2
+ import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
3
+ import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
4
+ import { getColumnName, normalizeToEntityRelation, resolveCollectionRelations } from "@rebasepro/common";
5
+ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
6
+ import { ConditionBuilderStatic } from "../interfaces";
7
+ import { logger } from "@rebasepro/server";
8
+ import { getColumnMeta } from "../services/collection-helpers";
9
+
10
+ /**
11
+ * Filter values may arrive as relation wire objects — `EntityRelation`
12
+ * instances or their JSON form `{ __type: "relation", id, path }` — e.g. when
13
+ * the admin filters a relation column. SQL comparisons need the raw id, so
14
+ * unwrap them here (element-wise for list operators like `in` / `not-in`).
15
+ */
16
+ function unwrapRelationFilterValue(value: unknown): unknown {
17
+ if (Array.isArray(value)) return value.map(unwrapRelationFilterValue);
18
+ const relation = normalizeToEntityRelation(value);
19
+ return relation ? relation.id : value;
20
+ }
21
+
22
+ /** Drizzle dynamic query builder — accepts innerJoin + where chaining */
23
+
24
+ export interface DrizzleDynamicQuery {
25
+ innerJoin(table: PgTable<any>, condition: SQL): this;
26
+ where(condition: SQL | undefined): this;
27
+ limit(limit: number): this;
28
+ }
29
+
30
+ /**
31
+ * Unified condition builder for Drizzle/PostgreSQL queries.
32
+ *
33
+ * This class uses static methods and satisfies the ConditionBuilderStatic<SQL> type.
34
+ * It translates Rebase filter conditions to Drizzle SQL conditions.
35
+ *
36
+ * @example
37
+ * const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
38
+ */
39
+ export class DrizzleConditionBuilder {
40
+
41
+ /**
42
+ * Build filter conditions from FilterValues
43
+ */
44
+ static buildFilterConditions<M extends Record<string, unknown>>(
45
+ filter: FilterValues<Extract<keyof M, string>>,
46
+ table: PgTable<any>,
47
+ collectionPath: string
48
+ ): SQL[] {
49
+ const conditions: SQL[] = [];
50
+
51
+ for (const [field, filterParam] of Object.entries(filter)) {
52
+ if (!filterParam) continue;
53
+
54
+ let fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
55
+
56
+ if (!fieldColumn) {
57
+ // Fallback for relations (e.g. project -> project_id)
58
+ const relationKey = `${field}_id`;
59
+ if (relationKey in table) {
60
+ fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
61
+ }
62
+ }
63
+
64
+ if (!fieldColumn) {
65
+ logger.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
66
+ continue;
67
+ }
68
+
69
+ const paramsList = Array.isArray(filterParam) && filterParam.length > 0 && Array.isArray(filterParam[0])
70
+ ? (filterParam as [WhereFilterOp, any][])
71
+ : [filterParam as [WhereFilterOp, any]];
72
+
73
+ for (const [op, value] of paramsList) {
74
+ const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
75
+ if (condition) {
76
+ conditions.push(condition);
77
+ }
78
+ }
79
+ }
80
+
81
+ return conditions;
82
+ }
83
+
84
+ /**
85
+ * Build logical conditions recursively from LogicalCondition or FilterCondition
86
+ */
87
+ static buildLogicalConditions(
88
+ cond: LogicalCondition | FilterCondition,
89
+ table: PgTable<any>,
90
+ collectionPath: string
91
+ ): SQL | null {
92
+ if ("type" in cond) {
93
+ const subSQLs = cond.conditions
94
+ .map(c => this.buildLogicalConditions(c, table, collectionPath))
95
+ .filter((sql): sql is SQL => sql !== null);
96
+ if (subSQLs.length === 0) return null;
97
+ return (cond.type === "or" ? or(...subSQLs) : and(...subSQLs)) ?? null;
98
+ } else {
99
+ let fieldColumn = table[cond.column as keyof typeof table] as AnyPgColumn;
100
+ if (!fieldColumn) {
101
+ const relationKey = `${cond.column}_id`;
102
+ if (relationKey in table) {
103
+ fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
104
+ }
105
+ }
106
+ if (!fieldColumn) {
107
+ logger.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
108
+ return null;
109
+ }
110
+ return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Build a single filter condition for a specific operator and value
116
+ */
117
+ static buildSingleFilterCondition(
118
+ column: AnyPgColumn,
119
+ op: WhereFilterOp,
120
+ value: unknown
121
+ ): SQL | null {
122
+ value = unwrapRelationFilterValue(value);
123
+ switch (op) {
124
+ case "==":
125
+ if (value === null || value === undefined) {
126
+ return sql`${column} IS NULL`;
127
+ }
128
+ return eq(column, value);
129
+ case "!=":
130
+ if (value === null || value === undefined) {
131
+ return sql`${column} IS NOT NULL`;
132
+ }
133
+ return sql`${column} != ${value}`;
134
+ case ">":
135
+ return sql`${column} > ${value}`;
136
+ case ">=":
137
+ return sql`${column} >= ${value}`;
138
+ case "<":
139
+ return sql`${column} < ${value}`;
140
+ case "<=":
141
+ return sql`${column} <= ${value}`;
142
+ case "in":
143
+ if (Array.isArray(value) && value.length > 0) {
144
+ return inArray(column, value);
145
+ }
146
+ return null;
147
+ case "array-contains": {
148
+ const meta = getColumnMeta(column);
149
+ if (meta.dataType === "array" || meta.columnType === "PgArray") {
150
+ return sql`${column} @> ARRAY[${value}]`;
151
+ }
152
+ // For JSONB arrays: checks if the column contains the given value
153
+ return sql`${column} @> ${JSON.stringify([value])}`;
154
+ }
155
+ case "array-contains-any": {
156
+ const meta = getColumnMeta(column);
157
+ const isNativeArray = meta.dataType === "array" || meta.columnType === "PgArray";
158
+ if (Array.isArray(value) && value.length > 0) {
159
+ if (isNativeArray) {
160
+ return sql`${column} && ARRAY[${sql.join(value.map(v => sql`${v}`), sql`, `)}]`;
161
+ } else {
162
+ // Use the ?| operator for JSONB overlap with text array
163
+ const textValues = value.map(v => String(v));
164
+ return sql`${column} ?| array[${sql.join(textValues.map(v => sql`${v}`), sql`, `)}]`;
165
+ }
166
+ }
167
+ // Single value fallback: treat as array-contains
168
+ if (isNativeArray) {
169
+ return sql`${column} @> ARRAY[${value}]`;
170
+ }
171
+ return sql`${column} @> ${JSON.stringify([value])}`;
172
+ }
173
+ case "not-in":
174
+ if (Array.isArray(value) && value.length > 0) {
175
+ return sql`${column} NOT IN (${sql.join(value.map(v => sql`${v}`), sql`, `)})`;
176
+ }
177
+ return null;
178
+ case "like":
179
+ return sql`${column} LIKE ${String(value)}`;
180
+ case "ilike":
181
+ return sql`${column} ILIKE ${String(value)}`;
182
+ case "not-like":
183
+ return sql`${column} NOT LIKE ${String(value)}`;
184
+ case "not-ilike":
185
+ return sql`${column} NOT ILIKE ${String(value)}`;
186
+ case "is-null":
187
+ return sql`${column} IS NULL`;
188
+ case "is-not-null":
189
+ return sql`${column} IS NOT NULL`;
190
+ default:
191
+ logger.warn(`Unsupported filter operation: ${op}`);
192
+ return null;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Build relation-based conditions for different relation types
198
+ */
199
+ static buildRelationConditions(
200
+ relation: Relation,
201
+ parentId: string | number | (string | number)[],
202
+ targetTable: PgTable<any>,
203
+ parentTable: PgTable<any>,
204
+ parentIdColumn: AnyPgColumn,
205
+ targetIdColumn: AnyPgColumn,
206
+ registry: PostgresCollectionRegistry
207
+ ): {
208
+ joinConditions: { table: PgTable<any>; condition: SQL }[];
209
+ whereConditions: SQL[];
210
+ } {
211
+ console.debug("🔍 [buildRelationConditions] Building conditions for relation:", {
212
+ relationName: relation.relationName,
213
+ cardinality: relation.cardinality,
214
+ direction: relation.direction,
215
+ hasThrough: !!relation.through,
216
+ hasForeignKeyOnTarget: !!relation.foreignKeyOnTarget,
217
+ inverseRelationName: relation.inverseRelationName,
218
+ parentId: parentId
219
+ });
220
+
221
+ const joinConditions: { table: PgTable<any>; condition: SQL }[] = [];
222
+ const whereConditions: SQL[] = [];
223
+
224
+ if (relation.joinPath && relation.joinPath.length > 0) {
225
+ console.debug("🔍 [buildRelationConditions] Using joinPath logic");
226
+ // Handle join path relations
227
+ const {
228
+ joins,
229
+ finalCondition
230
+ } = this.buildJoinPathConditions(
231
+ relation.joinPath,
232
+ targetTable,
233
+ parentTable,
234
+ parentIdColumn,
235
+ parentId,
236
+ registry
237
+ );
238
+ joinConditions.push(...joins);
239
+ whereConditions.push(finalCondition);
240
+
241
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
242
+ console.debug("🔍 [buildRelationConditions] Using owning many-to-many with explicit through");
243
+ // Handle many-to-many relations with junction table
244
+ const junctionResult = this.buildJunctionTableConditions(
245
+ relation.through,
246
+ targetIdColumn,
247
+ parentId,
248
+ registry
249
+ );
250
+ joinConditions.push(junctionResult.join);
251
+ whereConditions.push(junctionResult.condition);
252
+
253
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
254
+ console.debug("🔍 [buildRelationConditions] Using inverse many-to-many with explicit through");
255
+ // Handle inverse many-to-many relations with junction table
256
+ const junctionResult = this.buildInverseJunctionTableConditions(
257
+ relation.through,
258
+ targetIdColumn,
259
+ parentId,
260
+ registry
261
+ );
262
+ joinConditions.push(junctionResult.join);
263
+ whereConditions.push(junctionResult.condition);
264
+
265
+ } else if (relation.cardinality === "many" && relation.direction === "inverse" && !relation.through) {
266
+ console.debug("🔍 [buildRelationConditions] Handling inverse many relationship without explicit through");
267
+
268
+ // First, try to find a junction table (for many-to-many relationships)
269
+ const junctionInfo = this.findCorrespondingJunctionTable(relation, registry);
270
+ if (junctionInfo) {
271
+ console.debug("🔍 [buildRelationConditions] Found junction info for inverse many-to-many, building junction conditions");
272
+ const junctionResult = this.buildInverseJunctionTableConditions(
273
+ junctionInfo,
274
+ targetIdColumn,
275
+ parentId,
276
+ registry
277
+ );
278
+ joinConditions.push(junctionResult.join);
279
+ whereConditions.push(junctionResult.condition);
280
+ } else if (relation.foreignKeyOnTarget) {
281
+ console.debug("🔍 [buildRelationConditions] No junction table found, treating as inverse one-to-many with foreign key on target");
282
+ // This is a true inverse one-to-many relationship
283
+ const simpleCondition = this.buildSimpleRelationCondition(
284
+ relation,
285
+ targetTable,
286
+ parentTable,
287
+ parentId
288
+ );
289
+ whereConditions.push(simpleCondition);
290
+ } else {
291
+ logger.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
292
+ 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.`);
293
+ }
294
+ } else {
295
+ console.debug("🔍 [buildRelationConditions] Using simple relation logic - THIS IS WHERE THE ERROR MIGHT OCCUR");
296
+ // Handle simple relations
297
+ const simpleCondition = this.buildSimpleRelationCondition(
298
+ relation,
299
+ targetTable,
300
+ parentTable,
301
+ parentId
302
+ );
303
+ whereConditions.push(simpleCondition);
304
+ }
305
+
306
+ console.debug("🔍 [buildRelationConditions] Final result:", {
307
+ joinConditionsCount: joinConditions.length,
308
+ whereConditionsCount: whereConditions.length
309
+ });
310
+
311
+ return {
312
+ joinConditions,
313
+ whereConditions
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Build conditions for join path relations
319
+ */
320
+ private static buildJoinPathConditions(
321
+ joinPath: JoinStep[],
322
+ targetTable: PgTable<any>,
323
+ parentTable: PgTable<any>,
324
+ parentIdColumn: AnyPgColumn,
325
+ parentId: string | number | (string | number)[],
326
+ registry: PostgresCollectionRegistry
327
+ ): {
328
+ joins: { table: PgTable<any>; condition: SQL }[];
329
+ finalCondition: SQL;
330
+ } {
331
+ const joins: { table: PgTable<any>; condition: SQL }[] = [];
332
+ let currentTable = targetTable;
333
+
334
+ // Process join steps in reverse order to build path back to parent
335
+ for (const joinStep of [...joinPath].reverse()) {
336
+ const fromTableName = this.getTableNamesFromColumns(joinStep.on.from)[0];
337
+ const toTableName = this.getTableNamesFromColumns(joinStep.on.to)[0];
338
+ const fromColName = this.getColumnNamesFromColumns(joinStep.on.from)[0];
339
+ const toColName = this.getColumnNamesFromColumns(joinStep.on.to)[0];
340
+
341
+ const fromTable = registry.getTable(fromTableName);
342
+ const toTable = registry.getTable(toTableName);
343
+
344
+ if (!fromTable || !toTable) {
345
+ throw new Error(`Join tables not found for step: from ${fromTableName} to ${toTableName}`);
346
+ }
347
+
348
+ const {
349
+ joinTable,
350
+ condition,
351
+ additionalJoins
352
+ } = this.buildSingleJoinCondition(
353
+ currentTable,
354
+ fromTable,
355
+ toTable,
356
+ fromColName,
357
+ toColName,
358
+ fromTableName,
359
+ toTableName,
360
+ registry
361
+ );
362
+
363
+ joins.push({
364
+ table: joinTable,
365
+ condition
366
+ });
367
+ currentTable = joinTable;
368
+
369
+ // Add any additional joins needed for many-to-many relationships
370
+ if (additionalJoins && additionalJoins.length > 0) {
371
+ joins.push(...additionalJoins);
372
+ }
373
+ }
374
+
375
+ // Ensure we've connected back to the parent table
376
+ // For junction tables, we might end up at the junction table instead of the parent table
377
+ if (currentTable !== parentTable) {
378
+ // Try to get table names from the Drizzle table objects
379
+ let currentTableName = "unknown";
380
+ let parentTableName = "unknown";
381
+
382
+ // Try multiple ways to extract table names from Drizzle objects
383
+ if (currentTable && typeof currentTable === "object") {
384
+ // Check common Drizzle table name properties
385
+ currentTableName = (currentTable as unknown as Record<string | symbol, unknown>)[Symbol.for("drizzle:Name")] as string ||
386
+ ((currentTable as unknown as Record<string, unknown>)._ as Record<string, unknown>)?.name as string ||
387
+ (currentTable as unknown as Record<string, unknown>).tableName as string ||
388
+ (currentTable as unknown as Record<string, unknown>).name as string ||
389
+ "unknown";
390
+ }
391
+
392
+ if (parentTable && typeof parentTable === "object") {
393
+ parentTableName = (parentTable as unknown as Record<string | symbol, unknown>)[Symbol.for("drizzle:Name")] as string ||
394
+ ((parentTable as unknown as Record<string, unknown>)._ as Record<string, unknown>)?.name as string ||
395
+ (parentTable as unknown as Record<string, unknown>).tableName as string ||
396
+ (parentTable as unknown as Record<string, unknown>).name as string ||
397
+ "unknown";
398
+ }
399
+
400
+ // For junction table scenarios, be more lenient with validation
401
+ // If we can't determine table names reliably, or if this looks like a junction table scenario,
402
+ // we'll allow it and let the SQL execution validate the correctness
403
+ const couldBeJunctionScenario = currentTableName.includes("_") ||
404
+ currentTableName === "unknown" ||
405
+ parentTableName === "unknown";
406
+
407
+ if (!couldBeJunctionScenario) {
408
+ throw new Error(`Join path did not result in connecting to parent table. Current: ${currentTableName}, Parent: ${parentTableName}`);
409
+ }
410
+ }
411
+
412
+ // Handle both single ID and array of IDs
413
+ const finalCondition = Array.isArray(parentId)
414
+ ? inArray(parentIdColumn, parentId)
415
+ : eq(parentIdColumn, parentId);
416
+
417
+ return {
418
+ joins,
419
+ finalCondition
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Build a single join condition between tables
425
+ */
426
+ private static buildSingleJoinCondition(
427
+ currentTable: PgTable<any>,
428
+ fromTable: PgTable<any>,
429
+ toTable: PgTable<any>,
430
+ fromColName: string,
431
+ toColName: string,
432
+ fromTableName: string,
433
+ toTableName: string,
434
+ registry?: PostgresCollectionRegistry
435
+ ): { joinTable: PgTable<any>; condition: SQL; additionalJoins?: { table: PgTable<any>; condition: SQL }[] } {
436
+ let joinTable: PgTable<any>;
437
+ let condition: SQL;
438
+ const additionalJoins: { table: PgTable<any>; condition: SQL }[] = [];
439
+
440
+ if (currentTable === toTable) {
441
+ // current -> toTable, so join the fromTable
442
+ const left = fromTable[fromColName as keyof typeof fromTable] as AnyPgColumn;
443
+ const right = (currentTable as unknown as Record<string, unknown>)[toColName] as AnyPgColumn;
444
+
445
+ if (!left || !right) {
446
+ // Check if this might be a many-to-many relationship requiring a junction table
447
+ if (registry) {
448
+ const junctionResult = this.tryBuildJunctionJoin(
449
+ currentTable,
450
+ fromTable,
451
+ fromColName,
452
+ toColName,
453
+ fromTableName,
454
+ toTableName,
455
+ registry
456
+ );
457
+ if (junctionResult) {
458
+ return junctionResult;
459
+ }
460
+ }
461
+ throw new Error(`Join columns not found: ${fromTableName}.${fromColName} = ${toTableName}.${toColName}`);
462
+ }
463
+
464
+ joinTable = fromTable;
465
+ condition = eq(left, right);
466
+ } else if (currentTable === fromTable) {
467
+ // current -> fromTable, so join the toTable
468
+ const left = toTable[toColName as keyof typeof toTable] as AnyPgColumn;
469
+ const right = (currentTable as unknown as Record<string, unknown>)[fromColName] as AnyPgColumn;
470
+
471
+ if (!left || !right) {
472
+ // Check if this might be a many-to-many relationship requiring a junction table
473
+ if (registry) {
474
+ const junctionResult = this.tryBuildJunctionJoin(
475
+ currentTable,
476
+ toTable,
477
+ fromColName,
478
+ toColName,
479
+ fromTableName,
480
+ toTableName,
481
+ registry
482
+ );
483
+ if (junctionResult) {
484
+ return junctionResult;
485
+ }
486
+ }
487
+ throw new Error(`Join columns not found: ${toTableName}.${toColName} = ${fromTableName}.${fromColName}`);
488
+ }
489
+
490
+ joinTable = toTable;
491
+ condition = eq(left, right);
492
+ } else {
493
+ throw new Error(`Join step does not match current table. Current table does not match from: ${fromTableName} or to: ${toTableName}`);
494
+ }
495
+
496
+ return {
497
+ joinTable,
498
+ condition,
499
+ additionalJoins
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Try to build a junction table join when direct foreign key relationship is not found
505
+ */
506
+ private static tryBuildJunctionJoin(
507
+ currentTable: PgTable<any>,
508
+ targetTable: PgTable<any>,
509
+ fromColName: string,
510
+ toColName: string,
511
+ fromTableName: string,
512
+ toTableName: string,
513
+ registry: PostgresCollectionRegistry
514
+ ): { joinTable: PgTable<any>; condition: SQL; additionalJoins: { table: PgTable<any>; condition: SQL }[] } | null {
515
+ // Try to find a junction table that connects these two tables
516
+ // Common naming patterns: table1_table2, table1Table2, etc.
517
+ const possibleJunctionNames = [
518
+ `${fromTableName}_${toTableName}`,
519
+ `${toTableName}_${fromTableName}`,
520
+ `${fromTableName}${toTableName.charAt(0).toUpperCase() + toTableName.slice(1)}`,
521
+ `${toTableName}${fromTableName.charAt(0).toUpperCase() + fromTableName.slice(1)}`
522
+ ];
523
+
524
+ for (const junctionName of possibleJunctionNames) {
525
+ const junctionTable = registry.getTable(junctionName);
526
+ if (junctionTable) {
527
+ // Try to find the appropriate columns in the junction table
528
+ const sourceColName = `${fromTableName.slice(0, -1)}_id`; // Remove 's' and add '_id'
529
+ const targetColName = `${toTableName.slice(0, -1)}_id`;
530
+
531
+ const junctionSourceCol = junctionTable[sourceColName as keyof typeof junctionTable] as AnyPgColumn;
532
+ const junctionTargetCol = junctionTable[targetColName as keyof typeof junctionTable] as AnyPgColumn;
533
+
534
+ if (junctionSourceCol && junctionTargetCol) {
535
+ // Found a valid junction table setup
536
+ const currentTableIdCol = Object.values(currentTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
537
+ const targetTableIdCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
538
+
539
+ if (!currentTableIdCol || !targetTableIdCol) {
540
+ continue; // Skip if we can't find primary keys
541
+ }
542
+
543
+ // Determine which direction to join
544
+ if (currentTable === targetTable) {
545
+ // We're joining through junction to reach the other table
546
+ return {
547
+ joinTable: targetTable,
548
+ condition: eq(targetTableIdCol, junctionTargetCol),
549
+ additionalJoins: [
550
+ {
551
+ table: junctionTable,
552
+ condition: eq(currentTableIdCol, junctionSourceCol)
553
+ }
554
+ ]
555
+ };
556
+ } else {
557
+ // Standard junction join
558
+ return {
559
+ joinTable: junctionTable,
560
+ condition: eq(currentTableIdCol, junctionSourceCol),
561
+ additionalJoins: [
562
+ {
563
+ table: targetTable,
564
+ condition: eq(targetTableIdCol, junctionTargetCol)
565
+ }
566
+ ]
567
+ };
568
+ }
569
+ }
570
+ }
571
+ }
572
+
573
+ return null; // No junction table found
574
+ }
575
+
576
+ /**
577
+ * Build conditions for junction table (many-to-many) relations
578
+ */
579
+ private static buildJunctionTableConditions(
580
+ through: { table: string; sourceColumn: string; targetColumn: string },
581
+ targetIdColumn: AnyPgColumn,
582
+ parentId: string | number | (string | number)[],
583
+ registry: PostgresCollectionRegistry
584
+ ): { join: { table: PgTable<any>; condition: SQL }; condition: SQL } {
585
+ const junctionTable = registry.getTable(through.table);
586
+ if (!junctionTable) {
587
+ throw new Error(`Junction table not found: ${through.table}`);
588
+ }
589
+
590
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
591
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
592
+
593
+ if (!junctionSourceCol) {
594
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
595
+ }
596
+ if (!junctionTargetCol) {
597
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
598
+ }
599
+
600
+ // Handle both single ID and array of IDs
601
+ const condition = Array.isArray(parentId)
602
+ ? inArray(junctionSourceCol, parentId)
603
+ : eq(junctionSourceCol, parentId);
604
+
605
+ return {
606
+ join: {
607
+ table: junctionTable,
608
+ condition: eq(targetIdColumn, junctionTargetCol)
609
+ },
610
+ condition
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Build conditions for inverse junction table (many-to-many) relations
616
+ */
617
+ private static buildInverseJunctionTableConditions(
618
+ through: { table: string; sourceColumn: string; targetColumn: string },
619
+ targetIdColumn: AnyPgColumn,
620
+ parentId: string | number | (string | number)[],
621
+ registry: PostgresCollectionRegistry
622
+ ): { join: { table: PgTable<any>; condition: SQL }; condition: SQL } {
623
+ const junctionTable = registry.getTable(through.table);
624
+ if (!junctionTable) {
625
+ throw new Error(`Junction table not found: ${through.table}`);
626
+ }
627
+
628
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
629
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
630
+
631
+ if (!junctionSourceCol) {
632
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
633
+ }
634
+ if (!junctionTargetCol) {
635
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
636
+ }
637
+
638
+ // For inverse relations, the parentId (tag ID) should match the sourceColumn (tag_id)
639
+ // and we want to find target rows (posts) through the targetColumn (post_id)
640
+ const condition = Array.isArray(parentId)
641
+ ? inArray(junctionSourceCol, parentId)
642
+ : eq(junctionSourceCol, parentId);
643
+
644
+ return {
645
+ join: {
646
+ table: junctionTable,
647
+ condition: eq(targetIdColumn, junctionTargetCol)
648
+ },
649
+ condition
650
+ };
651
+ }
652
+
653
+ /**
654
+ * Build conditions for simple relations (owning/inverse without join paths)
655
+ */
656
+ private static buildSimpleRelationCondition(
657
+ relation: Relation,
658
+ targetTable: PgTable<any>,
659
+ parentTable: PgTable<any>,
660
+ parentId: string | number | (string | number)[]
661
+ ): SQL {
662
+ if (relation.direction === "owning" && relation.localKey) {
663
+ // For owning relations, the parentId is actually the foreign key value
664
+ // that should match the target table's primary key
665
+ const targetIdCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.primary) as AnyPgColumn;
666
+ if (!targetIdCol) {
667
+ // Fallback to looking for an "id" column by name
668
+ const idCol = Object.values(targetTable).find((col: Record<string, unknown>) => col.name === "id") as AnyPgColumn;
669
+ if (!idCol) {
670
+ throw new Error("No primary key or \"id\" column found in target table");
671
+ }
672
+ return Array.isArray(parentId)
673
+ ? inArray(idCol, parentId)
674
+ : eq(idCol, parentId);
675
+ }
676
+ return Array.isArray(parentId)
677
+ ? inArray(targetIdCol, parentId)
678
+ : eq(targetIdCol, parentId);
679
+
680
+ } else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
681
+ // Inverse relation: use foreign key on target table
682
+ const foreignKeyCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
683
+ if (!foreignKeyCol) {
684
+ // This could be a many-to-many relationship where foreignKeyOnTarget was set by sanitizeRelation
685
+ // but the column doesn't actually exist. In this case, we should suggest using junction tables.
686
+ 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.`);
687
+ }
688
+ return Array.isArray(parentId)
689
+ ? inArray(foreignKeyCol, parentId)
690
+ : eq(foreignKeyCol, parentId);
691
+
692
+ } else if (relation.direction === "inverse" && relation.cardinality === "many" && relation.inverseRelationName) {
693
+ // For inverse many-to-many relations, this should not be called directly
694
+ // The buildRelationConditions method should handle finding the junction table
695
+ // If we reach here, it means the junction table lookup failed
696
+ 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.`);
697
+
698
+ } else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) {
699
+ // Auto-infer foreign key column for inverse one-to-one relations
700
+ // Pattern: {inverseRelationName}_id (e.g., "author" -> "author_id")
701
+ const inferredForeignKeyName = `${relation.inverseRelationName}_id`;
702
+ const foreignKeyCol = targetTable[inferredForeignKeyName as keyof typeof targetTable] as AnyPgColumn;
703
+
704
+ if (!foreignKeyCol) {
705
+ throw new Error(`Auto-inferred foreign key column '${inferredForeignKeyName}' not found in target table for inverse relation '${relation.relationName}'. Please specify 'foreignKeyOnTarget' explicitly.`);
706
+ }
707
+
708
+ console.debug(`🔍 [DrizzleConditionBuilder] Auto-inferred foreign key '${inferredForeignKeyName}' for inverse relation '${relation.relationName}'`);
709
+
710
+ return Array.isArray(parentId)
711
+ ? inArray(foreignKeyCol, parentId)
712
+ : eq(foreignKeyCol, parentId);
713
+
714
+ } else {
715
+ throw new Error(`Relation '${relation.relationName}' lacks proper configuration. For many-to-many relations, use 'through' property. For simple relations, use 'localKey' or 'foreignKeyOnTarget'.`);
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Combine multiple conditions with AND operator
721
+ */
722
+ static combineConditionsWithAnd(conditions: SQL[]): SQL | undefined {
723
+ if (conditions.length === 0) return undefined;
724
+ if (conditions.length === 1) return conditions[0];
725
+ return and(...conditions);
726
+ }
727
+
728
+ /**
729
+ * Combine multiple conditions with OR operator
730
+ */
731
+ static combineConditionsWithOr(conditions: SQL[]): SQL | undefined {
732
+ if (conditions.length === 0) return undefined;
733
+ if (conditions.length === 1) return conditions[0];
734
+ return or(...conditions);
735
+ }
736
+
737
+ /**
738
+ * Build search conditions for text fields
739
+ */
740
+ static buildSearchConditions(
741
+ searchString: string,
742
+ properties: Record<string, unknown>,
743
+ table: PgTable<any>
744
+ ): SQL[] {
745
+ const searchConditions: SQL[] = [];
746
+
747
+ for (const [key, prop] of Object.entries(properties)) {
748
+ const p = prop as Record<string, unknown>;
749
+ // Only include string properties that don't have enum defined
750
+ // PostgreSQL enum and uuid columns don't support ILIKE, so we skip them
751
+ if (p.type === "string" && !p.enum && p.isId !== "uuid") {
752
+ const fieldColumn = table[key as keyof typeof table] as AnyPgColumn;
753
+ if (fieldColumn) {
754
+ // Verify that the underlying database column supports string pattern-matching
755
+ const supportsILike =
756
+ fieldColumn instanceof PgVarchar ||
757
+ fieldColumn instanceof PgText ||
758
+ fieldColumn instanceof PgChar ||
759
+ (fieldColumn && typeof fieldColumn === "object" && !("columnType" in fieldColumn));
760
+ if (supportsILike) {
761
+ searchConditions.push(ilike(fieldColumn, `%${searchString}%`));
762
+ }
763
+ }
764
+ }
765
+ }
766
+
767
+ return searchConditions;
768
+ }
769
+
770
+ /**
771
+ * Build a unique field check condition
772
+ */
773
+ static buildUniqueFieldCondition(
774
+ fieldColumn: AnyPgColumn,
775
+ value: unknown,
776
+ idColumn?: AnyPgColumn,
777
+ excludeId?: string | number
778
+ ): SQL[] {
779
+ const conditions: SQL[] = [eq(fieldColumn, value)];
780
+
781
+ if (excludeId && idColumn) {
782
+ conditions.push(sql`${idColumn} != ${excludeId}`);
783
+ }
784
+
785
+ return conditions;
786
+ }
787
+
788
+ /**
789
+ * Build relation-based query with joins and conditions
790
+ */
791
+ static buildRelationQuery<T extends DrizzleDynamicQuery>(
792
+ baseQuery: T,
793
+ relation: Relation,
794
+ parentId: string | number | (string | number)[],
795
+ targetTable: PgTable<any>,
796
+ parentTable: PgTable<any>,
797
+ parentIdColumn: AnyPgColumn,
798
+ targetIdColumn: AnyPgColumn,
799
+ registry: PostgresCollectionRegistry,
800
+ additionalFilters?: SQL[]
801
+ ): T {
802
+ const { joinConditions, whereConditions } = this.buildRelationConditions(
803
+ relation,
804
+ parentId,
805
+ targetTable,
806
+ parentTable,
807
+ parentIdColumn,
808
+ targetIdColumn,
809
+ registry
810
+ );
811
+
812
+ let query = baseQuery;
813
+
814
+ // Apply joins
815
+ for (const { table, condition } of joinConditions) {
816
+ query = query.innerJoin(table, condition);
817
+ }
818
+
819
+ // Combine all conditions
820
+ const allConditions = [...whereConditions];
821
+ if (additionalFilters) {
822
+ allConditions.push(...additionalFilters);
823
+ }
824
+
825
+ // Apply where conditions
826
+ if (allConditions.length > 0) {
827
+ query = query.where(and(...allConditions));
828
+ }
829
+
830
+ return query;
831
+ }
832
+
833
+ /**
834
+ * Build count query for relations with proper joins and conditions
835
+ */
836
+ static buildRelationCountQuery<T extends DrizzleDynamicQuery>(
837
+ baseCountQuery: T,
838
+ relation: Relation,
839
+ parentId: string | number,
840
+ targetTable: PgTable<any>,
841
+ parentTable: PgTable<any>,
842
+ parentIdColumn: AnyPgColumn,
843
+ targetIdColumn: AnyPgColumn,
844
+ registry: PostgresCollectionRegistry,
845
+ additionalFilters?: SQL[]
846
+ ): T {
847
+ // For count queries, we need to handle joins differently to avoid duplicates
848
+ if (relation.joinPath && relation.joinPath.length > 0) {
849
+ return this.buildJoinPathCountQuery(
850
+ baseCountQuery,
851
+ relation.joinPath,
852
+ targetTable,
853
+ parentTable,
854
+ parentIdColumn,
855
+ parentId,
856
+ registry,
857
+ additionalFilters
858
+ );
859
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
860
+ return this.buildJunctionCountQuery(
861
+ baseCountQuery,
862
+ relation.through,
863
+ targetIdColumn,
864
+ parentId,
865
+ registry,
866
+ additionalFilters
867
+ );
868
+ } else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") {
869
+ return this.buildInverseJunctionCountQuery(
870
+ baseCountQuery,
871
+ relation.through,
872
+ targetIdColumn,
873
+ parentId,
874
+ registry,
875
+ additionalFilters
876
+ );
877
+ } else {
878
+ // Simple relations
879
+ const simpleCondition = this.buildSimpleRelationCondition(
880
+ relation,
881
+ targetTable,
882
+ parentTable,
883
+ parentId
884
+ );
885
+
886
+ const allConditions = [simpleCondition];
887
+ if (additionalFilters) {
888
+ allConditions.push(...additionalFilters);
889
+ }
890
+
891
+ return baseCountQuery.where(and(...allConditions));
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Build join path conditions for count queries
897
+ */
898
+ private static buildJoinPathCountQuery<T extends DrizzleDynamicQuery>(
899
+ baseCountQuery: T,
900
+ joinPath: JoinStep[],
901
+ targetTable: PgTable<any>,
902
+ parentTable: PgTable<any>,
903
+ parentIdColumn: AnyPgColumn,
904
+ parentId: string | number,
905
+ registry: PostgresCollectionRegistry,
906
+ additionalFilters?: SQL[]
907
+ ): T {
908
+ let query = baseCountQuery;
909
+ let currentTable = targetTable;
910
+
911
+ // Process join steps in reverse order
912
+ for (const joinStep of [...joinPath].reverse()) {
913
+ const fromTableName = this.getTableNamesFromColumns(joinStep.on.from)[0];
914
+ const toTableName = this.getTableNamesFromColumns(joinStep.on.to)[0];
915
+ const fromColName = this.getColumnNamesFromColumns(joinStep.on.from)[0];
916
+ const toColName = this.getColumnNamesFromColumns(joinStep.on.to)[0];
917
+
918
+ const fromTable = registry.getTable(fromTableName);
919
+ const toTable = registry.getTable(toTableName);
920
+
921
+ if (!fromTable || !toTable) {
922
+ throw new Error(`Join tables not found for step: from ${fromTableName} to ${toTableName}`);
923
+ }
924
+
925
+ const { joinTable, condition } = this.buildSingleJoinCondition(
926
+ currentTable,
927
+ fromTable,
928
+ toTable,
929
+ fromColName,
930
+ toColName,
931
+ fromTableName,
932
+ toTableName
933
+ );
934
+
935
+ query = query.innerJoin(joinTable, condition);
936
+ currentTable = joinTable;
937
+ }
938
+
939
+ if (currentTable !== parentTable) {
940
+ throw new Error("Join path did not result in connecting to parent table");
941
+ }
942
+
943
+ const allConditions = [eq(parentIdColumn, parentId)];
944
+ if (additionalFilters) {
945
+ allConditions.push(...additionalFilters);
946
+ }
947
+
948
+ return query.where(and(...allConditions));
949
+ }
950
+
951
+ /**
952
+ * Build junction table conditions for count queries
953
+ */
954
+ private static buildJunctionCountQuery<T extends DrizzleDynamicQuery>(
955
+ baseCountQuery: T,
956
+ through: { table: string; sourceColumn: string; targetColumn: string },
957
+ targetIdColumn: AnyPgColumn,
958
+ parentId: string | number,
959
+ registry: PostgresCollectionRegistry,
960
+ additionalFilters?: SQL[]
961
+ ): T {
962
+ const junctionTable = registry.getTable(through.table);
963
+ if (!junctionTable) {
964
+ throw new Error(`Junction table not found: ${through.table}`);
965
+ }
966
+
967
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
968
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
969
+
970
+ if (!junctionSourceCol) {
971
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
972
+ }
973
+ if (!junctionTargetCol) {
974
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
975
+ }
976
+
977
+ const baseConditions = [eq(junctionSourceCol, parentId)];
978
+ if (additionalFilters && additionalFilters.length > 0) {
979
+ baseConditions.push(...additionalFilters);
980
+ }
981
+
982
+ return baseCountQuery
983
+ .innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol))
984
+ .where(and(...baseConditions));
985
+ }
986
+
987
+ /**
988
+ * Build inverse junction table conditions for count queries
989
+ */
990
+ private static buildInverseJunctionCountQuery<T extends DrizzleDynamicQuery>(
991
+ baseCountQuery: T,
992
+ through: { table: string; sourceColumn: string; targetColumn: string },
993
+ targetIdColumn: AnyPgColumn,
994
+ parentId: string | number,
995
+ registry: PostgresCollectionRegistry,
996
+ additionalFilters?: SQL[]
997
+ ): T {
998
+ const junctionTable = registry.getTable(through.table);
999
+ if (!junctionTable) {
1000
+ throw new Error(`Junction table not found: ${through.table}`);
1001
+ }
1002
+
1003
+ const junctionSourceCol = junctionTable[through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
1004
+ const junctionTargetCol = junctionTable[through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
1005
+
1006
+ if (!junctionSourceCol) {
1007
+ throw new Error(`Source column '${through.sourceColumn}' not found in junction table '${through.table}'`);
1008
+ }
1009
+ if (!junctionTargetCol) {
1010
+ throw new Error(`Target column '${through.targetColumn}' not found in junction table '${through.table}'`);
1011
+ }
1012
+
1013
+ const baseConditions = [eq(junctionSourceCol, parentId)];
1014
+ if (additionalFilters && additionalFilters.length > 0) {
1015
+ baseConditions.push(...additionalFilters);
1016
+ }
1017
+
1018
+ return baseCountQuery
1019
+ .innerJoin(junctionTable, eq(targetIdColumn, junctionTargetCol))
1020
+ .where(and(...baseConditions));
1021
+ }
1022
+
1023
+ /**
1024
+ * Helper method to extract table names from columns
1025
+ */
1026
+ static getTableNamesFromColumns(columns: string | string[]): string[] {
1027
+ if (Array.isArray(columns)) {
1028
+ return columns.map(col => col.includes(".") ? col.split(".")[0] : "");
1029
+ }
1030
+ return [columns.includes(".") ? columns.split(".")[0] : ""];
1031
+ }
1032
+
1033
+ /**
1034
+ * Helper method to extract column names from columns
1035
+ */
1036
+ static getColumnNamesFromColumns(columns: string | string[]): string[] {
1037
+ if (Array.isArray(columns)) {
1038
+ return columns.map(col => getColumnName(col));
1039
+ }
1040
+ return [getColumnName(columns)];
1041
+ }
1042
+
1043
+ /**
1044
+ * Find the corresponding junction table for an inverse many-to-many relation
1045
+ */
1046
+ private static findCorrespondingJunctionTable(
1047
+ relation: Relation,
1048
+ registry: PostgresCollectionRegistry
1049
+ ): { table: string; sourceColumn: string; targetColumn: string } | null {
1050
+ try {
1051
+ console.debug(`🔍 [findCorrespondingJunctionTable] Looking for junction table for inverse relation '${relation.relationName}' with inverseRelationName '${relation.inverseRelationName}'`);
1052
+
1053
+ if (!relation.inverseRelationName) {
1054
+ console.debug("🔍 [findCorrespondingJunctionTable] No inverseRelationName specified");
1055
+ return null;
1056
+ }
1057
+
1058
+ // Get the target collection of the inverse relation
1059
+ const targetCollection = relation.target();
1060
+ console.debug(`🔍 [findCorrespondingJunctionTable] Target collection: ${targetCollection.slug}`);
1061
+
1062
+ // Find the corresponding owning relation on the target collection
1063
+ const targetCollectionRelations = resolveCollectionRelations(targetCollection);
1064
+ console.debug("🔍 [findCorrespondingJunctionTable] Target collection relations:", Object.keys(targetCollectionRelations));
1065
+
1066
+ // Look for the owning many-to-many relation that matches our inverseRelationName
1067
+ const correspondingRelation = targetCollectionRelations[relation.inverseRelationName];
1068
+
1069
+ if (!correspondingRelation) {
1070
+ console.debug(`🔍 [findCorrespondingJunctionTable] No relation found with key '${relation.inverseRelationName}' on target collection`);
1071
+ return null;
1072
+ }
1073
+
1074
+ console.debug("🔍 [findCorrespondingJunctionTable] Found relation:", {
1075
+ relationName: correspondingRelation.relationName,
1076
+ cardinality: correspondingRelation.cardinality,
1077
+ direction: correspondingRelation.direction,
1078
+ hasThrough: !!correspondingRelation.through
1079
+ });
1080
+
1081
+ // Verify it's an owning many-to-many relation with junction table
1082
+ if (correspondingRelation.cardinality !== "many" ||
1083
+ correspondingRelation.direction !== "owning" ||
1084
+ !correspondingRelation.through) {
1085
+ console.debug("🔍 [findCorrespondingJunctionTable] Relation is not an owning many-to-many with junction table");
1086
+ return null;
1087
+ }
1088
+
1089
+ console.debug("🔍 [findCorrespondingJunctionTable] Found matching owning relation with junction table!");
1090
+
1091
+ // For inverse relation, we need to swap source and target columns
1092
+ const through = correspondingRelation.through;
1093
+ const result = {
1094
+ table: through.table,
1095
+ sourceColumn: through.targetColumn, // Swapped for inverse relation
1096
+ targetColumn: through.sourceColumn // Swapped for inverse relation
1097
+ };
1098
+
1099
+ console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
1100
+ return result;
1101
+ } catch (error) {
1102
+ logger.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}'`, { error: error });
1103
+ return null;
1104
+ }
1105
+ }
1106
+
1107
+ /**
1108
+ * Build vector similarity search expressions for pgvector.
1109
+ *
1110
+ * Returns:
1111
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
1112
+ * - `filter`: optional WHERE clause for distance threshold
1113
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
1114
+ */
1115
+ static buildVectorSearchConditions(
1116
+ table: PgTable<any>,
1117
+ vectorSearch: {
1118
+ property: string;
1119
+ vector: number[];
1120
+ distance?: "cosine" | "l2" | "inner_product";
1121
+ threshold?: number;
1122
+ }
1123
+ ): { orderBy: SQL; filter?: SQL; distanceSelect: SQL } {
1124
+ const column = table[vectorSearch.property as keyof typeof table] as AnyPgColumn;
1125
+ if (!column) {
1126
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
1127
+ }
1128
+
1129
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
1130
+ const distanceFn = vectorSearch.distance || "cosine";
1131
+
1132
+ let operator: string;
1133
+ switch (distanceFn) {
1134
+ case "cosine":
1135
+ operator = "<=>";
1136
+ break;
1137
+ case "l2":
1138
+ operator = "<->";
1139
+ break;
1140
+ case "inner_product":
1141
+ operator = "<#>";
1142
+ break;
1143
+ }
1144
+
1145
+ const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
1146
+
1147
+ return {
1148
+ orderBy: distanceExpr,
1149
+ filter: vectorSearch.threshold != null
1150
+ ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}`
1151
+ : undefined,
1152
+ distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
1153
+ };
1154
+ }
1155
+ }
1156
+
1157
+ /**
1158
+ * Alias for DrizzleConditionBuilder for consistent naming with other database implementations.
1159
+ * This allows code to use PostgresConditionBuilder alongside future MongoConditionBuilder, etc.
1160
+ */
1161
+ export const PostgresConditionBuilder = DrizzleConditionBuilder;
1162
+