@rebasepro/server-postgres 0.10.1-canary.b1e3dbf → 0.10.1-canary.ed8caed
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/chunk-DSJWtz9O.js +40 -0
- package/dist/ensure-collection-tables-BVvtkkRm.js +304 -0
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.es.js +499 -4644
- 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 +30 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/src-CBgtrPhJ.js +336 -0
- package/dist/src-CBgtrPhJ.js.map +1 -0
- package/dist/src-lcfUP4xg.js +4106 -0
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +40 -0
- package/src/schema/doctor.ts +6 -6
- 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 +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +22 -15
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/nested-path.ts +130 -0
- package/src/utils/drizzle-conditions.ts +143 -0
|
@@ -38,6 +38,149 @@ export interface DrizzleDynamicQuery {
|
|
|
38
38
|
*/
|
|
39
39
|
export class DrizzleConditionBuilder {
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Express "reachable from this parent through this relation" as a plain
|
|
43
|
+
* `WHERE` condition on the target table.
|
|
44
|
+
*
|
|
45
|
+
* This is the primitive that lets a relation be a *filter* rather than an
|
|
46
|
+
* addressing scheme. A nested listing used to be served by its own query
|
|
47
|
+
* builder — `fetchEntitiesUsingJoins`, which grew joins the root pipeline
|
|
48
|
+
* did not have and lost the options the root pipeline did have (offset,
|
|
49
|
+
* filter, orderBy, include). Reduced to a condition, the same listing runs
|
|
50
|
+
* through the ordinary collection query, so it inherits all of them and
|
|
51
|
+
* there is one read path instead of two.
|
|
52
|
+
*
|
|
53
|
+
* The shapes:
|
|
54
|
+
* - inverse FK → `target.<fk> = :parentId`, a column comparison.
|
|
55
|
+
* - `through` → `EXISTS (SELECT 1 FROM junction …)`, correlated on the
|
|
56
|
+
* target's key, so the junction never multiplies rows the
|
|
57
|
+
* way an `INNER JOIN` would.
|
|
58
|
+
* - `joinPath` → the same `EXISTS`, with the path's steps joined inside
|
|
59
|
+
* it and the final step correlating to the outer row.
|
|
60
|
+
*/
|
|
61
|
+
static buildRelationScopeCondition(
|
|
62
|
+
relation: Relation,
|
|
63
|
+
/**
|
|
64
|
+
* Lazy: only `joinPath` and `localKey` relations need the parent's own
|
|
65
|
+
* table. An inverse foreign key and a junction are both expressible
|
|
66
|
+
* from the parent's *id* alone, and requiring the table for them would
|
|
67
|
+
* make a child listing fail on a parent whose table isn't registered.
|
|
68
|
+
*/
|
|
69
|
+
parent: () => { table: PgTable<any>; idColumn: AnyPgColumn },
|
|
70
|
+
parentId: string | number,
|
|
71
|
+
targetTable: PgTable<any>,
|
|
72
|
+
targetIdColumn: AnyPgColumn,
|
|
73
|
+
registry: PostgresCollectionRegistry
|
|
74
|
+
): SQL {
|
|
75
|
+
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
76
|
+
const { table, idColumn } = parent();
|
|
77
|
+
return this.buildJoinPathScopeCondition(
|
|
78
|
+
relation.joinPath, table, idColumn, parentId, targetIdColumn, registry
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (relation.through) {
|
|
83
|
+
const junctionTable = registry.getTable(relation.through.table);
|
|
84
|
+
if (!junctionTable) {
|
|
85
|
+
throw new Error(`Junction table not found: ${relation.through.table}`);
|
|
86
|
+
}
|
|
87
|
+
const sourceCol = junctionTable[relation.through.sourceColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
88
|
+
const targetCol = junctionTable[relation.through.targetColumn as keyof typeof junctionTable] as AnyPgColumn;
|
|
89
|
+
if (!sourceCol || !targetCol) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Junction columns '${relation.through.sourceColumn}'/'${relation.through.targetColumn}' not found in '${relation.through.table}'`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (relation.foreignKeyOnTarget) {
|
|
98
|
+
const fkColumn = targetTable[relation.foreignKeyOnTarget as keyof typeof targetTable] as AnyPgColumn;
|
|
99
|
+
if (!fkColumn) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation ` +
|
|
102
|
+
`'${relation.relationName}'. A many-to-many relation needs \`through\` instead.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return eq(fkColumn, parentId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (relation.localKey) {
|
|
109
|
+
// A to-one owning relation read as a scope: the single target row is
|
|
110
|
+
// the one the parent's foreign key points at.
|
|
111
|
+
const { table, idColumn } = parent();
|
|
112
|
+
return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Relation '${relation.relationName}' declares no \`foreignKeyOnTarget\`, \`through\`, \`joinPath\` or ` +
|
|
117
|
+
"`localKey`, so there is no way to tell which target rows belong to a parent."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `EXISTS` for an explicit `joinPath`.
|
|
123
|
+
*
|
|
124
|
+
* The path is declared source → target. The subquery replays every step but
|
|
125
|
+
* the last from inside, and turns the last one into the correlation with the
|
|
126
|
+
* outer target row — so the target table is never named twice and needs no
|
|
127
|
+
* alias. Each intermediate table is aliased positionally, which keeps a path
|
|
128
|
+
* that revisits a table (a self-referencing many-to-many) unambiguous.
|
|
129
|
+
*/
|
|
130
|
+
private static buildJoinPathScopeCondition(
|
|
131
|
+
joinPath: JoinStep[],
|
|
132
|
+
parentTable: PgTable<any>,
|
|
133
|
+
parentIdColumn: AnyPgColumn,
|
|
134
|
+
parentId: string | number,
|
|
135
|
+
targetIdColumn: AnyPgColumn,
|
|
136
|
+
registry: PostgresCollectionRegistry
|
|
137
|
+
): SQL {
|
|
138
|
+
const sourceAlias = "__rel_src";
|
|
139
|
+
const aliasFor = (index: number) => `__rel_j${index}`;
|
|
140
|
+
|
|
141
|
+
// Column reference against the previous hop: the aliased source for the
|
|
142
|
+
// first step, the previous aliased join table after that.
|
|
143
|
+
const fromRef = (stepIndex: number, column: string) =>
|
|
144
|
+
sql`${sql.identifier(stepIndex === 0 ? sourceAlias : aliasFor(stepIndex - 1))}.${sql.identifier(getColumnName(column))}`;
|
|
145
|
+
|
|
146
|
+
const pairs = (step: JoinStep): { from: string; to: string }[] => {
|
|
147
|
+
const from = Array.isArray(step.on.from) ? step.on.from : [step.on.from];
|
|
148
|
+
const to = Array.isArray(step.on.to) ? step.on.to : [step.on.to];
|
|
149
|
+
if (from.length !== to.length) {
|
|
150
|
+
throw new Error(`Join step on '${step.table}' has ${from.length} \`from\` columns and ${to.length} \`to\` columns`);
|
|
151
|
+
}
|
|
152
|
+
return from.map((f, i) => ({ from: f, to: to[i] }));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const inner = joinPath.slice(0, -1);
|
|
156
|
+
const last = joinPath[joinPath.length - 1];
|
|
157
|
+
|
|
158
|
+
const joins: SQL[] = inner.map((step, index) => {
|
|
159
|
+
const table = registry.getTable(step.table);
|
|
160
|
+
if (!table) throw new Error(`Join table not found: ${step.table}`);
|
|
161
|
+
const on = pairs(step).map(({ from, to }) =>
|
|
162
|
+
sql`${fromRef(index, from)} = ${sql.identifier(aliasFor(index))}.${sql.identifier(getColumnName(to))}`
|
|
163
|
+
);
|
|
164
|
+
return sql`JOIN ${table} AS ${sql.identifier(aliasFor(index))} ON ${sql.join(on, sql` AND `)}`;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The last step correlates to the outer row instead of joining the
|
|
168
|
+
// target table into the subquery.
|
|
169
|
+
const lastPairs = pairs(last);
|
|
170
|
+
const correlation = lastPairs.length === 1
|
|
171
|
+
? sql`${fromRef(inner.length, lastPairs[0].from)} = ${targetIdColumn}`
|
|
172
|
+
: sql.join(
|
|
173
|
+
lastPairs.map(({ from, to }) =>
|
|
174
|
+
sql`${fromRef(inner.length, from)} = ${sql.identifier(getColumnName(to))}`
|
|
175
|
+
),
|
|
176
|
+
sql` AND `
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const joinsSql = joins.length > 0 ? sql` ${sql.join(joins, sql` `)}` : sql``;
|
|
180
|
+
|
|
181
|
+
return sql`EXISTS (SELECT 1 FROM ${parentTable} AS ${sql.identifier(sourceAlias)}${joinsSql} WHERE ${sql.identifier(sourceAlias)}.${sql.identifier(parentIdColumn.name)} = ${parentId} AND ${correlation})`;
|
|
182
|
+
}
|
|
183
|
+
|
|
41
184
|
/**
|
|
42
185
|
* Build filter conditions from FilterValues
|
|
43
186
|
*/
|