@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.
- package/LICENSE +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +11298 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +11306 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +192 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +40 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +92 -0
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +1008 -0
- package/src/PostgresBootstrapper.ts +231 -0
- package/src/auth/ensure-tables.ts +381 -0
- package/src/auth/services.ts +799 -0
- package/src/cli.ts +648 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +84 -0
- package/src/data-transformer.ts +608 -0
- package/src/databasePoolManager.ts +85 -0
- package/src/history/HistoryService.ts +248 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +169 -0
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +765 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/schema/introspect-db-logic.ts +542 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1576 -0
- package/src/services/EntityPersistService.ts +349 -0
- package/src/services/RelationService.ts +1274 -0
- package/src/services/entity-helpers.ts +147 -0
- package/src/services/entityService.ts +211 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1034 -0
- package/src/utils/drizzle-conditions.ts +1000 -0
- package/src/websocket.ts +518 -0
- package/test/auth-services.test.ts +661 -0
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +367 -0
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +367 -0
- package/test/entityService.relations.test.ts +1008 -0
- package/test/entityService.subcollection-search.test.ts +566 -0
- package/test/entityService.test.ts +1035 -0
- package/test/generate-drizzle-schema.test.ts +988 -0
- package/test/historyService.test.ts +141 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +389 -0
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +648 -0
- package/test/realtimeService.test.ts +307 -0
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +1115 -0
- package/test/unmapped-tables-safety.test.ts +345 -0
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +32 -0
- package/test_hash.js +14 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- 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;
|