@rebasepro/server-postgres 0.17.3 → 0.18.0

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 (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. package/src/websocket.ts +0 -855
package/src/connection.ts DELETED
@@ -1,333 +0,0 @@
1
- import { Pool, PoolConfig } from "pg";
2
- import { drizzle } from "drizzle-orm/node-postgres";
3
- import { logger } from "@rebasepro/server";
4
-
5
- /**
6
- * Configuration for the Postgres connection pool.
7
- *
8
- * Sensible defaults are provided for production Cloud Run / single-instance
9
- * deployments. Override via environment variables or explicit config.
10
- */
11
- export interface PostgresPoolConfig {
12
- /** Maximum number of connections in the pool (default: 20) */
13
- max?: number;
14
- /** Close idle connections after this many ms (default: 30 000) */
15
- idleTimeoutMillis?: number;
16
- /** Abort connection attempts after this many ms (default: 10 000) */
17
- connectionTimeoutMillis?: number;
18
- /** Per-query timeout in ms (default: 30 000) */
19
- queryTimeout?: number;
20
- /** Per-statement timeout in ms (default: 30 000) */
21
- statementTimeout?: number;
22
- /** Enable TCP keep-alive (default: true) */
23
- keepAlive?: boolean;
24
- /**
25
- * `search_path` pinned on every connection (default: `"public"`).
26
- *
27
- * Pass `false` to send no `search_path` at all and inherit whatever the
28
- * server/role defaults to. See {@link pinSearchPath} for why the default
29
- * is not "inherit".
30
- */
31
- searchPath?: string | false;
32
- }
33
-
34
- /**
35
- * Ceiling on pool size, from the environment.
36
- *
37
- * Exists for the managed development database. PGlite is a single session
38
- * behind a multiplexing socket server, and two pooled clients holding
39
- * overlapping transactions deadlock there — which a request-per-transaction
40
- * server produces the moment two requests overlap. One connection turns that
41
- * deadlock into ordinary queueing.
42
- *
43
- * A ceiling rather than a value, so it constrains every pool in the process
44
- * without any of them having to know why. Ignored when unset or unparseable,
45
- * because a malformed limit must not silently serialize a production server.
46
- */
47
- export function poolMaxCeiling(env: NodeJS.ProcessEnv = process.env): number | null {
48
- const raw = env.REBASE_DB_POOL_MAX;
49
- if (typeof raw !== "string") return null;
50
-
51
- // Plain decimal digits only. `Number` would also accept `1e3`, `0x10` and
52
- // ` 4 ` — none of which anyone writes on purpose in a config value, and all
53
- // of which would mean this silently did something other than it appears to.
54
- if (!/^[0-9]+$/.test(raw.trim())) return null;
55
-
56
- const parsed = Number(raw.trim());
57
-
58
- return parsed >= 1 ? parsed : null;
59
- }
60
-
61
- /** Apply {@link poolMaxCeiling} to a requested pool size. */
62
- export function cappedPoolMax(requested: number, env: NodeJS.ProcessEnv = process.env): number {
63
- const ceiling = poolMaxCeiling(env);
64
-
65
- return ceiling === null ? requested : Math.min(requested, ceiling);
66
- }
67
-
68
- const DEFAULT_POOL: Required<PostgresPoolConfig> = {
69
- max: 20,
70
- searchPath: "public",
71
- idleTimeoutMillis: 30_000,
72
- connectionTimeoutMillis: 10_000,
73
- // The client-side read timeout MUST be comfortably above the server-side
74
- // statement_timeout. When the client timer fires first, node-postgres
75
- // abandons the in-flight statement but keeps the connection — inside a
76
- // transaction that leaves the tx open (and any pending ROLLBACK is
77
- // spliced out of the client queue before it ever reaches the wire), so
78
- // the pooled connection is returned still in-transaction with its RLS
79
- // GUCs set. The server abort (SQLSTATE 57014) is the clean path; the
80
- // client timeout is only a backstop for a dead network.
81
- queryTimeout: 60_000,
82
- statementTimeout: 30_000,
83
- keepAlive: true
84
- };
85
-
86
- /** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */
87
- const TX_IDLE = "I";
88
-
89
- /**
90
- * Pin `search_path` into a connection string, so unqualified SQL resolves to a
91
- * schema this framework chose rather than to one Postgres inferred.
92
- *
93
- * Postgres defaults `search_path` to `"$user", public`: the *first* candidate
94
- * is a schema named after the connecting role. Rebase creates a schema called
95
- * `rebase` (auth, history, api keys), and every template, compose file and
96
- * deployment doc names the database role `rebase` too — so `$user` resolves to
97
- * a schema that exists, and every unqualified statement lands there instead of
98
- * in `public`. The generated Drizzle schema emits bare `pgTable("posts", …)`
99
- * for any collection without an explicit `schema`, which makes the *runtime's*
100
- * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the
101
- * Studio SQL editor and any hand-written migration are unqualified too. The
102
- * result is collection tables created in, and served from, `rebase`.
103
- *
104
- * Drizzle cannot express the fix on its side: `pgSchema("public")` throws by
105
- * design ("just use pgTable() instead"), so there is no way to emit a
106
- * public-qualified table from the generator. The pin has to live on the
107
- * connection.
108
- *
109
- * Precedence is deliberate and verified against node-postgres: `options` in
110
- * the connection string wins over the `options` field passed to `Pool`, so
111
- * rewriting the URL — rather than setting the field — is what makes this
112
- * authoritative. Two escape hatches survive it:
113
- *
114
- * - an `options` that already mentions `search_path` is left untouched, so a
115
- * deployment that deliberately pins something else keeps it;
116
- * - `searchPath: false` (or an unparseable, non-URL connection string) sends
117
- * nothing and inherits the server default.
118
- *
119
- * Anything else in `options` (a `statement_timeout`, say) is preserved and the
120
- * `search_path` flag is appended to it.
121
- */
122
- export function pinSearchPath(connectionString: string, searchPath: string | false = "public"): string {
123
- if (searchPath === false) return connectionString;
124
-
125
- let url: URL;
126
- try {
127
- url = new URL(connectionString);
128
- } catch {
129
- // Key/value DSNs and anything else we cannot parse are returned as
130
- // given: a connection that works unpinned beats one we corrupted.
131
- return connectionString;
132
- }
133
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
134
-
135
- const existing = url.searchParams.get("options");
136
- if (existing && /(^|\s)-c\s*search_path\s*=/.test(existing)) return connectionString;
137
-
138
- const flag = `-c search_path=${searchPath}`;
139
- url.searchParams.set("options", existing ? `${existing} ${flag}` : flag);
140
- // Re-serialize by hand. `URLSearchParams` writes a space as `+`, which
141
- // node-postgres happens to decode but libpq does not — and this same string
142
- // is handed to `pg_dump`/`psql` for backups. Percent-encoding is the form
143
- // both agree on, and is what the scaffolded `.env` already ships.
144
- url.search = Array.from(url.searchParams.entries())
145
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
146
- .join("&");
147
- return url.toString();
148
- }
149
-
150
- /**
151
- * Destroy pool clients that are released while still inside a transaction.
152
- *
153
- * pg-pool returns a client to the idle list whenever `release()` is called
154
- * without an error — even if the connection is still mid-transaction (status
155
- * `T`/`E`). That happens in practice: drizzle's pool transaction releases in
156
- * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
157
- * (e.g. it was queued behind a statement that hit the client-side
158
- * query_timeout), the client goes back dirty. The next checkout then runs
159
- * its statements inside the zombie transaction — with the previous request's
160
- * `app.*` RLS GUCs still applied, which turns unrelated queries into
161
- * RLS-scoped ones (observed in production as registration failing with
162
- * SQLSTATE 42501 under a leaked anonymous context).
163
- *
164
- * pg-pool emits `release` before it consults its private `_expired` set, so
165
- * marking the client expired here makes `_release()` destroy it instead of
166
- * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
167
- * private APIs — feature-detect and fall back to loud logging so an upstream
168
- * change degrades to observability, never to silent corruption.
169
- */
170
- export function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void {
171
- pool.on("release", (err: Error | undefined, client: unknown) => {
172
- if (err) return; // errored clients are already destroyed by pg-pool
173
- const txStatus = (client as { _txStatus?: string | null })?._txStatus;
174
- if (typeof txStatus !== "string" || txStatus === TX_IDLE) return;
175
-
176
- // pg-pool keeps expired clients in a WeakSet (a plain object works too
177
- // if upstream ever changes it — duck-type on add/has).
178
- const expired = (pool as unknown as { _expired?: { add(c: object): unknown; has(c: object): boolean } })._expired;
179
- if (expired && typeof expired.add === "function" && typeof expired.has === "function" && client && typeof client === "object") {
180
- expired.add(client);
181
- logger.error(
182
- `[${label}] Client released back to the pool while still in a transaction ` +
183
- `(status '${txStatus}') — destroying it so the open transaction and its ` +
184
- `session state (RLS GUCs) cannot leak into the next request.`
185
- );
186
- } else {
187
- logger.error(
188
- `[${label}] Client released mid-transaction (status '${txStatus}') but the ` +
189
- `pool's internal expiry set is unavailable (pg-pool internals changed?). ` +
190
- `The connection may leak its open transaction into subsequent requests.`
191
- );
192
- }
193
- });
194
- }
195
-
196
- /**
197
- * Create a Drizzle-backed Postgres connection with a production-grade
198
- * connection pool.
199
- *
200
- * @param connectionString Postgres connection URL
201
- * @param schema Optional Drizzle schema for the relational API
202
- * @param poolConfig Optional pool tuning (merged over defaults)
203
- *
204
- * @returns `{ db, pool, connectionString }` — the `pool` is exposed so
205
- * callers can register shutdown hooks (`pool.end()`) or monitor
206
- * pool metrics.
207
- */
208
- export function createPostgresDatabaseConnection(
209
- connectionString: string,
210
- schema?: Record<string, unknown>,
211
- poolConfig?: PostgresPoolConfig
212
- ) {
213
- const opts = { ...DEFAULT_POOL,
214
- ...poolConfig };
215
- connectionString = pinSearchPath(connectionString, opts.searchPath);
216
-
217
- const pgPoolConfig: PoolConfig = {
218
- connectionString,
219
- max: cappedPoolMax(opts.max),
220
- idleTimeoutMillis: opts.idleTimeoutMillis,
221
- connectionTimeoutMillis: opts.connectionTimeoutMillis,
222
- query_timeout: opts.queryTimeout,
223
- statement_timeout: opts.statementTimeout,
224
- keepAlive: opts.keepAlive,
225
- keepAliveInitialDelayMillis: 0
226
- };
227
-
228
- const pool = new Pool(pgPoolConfig);
229
-
230
- // ── Pool event logging ────────────────────────────────────────────────
231
- // Uses console.* because the structured logger lives in server
232
- // (a separate package). The caller can replace these with the structured
233
- // logger if desired via pool.on() after creation.
234
- pool.on("error", (err) => {
235
- logger.error("[pg-pool] Unexpected pool error", { detail: err.message });
236
- if (err.message.includes("ETIMEDOUT")) {
237
- logger.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
238
- }
239
- });
240
- guardPoolAgainstDirtyRelease(pool, "pg-pool");
241
-
242
- // Create drizzle instance — pass schema when available to enable db.query relational API
243
- const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
244
-
245
- return { db,
246
- pool,
247
- connectionString };
248
- }
249
-
250
- /**
251
- * Create a direct (non-pooled) connection for operations that require
252
- * session-level features incompatible with PgBouncer transaction mode,
253
- * such as LISTEN/NOTIFY, prepared statements, or advisory locks.
254
- *
255
- * Uses a smaller pool since this is only for specific use cases.
256
- */
257
- export function createDirectDatabaseConnection(
258
- connectionString: string,
259
- schema?: Record<string, unknown>,
260
- poolConfig?: PostgresPoolConfig
261
- ) {
262
- const opts = {
263
- ...DEFAULT_POOL,
264
- max: 5,
265
- ...poolConfig
266
- };
267
- connectionString = pinSearchPath(connectionString, opts.searchPath);
268
-
269
- const pgPoolConfig: PoolConfig = {
270
- connectionString,
271
- max: cappedPoolMax(opts.max),
272
- idleTimeoutMillis: opts.idleTimeoutMillis,
273
- connectionTimeoutMillis: opts.connectionTimeoutMillis,
274
- query_timeout: opts.queryTimeout,
275
- statement_timeout: opts.statementTimeout,
276
- keepAlive: opts.keepAlive,
277
- keepAliveInitialDelayMillis: 0
278
- };
279
-
280
- const pool = new Pool(pgPoolConfig);
281
-
282
- pool.on("error", (err) => {
283
- logger.error("[pg-direct-pool] Unexpected pool error", { detail: err.message });
284
- });
285
- guardPoolAgainstDirtyRelease(pool, "pg-direct-pool");
286
-
287
- const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
288
-
289
- return { db,
290
- pool,
291
- connectionString };
292
- }
293
-
294
- /**
295
- * Create a read-only connection for routing read queries to replicas.
296
- * Uses a moderate pool size since reads are distributed across replicas.
297
- */
298
- export function createReadReplicaConnection(
299
- connectionString: string,
300
- schema?: Record<string, unknown>,
301
- poolConfig?: PostgresPoolConfig
302
- ) {
303
- const opts = {
304
- ...DEFAULT_POOL,
305
- max: 10,
306
- ...poolConfig
307
- };
308
- connectionString = pinSearchPath(connectionString, opts.searchPath);
309
-
310
- const pgPoolConfig: PoolConfig = {
311
- connectionString,
312
- max: cappedPoolMax(opts.max),
313
- idleTimeoutMillis: opts.idleTimeoutMillis,
314
- connectionTimeoutMillis: opts.connectionTimeoutMillis,
315
- query_timeout: opts.queryTimeout,
316
- statement_timeout: opts.statementTimeout,
317
- keepAlive: opts.keepAlive,
318
- keepAliveInitialDelayMillis: 0
319
- };
320
-
321
- const pool = new Pool(pgPoolConfig);
322
-
323
- pool.on("error", (err) => {
324
- logger.error("[pg-replica-pool] Unexpected pool error", { detail: err.message });
325
- });
326
- guardPoolAgainstDirtyRelease(pool, "pg-replica-pool");
327
-
328
- const db = schema ? drizzle(pool, { schema }) : drizzle(pool);
329
-
330
- return { db,
331
- pool,
332
- connectionString };
333
- }