@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95
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/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +33 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +1234 -9753
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +40 -2
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +24 -1
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +4 -2
- package/dist/{src-3VmUJ8Xn.js → src-CzbghKwf.js} +464 -187
- package/dist/src-CzbghKwf.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +162 -7
- package/dist/utils/pg-error-utils.d.ts +25 -3
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +212 -36
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/buildRegistry.ts +1 -1
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +33 -8
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +32 -10
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/FetchService.ts +148 -18
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +40 -1
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +4 -2
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +539 -50
- package/src/utils/pg-error-utils.ts +98 -3
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -1,7 +1,54 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { FilterValues, WhereFilterOp, LogicalCondition, FilterCondition, ResolvedRelation } from "@rebasepro/types";
|
|
3
|
+
import { CollectionConfig, FilterValues, WhereFilterOp, LogicalCondition, FilterCondition, ResolvedRelation, ResolvedForeignKeyOnTarget, ResolvedManyToMany } from "@rebasepro/types";
|
|
4
4
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
5
|
+
/**
|
|
6
|
+
* What to do with a filter field that resolves to no column at all.
|
|
7
|
+
*
|
|
8
|
+
* - `"error"` (default) — reject the request. A filter that cannot be
|
|
9
|
+
* compiled is *dropped*, and dropping a condition can only ever widen the
|
|
10
|
+
* result set. On a data plane where row-level security is the last line of
|
|
11
|
+
* defence, a typo'd or renamed filter key therefore runs the query without
|
|
12
|
+
* that condition and returns everything RLS happens to allow.
|
|
13
|
+
* - `"warn"` — the historical behaviour: log and silently drop the condition.
|
|
14
|
+
* Only for a deployment that knowingly sends filter keys the table does not
|
|
15
|
+
* have and has satisfied itself that widening is safe there.
|
|
16
|
+
*/
|
|
17
|
+
export type UnknownFilterFieldsMode = "error" | "warn";
|
|
18
|
+
/** Set the process-wide behaviour for unresolvable filter fields. */
|
|
19
|
+
export declare function configureUnknownFilterFields(mode: UnknownFilterFieldsMode): void;
|
|
20
|
+
/** The process-wide behaviour for unresolvable filter fields. */
|
|
21
|
+
export declare function getUnknownFilterFieldsMode(): UnknownFilterFieldsMode;
|
|
22
|
+
/** Per-call context for compiling a filter into SQL. */
|
|
23
|
+
export interface FilterCompilationOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Overrides the process-wide {@link UnknownFilterFieldsMode} for this call.
|
|
26
|
+
*/
|
|
27
|
+
unknownFields?: UnknownFilterFieldsMode;
|
|
28
|
+
/**
|
|
29
|
+
* The collection the filter is written against. Its resolved relations are
|
|
30
|
+
* what turn an owning-relation filter key into the foreign-key column it
|
|
31
|
+
* actually lives in; without it only the default key shapes can be guessed.
|
|
32
|
+
*/
|
|
33
|
+
collection?: CollectionConfig;
|
|
34
|
+
/**
|
|
35
|
+
* The driver's registry, for relations whose link is not on this row at
|
|
36
|
+
* all. A `manyToMany` compiles to an `EXISTS` over its junction and a
|
|
37
|
+
* `hasMany`/`hasOne` to one over the target table — neither of which this
|
|
38
|
+
* builder can reach from the collection alone.
|
|
39
|
+
*/
|
|
40
|
+
registry?: PostgresCollectionRegistry;
|
|
41
|
+
/**
|
|
42
|
+
* The key column of the table being filtered — what those `EXISTS`
|
|
43
|
+
* subqueries correlate back to.
|
|
44
|
+
*
|
|
45
|
+
* It has to be the Drizzle column object rather than a name: a column
|
|
46
|
+
* renders qualified with its own table, which is what binds it to the
|
|
47
|
+
* *outer* row instead of to the junction or target aliased inside the
|
|
48
|
+
* subquery. See {@link DrizzleConditionBuilder.buildRelationFilterCondition}.
|
|
49
|
+
*/
|
|
50
|
+
sourceIdColumn?: AnyPgColumn;
|
|
51
|
+
}
|
|
5
52
|
/** Drizzle dynamic query builder — accepts innerJoin + where chaining */
|
|
6
53
|
export interface DrizzleDynamicQuery {
|
|
7
54
|
innerJoin(table: PgTable<any>, condition: SQL): this;
|
|
@@ -40,10 +87,11 @@ export declare class DrizzleConditionBuilder {
|
|
|
40
87
|
*/
|
|
41
88
|
static buildRelationScopeCondition(relation: ResolvedRelation,
|
|
42
89
|
/**
|
|
43
|
-
* Lazy:
|
|
44
|
-
*
|
|
45
|
-
* the parent's *id* alone, and
|
|
46
|
-
* a child listing fail on a
|
|
90
|
+
* Lazy: `via`, `belongsTo`, and a foreign key that points at a
|
|
91
|
+
* `sourceKey` need the parent's own table. A junction and a plain
|
|
92
|
+
* foreign key are expressible from the parent's *id* alone, and
|
|
93
|
+
* requiring the table for them would make a child listing fail on a
|
|
94
|
+
* parent whose table isn't registered.
|
|
47
95
|
*/
|
|
48
96
|
parent: () => {
|
|
49
97
|
table: PgTable<any>;
|
|
@@ -59,14 +107,121 @@ export declare class DrizzleConditionBuilder {
|
|
|
59
107
|
* that revisits a table (a self-referencing many-to-many) unambiguous.
|
|
60
108
|
*/
|
|
61
109
|
private static buildJoinPathScopeCondition;
|
|
110
|
+
/**
|
|
111
|
+
* What a filter field names, or `undefined` if it names nothing.
|
|
112
|
+
*
|
|
113
|
+
* Three ways a field resolves. It may address its column directly; it may
|
|
114
|
+
* be an owning relation, whose foreign key is a column here; or it may be
|
|
115
|
+
* a relation whose link lives on another table entirely, which compiles to
|
|
116
|
+
* a subquery instead of a column. Only a field that resolves to *none* of
|
|
117
|
+
* them is an error, and by default it is one: see
|
|
118
|
+
* {@link UnknownFilterFieldsMode} for why silently dropping it is a
|
|
119
|
+
* data-exposure primitive rather than a convenience.
|
|
120
|
+
*
|
|
121
|
+
* For an owning relation the relation's own `localKey` is the authority,
|
|
122
|
+
* not `<field>_id`. The default local key is `generateForeignKeyName`,
|
|
123
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
124
|
+
* `users` → `user_id` — and it can be overridden outright. Guessing
|
|
125
|
+
* `<field>_id` therefore misses perfectly ordinary owning relations, and
|
|
126
|
+
* with this resolution failing closed that miss is a 400 on a filter that
|
|
127
|
+
* has nothing wrong with it. The guesses stay, last, for callers that hand
|
|
128
|
+
* over no collection to resolve against.
|
|
129
|
+
*
|
|
130
|
+
* The subquery kinds need a registry and the source table's key column on
|
|
131
|
+
* top of the collection. A caller that supplies neither gets the behaviour
|
|
132
|
+
* it had before they were compilable — unresolvable, and so fail-closed —
|
|
133
|
+
* rather than a half-built condition.
|
|
134
|
+
*/
|
|
135
|
+
private static resolveFilterTarget;
|
|
62
136
|
/**
|
|
63
137
|
* Build filter conditions from FilterValues
|
|
64
138
|
*/
|
|
65
|
-
static buildFilterConditions<M extends Record<string, unknown>>(filter: FilterValues<Extract<keyof M, string>>, table: PgTable<any>, collectionPath: string): SQL[];
|
|
139
|
+
static buildFilterConditions<M extends Record<string, unknown>>(filter: FilterValues<Extract<keyof M, string>>, table: PgTable<any>, collectionPath: string, options?: FilterCompilationOptions): SQL[];
|
|
66
140
|
/**
|
|
67
141
|
* Build logical conditions recursively from LogicalCondition or FilterCondition
|
|
68
142
|
*/
|
|
69
|
-
static buildLogicalConditions(cond: LogicalCondition | FilterCondition, table: PgTable<any>, collectionPath: string): SQL | null;
|
|
143
|
+
static buildLogicalConditions(cond: LogicalCondition | FilterCondition, table: PgTable<any>, collectionPath: string, options?: FilterCompilationOptions): SQL | null;
|
|
144
|
+
/** Dispatch a resolved filter field onto the shape it actually compiles to. */
|
|
145
|
+
private static compileFilterTarget;
|
|
146
|
+
/**
|
|
147
|
+
* A filter on a relation that owns no column on this row — `EXISTS` over
|
|
148
|
+
* the rows it reaches.
|
|
149
|
+
*
|
|
150
|
+
* `posts` filtered by `tags == <tagId>` is not a comparison on `posts`; it
|
|
151
|
+
* is a question about the junction:
|
|
152
|
+
*
|
|
153
|
+
* EXISTS (SELECT 1 FROM posts_tags AS j
|
|
154
|
+
* WHERE j.post_id = posts.id AND j.tag_id = <tagId>)
|
|
155
|
+
*
|
|
156
|
+
* which is {@link buildRelationScopeCondition}'s many-to-many shape with
|
|
157
|
+
* source and target swapped — there the junction's *target* column
|
|
158
|
+
* correlates and the source is pinned; here the *source* column correlates
|
|
159
|
+
* and the target is what the filter constrains.
|
|
160
|
+
*
|
|
161
|
+
* `hasMany`/`hasOne` are the same shape one table over: the target row
|
|
162
|
+
* carries the foreign key, so the correlation is on that key and the
|
|
163
|
+
* compared column is the target's own id.
|
|
164
|
+
*
|
|
165
|
+
* `EXISTS` and not a join, for the reason the scope condition gives: a join
|
|
166
|
+
* through a junction multiplies the outer rows by the number of matching
|
|
167
|
+
* links, which duplicates results and silently breaks `limit`/`offset`.
|
|
168
|
+
*
|
|
169
|
+
* Everything inside the subquery is referenced by identifier against a
|
|
170
|
+
* local alias, and only `sourceIdColumn` stays a Drizzle column object —
|
|
171
|
+
* again see {@link buildRelationScopeCondition}, which explains why a
|
|
172
|
+
* column object renders against whatever table the surrounding builder
|
|
173
|
+
* thinks is current and so cannot be used for the inner references. The
|
|
174
|
+
* alias is also what keeps a self-referential relation unambiguous
|
|
175
|
+
* (`categories.children`, or a many-to-many whose junction and target are
|
|
176
|
+
* the same table), where the subquery's table and the outer one coincide.
|
|
177
|
+
*/
|
|
178
|
+
static buildRelationFilterCondition(relation: ResolvedForeignKeyOnTarget | ResolvedManyToMany, op: WhereFilterOp, value: unknown, sourceIdColumn: AnyPgColumn, registry: PostgresCollectionRegistry, field: string, collectionPath: string): SQL;
|
|
179
|
+
/**
|
|
180
|
+
* The inner predicate of a relation filter, and whether the `EXISTS`
|
|
181
|
+
* wrapping it is negated.
|
|
182
|
+
*
|
|
183
|
+
* Negation is `NOT EXISTS` of the *positive* predicate, never `EXISTS` of a
|
|
184
|
+
* negated one. On a many-valued relation the two are different questions:
|
|
185
|
+
* `EXISTS (… AND tag_id != X)` asks "does some tag differ from X", which is
|
|
186
|
+
* true of nearly every post with more than one tag and answers nothing
|
|
187
|
+
* anybody asked. `NOT EXISTS (… AND tag_id = X)` asks "is X absent", which
|
|
188
|
+
* is what unticking a value in a filter control means — and it makes `==`
|
|
189
|
+
* and `!=` partition the rows, the way a filter implies they do.
|
|
190
|
+
*
|
|
191
|
+
* `is-null`/`is-not-null` drop the predicate entirely: with nothing but the
|
|
192
|
+
* correlation left, they become "has no related row at all" and "has at
|
|
193
|
+
* least one", which is the only reading of null a link can have.
|
|
194
|
+
*
|
|
195
|
+
* Under RLS, "no related row" means *no row this reader can see*. A junction
|
|
196
|
+
* with row-level security but no `SELECT` policy for `rebase_user` is opaque
|
|
197
|
+
* to it, so every row comes back looking unlinked and `is-null` matches all
|
|
198
|
+
* of them. That is not a leak — the outer table's own policies still decide
|
|
199
|
+
* which rows exist at all, and the positive direction correctly returns
|
|
200
|
+
* nothing — but it over-reports, and the cause is a missing junction policy
|
|
201
|
+
* rather than anything here. Rebase derives one for a declared many-to-many;
|
|
202
|
+
* a hand-written schema has to supply it.
|
|
203
|
+
*
|
|
204
|
+
* `in`/`not-in` against a *null value* mean the same thing, rather than
|
|
205
|
+
* membership of an empty list. Membership against null is not a membership
|
|
206
|
+
* question, and the admin's "filter for null values" control emits the
|
|
207
|
+
* operator that happens to be selected — on a to-many relation that is
|
|
208
|
+
* always `in` or `not-in`, because those are the only ones the multi-select
|
|
209
|
+
* can produce. Reading `["in", null]` as an empty list would answer "posts
|
|
210
|
+
* with no tags" with no posts at all.
|
|
211
|
+
*
|
|
212
|
+
* An empty `in` list compiles to `FALSE` rather than being dropped. Dropped
|
|
213
|
+
* is what the column path does, and dropping a condition widens the result
|
|
214
|
+
* — the whole reason this resolution fails closed. `in []` matches nothing
|
|
215
|
+
* and `not-in []` matches everything, and `NOT EXISTS (… AND FALSE)` gives
|
|
216
|
+
* the second for free.
|
|
217
|
+
*
|
|
218
|
+
* Anything else is rejected. Returning `null` for an operator this cannot
|
|
219
|
+
* express would drop the condition, and the operators the admin offers for
|
|
220
|
+
* a relation are exactly the six below.
|
|
221
|
+
*/
|
|
222
|
+
private static buildRelationFilterPredicate;
|
|
223
|
+
/** The column a table's rows are keyed by: its primary key, else `id`. */
|
|
224
|
+
private static primaryKeyColumn;
|
|
70
225
|
/**
|
|
71
226
|
* Build a single filter condition for a specific operator and value
|
|
72
227
|
*/
|
|
@@ -28,6 +28,25 @@ export declare function extractPgError(error: unknown): PostgresError | null;
|
|
|
28
28
|
* Walk the error cause chain and return the deepest meaningful message.
|
|
29
29
|
*/
|
|
30
30
|
export declare function extractCauseMessage(error: unknown): string | null;
|
|
31
|
+
export interface ConnectFailure {
|
|
32
|
+
/** True when retrying cannot help: the connection string itself is wrong. */
|
|
33
|
+
fatal: boolean;
|
|
34
|
+
/** The deepest message available — the Postgres one where there is one. */
|
|
35
|
+
reason: string;
|
|
36
|
+
/** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */
|
|
37
|
+
code?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Describe a failed connection attempt in terms a developer can act on.
|
|
41
|
+
*
|
|
42
|
+
* The error a caller catches is Drizzle's wrapper: its message is
|
|
43
|
+
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
44
|
+
* the sentence that says what is actually wrong — "password authentication
|
|
45
|
+
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
46
|
+
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
47
|
+
* their `DATABASE_URL` nothing at all.
|
|
48
|
+
*/
|
|
49
|
+
export declare function classifyConnectFailure(error: unknown): ConnectFailure;
|
|
31
50
|
/**
|
|
32
51
|
* Detect whether an error is specifically a role-switching permission failure
|
|
33
52
|
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
@@ -52,12 +71,15 @@ export declare function pgErrorToFriendlyMessage(pgError: PostgresError, context
|
|
|
52
71
|
/**
|
|
53
72
|
* Sanitize any error into a message safe and helpful for the client.
|
|
54
73
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
74
|
+
* A deliberate 4xx (`ApiError`) passes through untouched — the server already
|
|
75
|
+
* decided what the client should read. Otherwise the PG error is extracted
|
|
76
|
+
* from the Drizzle cause chain, falling back to a generic message that
|
|
77
|
+
* doesn't leak SQL.
|
|
57
78
|
*
|
|
58
79
|
* @param error - The raw caught error
|
|
59
80
|
* @param context - A human-readable context string (e.g. collection path)
|
|
60
|
-
* @returns An object with `message` (user-friendly) and optional `code`
|
|
81
|
+
* @returns An object with `message` (user-friendly) and optional `code`
|
|
82
|
+
* (the `ApiError` code, or the PG SQLSTATE).
|
|
61
83
|
*/
|
|
62
84
|
export declare function sanitizeErrorForClient(error: unknown, context: string): {
|
|
63
85
|
message: string;
|