@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.
Files changed (98) hide show
  1. package/dist/PostgresBackendDriver.d.ts +1 -1
  2. package/dist/PostgresBootstrapper.d.ts +33 -1
  3. package/dist/auth/services.d.ts +21 -0
  4. package/dist/backup/backup-service.d.ts +10 -1
  5. package/dist/backup/pg-tools.d.ts +47 -0
  6. package/dist/backup-service-CD8o_1Sl.js +8999 -0
  7. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  8. package/dist/cli-helpers.d.ts +39 -0
  9. package/dist/collections/buildRegistry.d.ts +1 -1
  10. package/dist/connection-BuZ97wsr.js +250 -0
  11. package/dist/connection-BuZ97wsr.js.map +1 -0
  12. package/dist/connection.d.ts +42 -0
  13. package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
  14. package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
  15. package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
  16. package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
  17. package/dist/history/HistoryService.d.ts +9 -29
  18. package/dist/index.es.js +1234 -9753
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/policy-CeA1JcxP.js +105 -0
  21. package/dist/policy-CeA1JcxP.js.map +1 -0
  22. package/dist/schema/auth-schema.d.ts +83 -144
  23. package/dist/schema/dynamic-tables.d.ts +1 -1
  24. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  25. package/dist/schema/ensure-collection-tables.d.ts +44 -2
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
  27. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  28. package/dist/schema/introspect-db-logic.d.ts +94 -5
  29. package/dist/schema/introspect-db-queries.d.ts +119 -0
  30. package/dist/schema/introspect-db-structure.d.ts +263 -0
  31. package/dist/schema/introspect-db-types.d.ts +11 -0
  32. package/dist/schema/introspect-runtime.d.ts +1 -1
  33. package/dist/services/FetchService.d.ts +40 -2
  34. package/dist/services/RelationService.d.ts +24 -1
  35. package/dist/services/channel-bus/index.d.ts +1 -7
  36. package/dist/services/collection-helpers.d.ts +24 -1
  37. package/dist/services/dataService.d.ts +3 -1
  38. package/dist/services/row-pipeline.d.ts +4 -2
  39. package/dist/{src-3VmUJ8Xn.js → src-CzbghKwf.js} +464 -187
  40. package/dist/src-CzbghKwf.js.map +1 -0
  41. package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
  42. package/dist/src-DoU9yPqq.js.map +1 -0
  43. package/dist/utils/connection-string.d.ts +29 -0
  44. package/dist/utils/drizzle-conditions.d.ts +162 -7
  45. package/dist/utils/pg-error-utils.d.ts +25 -3
  46. package/dist/websocket-B2LsrINK.js +530 -0
  47. package/dist/websocket-B2LsrINK.js.map +1 -0
  48. package/package.json +14 -14
  49. package/src/PostgresAdapter.ts +21 -2
  50. package/src/PostgresBackendDriver.ts +4 -0
  51. package/src/PostgresBootstrapper.ts +212 -36
  52. package/src/auth/ensure-tables.ts +164 -9
  53. package/src/auth/services.ts +24 -2
  54. package/src/backup/backup-cli.ts +41 -2
  55. package/src/backup/backup-service.ts +38 -5
  56. package/src/backup/pg-tools.ts +96 -3
  57. package/src/cli-helpers.ts +70 -0
  58. package/src/cli.ts +44 -26
  59. package/src/collections/buildRegistry.ts +1 -1
  60. package/src/collections/validate-relations.ts +15 -0
  61. package/src/connection.ts +73 -0
  62. package/src/data-transformer.ts +9 -3
  63. package/src/databasePoolManager.ts +5 -2
  64. package/src/history/HistoryService.ts +13 -31
  65. package/src/schema/auth-schema.ts +30 -19
  66. package/src/schema/dynamic-tables.ts +1 -1
  67. package/src/schema/ensure-collection-policies.ts +105 -0
  68. package/src/schema/ensure-collection-tables.test.ts +105 -9
  69. package/src/schema/ensure-collection-tables.ts +220 -32
  70. package/src/schema/generate-drizzle-schema-logic.ts +33 -8
  71. package/src/schema/generate-postgres-ddl-logic.ts +382 -19
  72. package/src/schema/introspect-db-constraints.ts +385 -0
  73. package/src/schema/introspect-db-inference.ts +18 -8
  74. package/src/schema/introspect-db-logic.ts +385 -71
  75. package/src/schema/introspect-db-queries.ts +326 -0
  76. package/src/schema/introspect-db-structure.ts +670 -0
  77. package/src/schema/introspect-db-types.ts +56 -0
  78. package/src/schema/introspect-db.ts +37 -80
  79. package/src/schema/introspect-runtime.test.ts +56 -8
  80. package/src/schema/introspect-runtime.ts +32 -10
  81. package/src/security/policy-drift.test.ts +11 -3
  82. package/src/services/FetchService.ts +148 -18
  83. package/src/services/PersistService.ts +20 -6
  84. package/src/services/RelationService.ts +249 -48
  85. package/src/services/channel-bus/index.ts +0 -9
  86. package/src/services/collection-helpers.ts +40 -1
  87. package/src/services/dataService.ts +3 -1
  88. package/src/services/realtimeService.ts +3 -3
  89. package/src/services/row-pipeline.ts +4 -2
  90. package/src/utils/connection-string.ts +58 -0
  91. package/src/utils/drizzle-conditions.ts +539 -50
  92. package/src/utils/pg-error-utils.ts +98 -3
  93. package/src/websocket.ts +18 -9
  94. package/dist/chunk-DSJWtz9O.js +0 -40
  95. package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
  96. package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
  97. package/dist/src-3VmUJ8Xn.js.map +0 -1
  98. 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: only `via` and `belongsTo` need the parent's own table. A
44
- * foreign key on the target and a junction are both expressible from
45
- * the parent's *id* alone, and requiring the table for them would make
46
- * a child listing fail on a parent whose table isn't registered.
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
- * Extracts the PG error from the Drizzle cause chain when possible;
56
- * falls back to a generic message that doesn't leak SQL.
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` (PG 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;