@rebasepro/server-postgres 0.10.1-canary.6f89f77 → 0.10.1-canary.7801eed
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/dist/PostgresBootstrapper.d.ts +7 -3
- package/dist/auth/schema-version.d.ts +106 -0
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/collections/validate-relations.d.ts +53 -0
- package/dist/data-transformer.d.ts +3 -3
- package/dist/ensure-collection-tables-DGMYK0fr.js +304 -0
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1853 -4900
- package/dist/index.es.js.map +1 -1
- package/dist/schema/ensure-collection-tables.d.ts +79 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
- package/dist/services/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +39 -8
- package/dist/services/cdc/CdcListener.d.ts +7 -14
- package/dist/services/cdc/junction-tables.d.ts +38 -0
- package/dist/services/channel-bus/ChannelBus.d.ts +29 -0
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +111 -0
- package/dist/services/channel-bus/index.d.ts +55 -0
- package/dist/services/channel-history.d.ts +11 -0
- package/dist/services/channel-presence.d.ts +66 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/services/pg-notify-listener.d.ts +47 -0
- package/dist/services/realtimeService.d.ts +133 -6
- package/dist/services/row-pipeline.d.ts +2 -2
- package/dist/src-3VmUJ8Xn.js +3994 -0
- package/dist/src-3VmUJ8Xn.js.map +1 -0
- package/dist/src-D5xBTl32.js +346 -0
- package/dist/src-D5xBTl32.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +71 -18
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +87 -5
- package/src/auth/ensure-tables.ts +23 -0
- package/src/auth/schema-version.ts +260 -0
- package/src/cli-errors.ts +1 -1
- package/src/cli-helpers.ts +4 -3
- package/src/collections/PostgresCollectionRegistry.ts +9 -4
- package/src/collections/buildRegistry.ts +7 -0
- package/src/collections/validate-relations.ts +280 -0
- package/src/data-transformer.ts +28 -38
- package/src/index.ts +4 -0
- package/src/schema/doctor.ts +14 -14
- package/src/schema/ensure-collection-tables.test.ts +156 -0
- package/src/schema/ensure-collection-tables.ts +297 -0
- package/src/schema/generate-drizzle-schema-logic.ts +62 -110
- package/src/schema/generate-postgres-ddl-logic.ts +31 -24
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +25 -29
- package/src/services/FetchService.ts +116 -126
- package/src/services/PersistService.ts +126 -88
- package/src/services/RelationService.ts +157 -86
- package/src/services/cdc/CdcListener.ts +27 -91
- package/src/services/cdc/junction-tables.ts +91 -0
- package/src/services/channel-bus/ChannelBus.ts +44 -0
- package/src/services/channel-bus/PostgresChannelBus.ts +299 -0
- package/src/services/channel-bus/index.ts +123 -0
- package/src/services/channel-history.ts +35 -0
- package/src/services/channel-presence.ts +148 -0
- package/src/services/nested-path.ts +145 -0
- package/src/services/pg-notify-listener.ts +137 -0
- package/src/services/realtimeService.ts +430 -11
- package/src/services/row-pipeline.ts +5 -6
- package/src/utils/drizzle-conditions.ts +268 -330
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FilterValues, WhereFilterOp, JoinStep, LogicalCondition, FilterCondition,
|
|
5
|
+
ResolvedRelation, ResolvedBelongsTo, ResolvedHasOne, ResolvedHasMany
|
|
6
|
+
} from "@rebasepro/types";
|
|
4
7
|
import { getColumnName, normalizeToEntityRelation, resolveCollectionRelations } from "@rebasepro/common";
|
|
5
8
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
9
|
import { ConditionBuilderStatic } from "../interfaces";
|
|
@@ -38,6 +41,168 @@ export interface DrizzleDynamicQuery {
|
|
|
38
41
|
*/
|
|
39
42
|
export class DrizzleConditionBuilder {
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Express "reachable from this parent through this relation" as a plain
|
|
46
|
+
* `WHERE` condition on the target table.
|
|
47
|
+
*
|
|
48
|
+
* This is the primitive that lets a relation be a *filter* rather than an
|
|
49
|
+
* addressing scheme. A nested listing used to be served by its own query
|
|
50
|
+
* builder — `fetchEntitiesUsingJoins`, which grew joins the root pipeline
|
|
51
|
+
* did not have and lost the options the root pipeline did have (offset,
|
|
52
|
+
* filter, orderBy, include). Reduced to a condition, the same listing runs
|
|
53
|
+
* through the ordinary collection query, so it inherits all of them and
|
|
54
|
+
* there is one read path instead of two.
|
|
55
|
+
*
|
|
56
|
+
* The shapes:
|
|
57
|
+
* - inverse FK → `target.<fk> = :parentId`, a column comparison.
|
|
58
|
+
* - `through` → `EXISTS (SELECT 1 FROM junction …)`, correlated on the
|
|
59
|
+
* target's key, so the junction never multiplies rows the
|
|
60
|
+
* way an `INNER JOIN` would.
|
|
61
|
+
* - `joinPath` → the same `EXISTS`, with the path's steps joined inside
|
|
62
|
+
* it and the final step correlating to the outer row.
|
|
63
|
+
*/
|
|
64
|
+
static buildRelationScopeCondition(
|
|
65
|
+
relation: ResolvedRelation,
|
|
66
|
+
/**
|
|
67
|
+
* Lazy: only `via` and `belongsTo` need the parent's own table. A
|
|
68
|
+
* foreign key on the target and a junction are both expressible from
|
|
69
|
+
* the parent's *id* alone, and requiring the table for them would make
|
|
70
|
+
* a child listing fail on a parent whose table isn't registered.
|
|
71
|
+
*/
|
|
72
|
+
parent: () => { table: PgTable<any>; idColumn: AnyPgColumn },
|
|
73
|
+
parentId: string | number,
|
|
74
|
+
targetTable: PgTable<any>,
|
|
75
|
+
targetIdColumn: AnyPgColumn,
|
|
76
|
+
registry: PostgresCollectionRegistry
|
|
77
|
+
): SQL {
|
|
78
|
+
switch (relation.kind) {
|
|
79
|
+
case "via": {
|
|
80
|
+
const { table, idColumn } = parent();
|
|
81
|
+
return this.buildJoinPathScopeCondition(
|
|
82
|
+
relation.joinPath, table, idColumn, parentId, targetTable, registry
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "manyToMany": {
|
|
87
|
+
const { table: junctionName, sourceColumn, targetColumn } = relation.through;
|
|
88
|
+
const junctionTable = registry.getTable(junctionName);
|
|
89
|
+
if (!junctionTable) {
|
|
90
|
+
throw new Error(`Junction table not found: ${junctionName}`);
|
|
91
|
+
}
|
|
92
|
+
const sourceCol = junctionTable[sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
93
|
+
const targetCol = junctionTable[targetColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
94
|
+
if (!sourceCol || !targetCol) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Junction columns '${sourceColumn}'/'${targetColumn}' not found in '${junctionName}'`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
// Correlated, not joined: a join through a junction multiplies
|
|
100
|
+
// the target rows by the number of matching links and silently
|
|
101
|
+
// breaks `limit`/`offset`.
|
|
102
|
+
return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "hasOne":
|
|
106
|
+
case "hasMany": {
|
|
107
|
+
const fkColumn = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
|
|
108
|
+
if (!fkColumn) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of ` +
|
|
111
|
+
`relation '${relation.relationName}'.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return eq(fkColumn, parentId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case "belongsTo": {
|
|
118
|
+
// The single target row the parent's foreign key points at.
|
|
119
|
+
const { table, idColumn } = parent();
|
|
120
|
+
return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
default: {
|
|
124
|
+
// Exhaustive. There is no "declares nothing" case to fall
|
|
125
|
+
// through to any more — every kind names its own link.
|
|
126
|
+
const exhaustive: never = relation;
|
|
127
|
+
throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `EXISTS` for an explicit `joinPath`.
|
|
134
|
+
*
|
|
135
|
+
* The path is declared source → target. The subquery replays every step but
|
|
136
|
+
* the last from inside, and turns the last one into the correlation with the
|
|
137
|
+
* outer target row — so the target table is never named twice and needs no
|
|
138
|
+
* alias. Each intermediate table is aliased positionally, which keeps a path
|
|
139
|
+
* that revisits a table (a self-referencing many-to-many) unambiguous.
|
|
140
|
+
*/
|
|
141
|
+
private static buildJoinPathScopeCondition(
|
|
142
|
+
joinPath: JoinStep[],
|
|
143
|
+
parentTable: PgTable<any>,
|
|
144
|
+
parentIdColumn: AnyPgColumn,
|
|
145
|
+
parentId: string | number,
|
|
146
|
+
targetTable: PgTable<any>,
|
|
147
|
+
registry: PostgresCollectionRegistry
|
|
148
|
+
): SQL {
|
|
149
|
+
const sourceAlias = "__rel_src";
|
|
150
|
+
const aliasFor = (index: number) => `__rel_j${index}`;
|
|
151
|
+
|
|
152
|
+
// Column reference against the previous hop: the aliased source for the
|
|
153
|
+
// first step, the previous aliased join table after that.
|
|
154
|
+
const fromRef = (stepIndex: number, column: string) =>
|
|
155
|
+
sql`${sql.identifier(stepIndex === 0 ? sourceAlias : aliasFor(stepIndex - 1))}.${sql.identifier(getColumnName(column))}`;
|
|
156
|
+
|
|
157
|
+
const pairs = (step: JoinStep): { from: string; to: string }[] => {
|
|
158
|
+
const from = Array.isArray(step.on.from) ? step.on.from : [step.on.from];
|
|
159
|
+
const to = Array.isArray(step.on.to) ? step.on.to : [step.on.to];
|
|
160
|
+
if (from.length !== to.length) {
|
|
161
|
+
throw new Error(`Join step on '${step.table}' has ${from.length} \`from\` columns and ${to.length} \`to\` columns`);
|
|
162
|
+
}
|
|
163
|
+
return from.map((f, i) => ({ from: f, to: to[i] }));
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const inner = joinPath.slice(0, -1);
|
|
167
|
+
const last = joinPath[joinPath.length - 1];
|
|
168
|
+
|
|
169
|
+
const joins: SQL[] = inner.map((step, index) => {
|
|
170
|
+
const table = registry.getTable(step.table);
|
|
171
|
+
if (!table) throw new Error(`Join table not found: ${step.table}`);
|
|
172
|
+
const on = pairs(step).map(({ from, to }) =>
|
|
173
|
+
sql`${fromRef(index, from)} = ${sql.identifier(aliasFor(index))}.${sql.identifier(getColumnName(to))}`
|
|
174
|
+
);
|
|
175
|
+
return sql`JOIN ${table} AS ${sql.identifier(aliasFor(index))} ON ${sql.join(on, sql` AND `)}`;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// The last step correlates to the outer row instead of joining the
|
|
179
|
+
// target table into the subquery.
|
|
180
|
+
//
|
|
181
|
+
// On the step's own `to` column, which is not always the target's
|
|
182
|
+
// primary key: a one-step path like `{ table: "posts", on: { from:
|
|
183
|
+
// "id", to: "author_id" } }` correlates through a foreign key, and
|
|
184
|
+
// matching `authors.id = posts.id` there compares two unrelated
|
|
185
|
+
// identifiers — which returns nothing, quietly. Referencing the Drizzle
|
|
186
|
+
// column rather than a bare name also keeps it qualified, so it binds
|
|
187
|
+
// to the outer target and not to a table joined inside the EXISTS.
|
|
188
|
+
const targetColumn = (name: string): AnyPgColumn => {
|
|
189
|
+
const column = targetTable[getColumnName(name) as keyof typeof targetTable] as AnyPgColumn;
|
|
190
|
+
if (!column) {
|
|
191
|
+
throw new Error(`Join step column '${name}' not found in the target table of this joinPath`);
|
|
192
|
+
}
|
|
193
|
+
return column;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const correlation = sql.join(
|
|
197
|
+
pairs(last).map(({ from, to }) => sql`${fromRef(inner.length, from)} = ${targetColumn(to)}`),
|
|
198
|
+
sql` AND `
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const joinsSql = joins.length > 0 ? sql` ${sql.join(joins, sql` `)}` : sql``;
|
|
202
|
+
|
|
203
|
+
return sql`EXISTS (SELECT 1 FROM ${parentTable} AS ${sql.identifier(sourceAlias)}${joinsSql} WHERE ${sql.identifier(sourceAlias)}.${sql.identifier(parentIdColumn.name)} = ${parentId} AND ${correlation})`;
|
|
204
|
+
}
|
|
205
|
+
|
|
41
206
|
/**
|
|
42
207
|
* Build filter conditions from FilterValues
|
|
43
208
|
*/
|
|
@@ -196,8 +361,22 @@ export class DrizzleConditionBuilder {
|
|
|
196
361
|
/**
|
|
197
362
|
* Build relation-based conditions for different relation types
|
|
198
363
|
*/
|
|
364
|
+
/**
|
|
365
|
+
* Joins and where-conditions that reach a relation's target rows.
|
|
366
|
+
*
|
|
367
|
+
* One case per kind. This used to be a chain of six `else if`s over
|
|
368
|
+
* `cardinality`/`direction`/`through`, ending in
|
|
369
|
+
* `findCorrespondingJunctionTable` — a search through the *target's* own
|
|
370
|
+
* relations to work out whether an "inverse many" was a one-to-many or the
|
|
371
|
+
* far side of a junction. That search is gone: the kind says which it is.
|
|
372
|
+
*
|
|
373
|
+
* The owning/inverse split for junctions is gone too. Both variants built
|
|
374
|
+
* the identical condition — `through` is always written from the declaring
|
|
375
|
+
* side's point of view — so the second was a distinction without a
|
|
376
|
+
* difference and one of the places the two could drift apart.
|
|
377
|
+
*/
|
|
199
378
|
static buildRelationConditions(
|
|
200
|
-
relation:
|
|
379
|
+
relation: ResolvedRelation,
|
|
201
380
|
parentId: string | number | (string | number)[],
|
|
202
381
|
targetTable: PgTable<any>,
|
|
203
382
|
parentTable: PgTable<any>,
|
|
@@ -208,110 +387,44 @@ export class DrizzleConditionBuilder {
|
|
|
208
387
|
joinConditions: { table: PgTable<any>; condition: SQL }[];
|
|
209
388
|
whereConditions: SQL[];
|
|
210
389
|
} {
|
|
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
390
|
const joinConditions: { table: PgTable<any>; condition: SQL }[] = [];
|
|
222
391
|
const whereConditions: SQL[] = [];
|
|
223
392
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
393
|
+
switch (relation.kind) {
|
|
394
|
+
case "via": {
|
|
395
|
+
const { joins, finalCondition } = this.buildJoinPathConditions(
|
|
396
|
+
relation.joinPath, targetTable, parentTable, parentIdColumn, parentId, registry
|
|
397
|
+
);
|
|
398
|
+
joinConditions.push(...joins);
|
|
399
|
+
whereConditions.push(finalCondition);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case "manyToMany": {
|
|
404
|
+
const junctionResult = this.buildJunctionTableConditions(
|
|
405
|
+
relation.through, targetIdColumn, parentId, registry
|
|
277
406
|
);
|
|
278
407
|
joinConditions.push(junctionResult.join);
|
|
279
408
|
whereConditions.push(junctionResult.condition);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
parentId
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case "hasOne":
|
|
413
|
+
case "hasMany":
|
|
414
|
+
case "belongsTo":
|
|
415
|
+
whereConditions.push(
|
|
416
|
+
this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId)
|
|
288
417
|
);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
default: {
|
|
421
|
+
const exhaustive: never = relation;
|
|
422
|
+
throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
|
|
293
423
|
}
|
|
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
424
|
}
|
|
305
425
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
whereConditionsCount: whereConditions.length
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
joinConditions,
|
|
313
|
-
whereConditions
|
|
314
|
-
};
|
|
426
|
+
return { joinConditions,
|
|
427
|
+
whereConditions };
|
|
315
428
|
}
|
|
316
429
|
|
|
317
430
|
/**
|
|
@@ -611,109 +724,47 @@ export class DrizzleConditionBuilder {
|
|
|
611
724
|
};
|
|
612
725
|
}
|
|
613
726
|
|
|
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
727
|
|
|
653
728
|
/**
|
|
654
|
-
*
|
|
729
|
+
* The condition for a relation whose link is a single column.
|
|
730
|
+
*
|
|
731
|
+
* Two cases. It had five: two of them existed only to throw ("should not be
|
|
732
|
+
* called directly", "lacks proper configuration"), and one guessed a column
|
|
733
|
+
* name by appending `_id` to `inverseRelationName` when no foreign key had
|
|
734
|
+
* been resolved. All three were reachable only because the old type let a
|
|
735
|
+
* relation arrive here under-specified. It cannot now.
|
|
655
736
|
*/
|
|
656
737
|
private static buildSimpleRelationCondition(
|
|
657
|
-
relation:
|
|
738
|
+
relation: ResolvedBelongsTo | ResolvedHasOne | ResolvedHasMany,
|
|
658
739
|
targetTable: PgTable<any>,
|
|
659
740
|
parentTable: PgTable<any>,
|
|
660
741
|
parentId: string | number | (string | number)[]
|
|
661
742
|
): SQL {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
743
|
+
const match = (column: AnyPgColumn) =>
|
|
744
|
+
Array.isArray(parentId) ? inArray(column, parentId) : eq(column, parentId);
|
|
745
|
+
|
|
746
|
+
if (relation.kind === "belongsTo") {
|
|
747
|
+
// `parentId` is the foreign key's value, matched against the
|
|
748
|
+
// target's own key.
|
|
749
|
+
const targetIdCol =
|
|
750
|
+
(Object.values(targetTable).find((col: Record<string, unknown>) => col.primary)
|
|
751
|
+
?? Object.values(targetTable).find((col: Record<string, unknown>) => col.name === "id")) as AnyPgColumn | undefined;
|
|
666
752
|
if (!targetIdCol) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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.`);
|
|
753
|
+
throw new Error(
|
|
754
|
+
`No primary key or "id" column in the target table of relation '${relation.relationName}'.`
|
|
755
|
+
);
|
|
706
756
|
}
|
|
757
|
+
return match(targetIdCol);
|
|
758
|
+
}
|
|
707
759
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
:
|
|
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'.`);
|
|
760
|
+
const foreignKeyCol = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
|
|
761
|
+
if (!foreignKeyCol) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation ` +
|
|
764
|
+
`'${relation.relationName}'. A link through a junction is \`kind: "manyToMany"\`.`
|
|
765
|
+
);
|
|
716
766
|
}
|
|
767
|
+
return match(foreignKeyCol);
|
|
717
768
|
}
|
|
718
769
|
|
|
719
770
|
/**
|
|
@@ -790,7 +841,7 @@ export class DrizzleConditionBuilder {
|
|
|
790
841
|
*/
|
|
791
842
|
static buildRelationQuery<T extends DrizzleDynamicQuery>(
|
|
792
843
|
baseQuery: T,
|
|
793
|
-
relation:
|
|
844
|
+
relation: ResolvedRelation,
|
|
794
845
|
parentId: string | number | (string | number)[],
|
|
795
846
|
targetTable: PgTable<any>,
|
|
796
847
|
parentTable: PgTable<any>,
|
|
@@ -831,11 +882,15 @@ export class DrizzleConditionBuilder {
|
|
|
831
882
|
}
|
|
832
883
|
|
|
833
884
|
/**
|
|
834
|
-
*
|
|
885
|
+
* A count over a relation's target rows.
|
|
886
|
+
*
|
|
887
|
+
* The junction case counts `distinct` because the caller's query joins the
|
|
888
|
+
* junction; the owning/inverse pair that used to sit here built the same
|
|
889
|
+
* query twice.
|
|
835
890
|
*/
|
|
836
891
|
static buildRelationCountQuery<T extends DrizzleDynamicQuery>(
|
|
837
892
|
baseCountQuery: T,
|
|
838
|
-
relation:
|
|
893
|
+
relation: ResolvedRelation,
|
|
839
894
|
parentId: string | number,
|
|
840
895
|
targetTable: PgTable<any>,
|
|
841
896
|
parentTable: PgTable<any>,
|
|
@@ -844,51 +899,32 @@ export class DrizzleConditionBuilder {
|
|
|
844
899
|
registry: PostgresCollectionRegistry,
|
|
845
900
|
additionalFilters?: SQL[]
|
|
846
901
|
): T {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
);
|
|
902
|
+
switch (relation.kind) {
|
|
903
|
+
case "via":
|
|
904
|
+
return this.buildJoinPathCountQuery(
|
|
905
|
+
baseCountQuery, relation.joinPath, targetTable, parentTable,
|
|
906
|
+
parentIdColumn, parentId, registry, additionalFilters
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
case "manyToMany":
|
|
910
|
+
return this.buildJunctionCountQuery(
|
|
911
|
+
baseCountQuery, relation.through, targetIdColumn, parentId, registry, additionalFilters
|
|
912
|
+
);
|
|
885
913
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
914
|
+
case "belongsTo":
|
|
915
|
+
case "hasOne":
|
|
916
|
+
case "hasMany": {
|
|
917
|
+
const allConditions = [
|
|
918
|
+
this.buildSimpleRelationCondition(relation, targetTable, parentTable, parentId),
|
|
919
|
+
...(additionalFilters ?? [])
|
|
920
|
+
];
|
|
921
|
+
return baseCountQuery.where(and(...allConditions));
|
|
889
922
|
}
|
|
890
923
|
|
|
891
|
-
|
|
924
|
+
default: {
|
|
925
|
+
const exhaustive: never = relation;
|
|
926
|
+
throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
|
|
927
|
+
}
|
|
892
928
|
}
|
|
893
929
|
}
|
|
894
930
|
|
|
@@ -984,41 +1020,6 @@ export class DrizzleConditionBuilder {
|
|
|
984
1020
|
.where(and(...baseConditions));
|
|
985
1021
|
}
|
|
986
1022
|
|
|
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
|
|
|
1023
1024
|
/**
|
|
1024
1025
|
* Helper method to extract table names from columns
|
|
@@ -1040,69 +1041,6 @@ export class DrizzleConditionBuilder {
|
|
|
1040
1041
|
return [getColumnName(columns)];
|
|
1041
1042
|
}
|
|
1042
1043
|
|
|
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
1044
|
|
|
1107
1045
|
/**
|
|
1108
1046
|
* Build vector similarity search expressions for pgvector.
|