@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.g19596ef
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/README.md +16 -0
- package/dist/{BranchService-BRt78gfa.js → BranchService-UgKa416x.js} +14 -3
- package/dist/BranchService-UgKa416x.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +104 -2
- package/dist/PostgresBootstrapper.d.ts +101 -2
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +6 -0
- package/dist/backup/backup-cli.d.ts +75 -2
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-DW5p9_zv.js → backup-cli-yiKd1GsC.js} +168 -75
- package/dist/backup-cli-yiKd1GsC.js.map +1 -0
- package/dist/{backup-service-EwcDVG-8.js → backup-service-AZWc2wvI.js} +99 -28
- package/dist/backup-service-AZWc2wvI.js.map +1 -0
- package/dist/boot-ddl-B9ZmBJ75.js +105 -0
- package/dist/boot-ddl-B9ZmBJ75.js.map +1 -0
- package/dist/cli-console-DGee3Zl7.js +225 -0
- package/dist/cli-console-DGee3Zl7.js.map +1 -0
- package/dist/cli-console.d.ts +39 -0
- package/dist/cli-errors-oVUuD8k0.js +2180 -0
- package/dist/cli-errors-oVUuD8k0.js.map +1 -0
- package/dist/cli-errors.d.ts +91 -9
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-DnPHXjRH.js} +56 -6
- package/dist/cli-flags-DnPHXjRH.js.map +1 -0
- package/dist/cli-flags.d.ts +15 -0
- package/dist/cli-helpers.d.ts +76 -22
- package/dist/cli-output.d.ts +16 -28
- package/dist/{arg-Dni7MzLB.js → cli-prompt-egtNKkZl.js} +49 -3
- package/dist/cli-prompt-egtNKkZl.js.map +1 -0
- package/dist/cli-prompt.d.ts +19 -0
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli-scratch-database.d.ts +33 -8
- package/dist/cli.js +764 -222
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-D_V4oaZh.js} +11 -5
- package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-D_V4oaZh.js.map} +1 -1
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C938nR3E.js} +25 -113
- package/dist/connection-C938nR3E.js.map +1 -0
- package/dist/connection.d.ts +13 -2
- package/dist/databasePoolManager-DZgnBOgj.js +119 -0
- package/dist/databasePoolManager-DZgnBOgj.js.map +1 -0
- package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-BqmEHGaD.js} +485 -347
- package/dist/deferred-index-build-BqmEHGaD.js.map +1 -0
- package/dist/{doctor-C-sYWbmt.js → doctor-CUAAOFZm.js} +65 -54
- package/dist/doctor-CUAAOFZm.js.map +1 -0
- package/dist/ensure-collection-policies-Bb51V2OV.js +541 -0
- package/dist/ensure-collection-policies-Bb51V2OV.js.map +1 -0
- package/dist/ensure-collection-tables-egjGBjhv.js +532 -0
- package/dist/ensure-collection-tables-egjGBjhv.js.map +1 -0
- package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-DMPWhJ6R.js} +174 -78
- package/dist/ensure-tables-DMPWhJ6R.js.map +1 -0
- package/dist/enum-labels-kept-Msq5gMcb.js +56 -0
- package/dist/enum-labels-kept-Msq5gMcb.js.map +1 -0
- package/dist/generate-drizzle-schema-DrA-YgXv.js +57 -0
- package/dist/generate-drizzle-schema-DrA-YgXv.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-BaT0H7Gq.js} +6 -6
- package/dist/generate-drizzle-schema-logic-BaT0H7Gq.js.map +1 -0
- package/dist/generate-files-OoRJLEdk.js +199 -0
- package/dist/generate-files-OoRJLEdk.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B4k9djAV.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B4k9djAV.js.map} +1 -1
- package/dist/{generated-schema-staleness-Di9c4ZxN.js → generated-schema-staleness-BWN_PsMi.js} +2 -2
- package/dist/{generated-schema-staleness-Di9c4ZxN.js.map → generated-schema-staleness-BWN_PsMi.js.map} +1 -1
- package/dist/generated-sql.d.ts +8 -2
- package/dist/history/HistoryService.d.ts +64 -5
- package/dist/history/ensure-history-table.d.ts +63 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +139 -19236
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-BCc1Jym2.js} +136 -9
- package/dist/introspect-db-logic-BCc1Jym2.js.map +1 -0
- package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-BhhJg0b8.js} +48 -1
- package/dist/introspect-db-queries-BhhJg0b8.js.map +1 -0
- package/dist/open-rebase-CAjWpPjs.js +20895 -0
- package/dist/open-rebase-CAjWpPjs.js.map +1 -0
- package/dist/open-rebase.d.ts +94 -0
- package/dist/pg-error-utils-Bs_Ue4wn.js +395 -0
- package/dist/pg-error-utils-Bs_Ue4wn.js.map +1 -0
- package/dist/{plan-schema-DU9exq6C.js → plan-schema-CsNgK0qi.js} +152 -42
- package/dist/plan-schema-CsNgK0qi.js.map +1 -0
- package/dist/{policy-drift-B-J2hhm0.js → policy-drift-BKC50y2p.js} +32 -8
- package/dist/policy-drift-BKC50y2p.js.map +1 -0
- package/dist/push-index-prebuild-C_qhyoJa.js +45 -0
- package/dist/push-index-prebuild-C_qhyoJa.js.map +1 -0
- package/dist/push-legacy-renames-5g9h3HOa.js +34 -0
- package/dist/push-legacy-renames-5g9h3HOa.js.map +1 -0
- package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CrfEe__B.js} +15 -10
- package/dist/render-ddl-CrfEe__B.js.map +1 -0
- package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-Bd-hJk08.js} +125 -1325
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +1 -0
- package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-D7P4-ZYX.js} +130 -18
- package/dist/rls-enforcement-D7P4-ZYX.js.map +1 -0
- package/dist/{__vite-browser-external-BnuHet1e.js → rolldown-runtime-V7rFbMRe.js} +1 -8
- package/dist/schema/atlas-exclude.d.ts +42 -0
- package/dist/schema/auth-schema.d.ts +2 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/carved-out-migration.d.ts +5 -4
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +5 -0
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +30 -1
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +20 -1
- package/dist/schema/ensure-collection-policies.d.ts +73 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-schema-diff.d.ts +7 -0
- package/dist/schema/introspect-db-logic.d.ts +32 -0
- package/dist/schema/introspect-db-queries.d.ts +20 -1
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +16 -20
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-baseline.d.ts +33 -0
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +126 -0
- package/dist/schema/plan/render-ddl.d.ts +2 -1
- package/dist/schema/plan/types.d.ts +8 -0
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/schema/undeclarable-constraints.d.ts +59 -0
- package/dist/security/rls-enforcement.d.ts +32 -4
- package/dist/services/BranchService.d.ts +7 -0
- package/dist/services/FetchService.d.ts +58 -8
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +88 -3
- package/dist/services/cdc/CdcListener.d.ts +17 -6
- package/dist/services/cdc/trigger-cdc.d.ts +151 -27
- package/dist/services/channel-bus/index.d.ts +1 -1
- package/dist/services/channel-history.d.ts +15 -4
- package/dist/services/collection-helpers.d.ts +15 -0
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/realtimeService.d.ts +314 -14
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +42 -2
- package/dist/services/sql-script.d.ts +207 -3
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +6 -0
- package/dist/services/write-transaction-scope.d.ts +9 -0
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DB6xJv3p.js +873 -0
- package/dist/source-DB6xJv3p.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/connection-string.d.ts +31 -0
- package/dist/utils/drizzle-conditions.d.ts +21 -1
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/{vector-index-CMJKb3C9.js → vector-index-BFhyEaiz.js} +2 -2
- package/dist/{vector-index-CMJKb3C9.js.map → vector-index-BFhyEaiz.js.map} +1 -1
- package/dist/websocket-DUiiwQd2.js +1632 -0
- package/dist/websocket-DUiiwQd2.js.map +1 -0
- package/dist/websocket.d.ts +13 -0
- package/package.json +7 -7
- package/dist/BranchService-BRt78gfa.js.map +0 -1
- package/dist/__vite-browser-external-BnuHet1e.js.map +0 -1
- package/dist/arg-Dni7MzLB.js.map +0 -1
- package/dist/backup-cli-DW5p9_zv.js.map +0 -1
- package/dist/backup-service-EwcDVG-8.js.map +0 -1
- package/dist/cli-errors-DsA-K9uP.js +0 -893
- package/dist/cli-errors-DsA-K9uP.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-C-sYWbmt.js.map +0 -1
- package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
- package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
- package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
- package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
- package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
- package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
- package/dist/plan-schema-DU9exq6C.js.map +0 -1
- package/dist/policy-drift-B-J2hhm0.js.map +0 -1
- package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
- package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
- package/dist/utils/sql-redaction.d.ts +0 -21
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ Implements the Rebase `DatabaseAdapter` / `BackendBootstrapper` interfaces for P
|
|
|
50
50
|
|--------|-------------|
|
|
51
51
|
| `createPostgresAdapter(config)` | Creates a `DatabaseAdapter` for use with `initializeRebaseBackend({ database: ... })`. Recommended API. |
|
|
52
52
|
| `createPostgresBootstrapper(config)` | Lower-level `BackendBootstrapper` factory. Used internally by the adapter. |
|
|
53
|
+
| `openPostgresDriver(pgConfig, init)` | Opens the data driver from explicit settings, reading no environment variable; `provision: false` opens it with no DDL. What the adapter runs at boot. New in 0.25. |
|
|
54
|
+
| `postgresDriverConfigFromEnv(config, env?)` | Reads `DATABASE_READ_URL`, `DATABASE_DIRECT_URL` and the `REALTIME_*` variables into a driver config, as the server's boot does. New in 0.25. |
|
|
55
|
+
| `openRebase({ databaseUrl, collections })` | A project's data from a script, a test or a REPL: `dataAsAdmin`, `sql`, `withAuth(user)` and `close()`. No server, no DDL, no environment reads. `rebase console` is built on it. New in 0.25. |
|
|
53
56
|
| `createPostgresDatabaseConnection(url, schema?, poolConfig?)` | Creates a production-grade pooled Drizzle connection. Returns `{ db, pool, connectionString }`. |
|
|
54
57
|
| `createDirectDatabaseConnection(url, schema?, poolConfig?)` | Non-pooled connection for LISTEN/NOTIFY and advisory locks (bypasses PgBouncer). |
|
|
55
58
|
| `createReadReplicaConnection(url, schema?, poolConfig?)` | Read-only connection for routing reads to replicas. |
|
|
@@ -98,6 +101,19 @@ process.on("SIGTERM", async () => {
|
|
|
98
101
|
});
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
## From a script
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { openRebase } from "@rebasepro/server-postgres";
|
|
108
|
+
|
|
109
|
+
const rebase = await openRebase({ databaseUrl: process.env.DATABASE_URL!, collections });
|
|
110
|
+
const { data } = await rebase.dataAsAdmin.posts.find({ limit: 10 });
|
|
111
|
+
const ada = await rebase.withAuth({ uid: "ada", roles: ["user"] }); // what Ada's RLS lets her see
|
|
112
|
+
await rebase.close();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
See [Scripts and the Console](https://rebase.pro/docs/backend/scripts).
|
|
116
|
+
|
|
101
117
|
## Connection Pool Defaults
|
|
102
118
|
|
|
103
119
|
| Option | Default |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { n as __exportAll } from "./rolldown-runtime-V7rFbMRe.js";
|
|
4
|
+
import { Z as extractCauseMessage } from "./cli-errors-oVUuD8k0.js";
|
|
5
|
+
import { n as extractPgError } from "./pg-error-utils-Bs_Ue4wn.js";
|
|
5
6
|
import { sql } from "drizzle-orm";
|
|
6
7
|
import { revokeInternalTableSql } from "@rebasepro/common";
|
|
7
8
|
//#region src/services/BranchService.ts
|
|
@@ -137,6 +138,16 @@ var BranchService = class {
|
|
|
137
138
|
if (await this.poolManager.servesOneDatabase()) throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
141
|
+
* Whether the `rebase.branches` registry exists. A database that has never
|
|
142
|
+
* had a branch made on it may not have it, and reading it there is an
|
|
143
|
+
* empty answer, not an error — nor a reason to create it. The CLI's `list`
|
|
144
|
+
* and `info` ask this rather than running the DDL that creates it.
|
|
145
|
+
*/
|
|
146
|
+
async hasBranchRegistry() {
|
|
147
|
+
const [row] = (await (await this.home.registry()).execute(sql`SELECT to_regclass('rebase.branches') IS NOT NULL AS present`)).rows;
|
|
148
|
+
return row !== void 0 && Reflect.get(row, "present") === true;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
140
151
|
* Ensure the `rebase.branches` metadata table exists in the default database.
|
|
141
152
|
* Idempotent — safe to call on every startup.
|
|
142
153
|
*/
|
|
@@ -374,4 +385,4 @@ var BranchService = class {
|
|
|
374
385
|
//#endregion
|
|
375
386
|
export { BranchService_exports as n, BranchingUnsupportedError as r, BranchService as t };
|
|
376
387
|
|
|
377
|
-
//# sourceMappingURL=BranchService-
|
|
388
|
+
//# sourceMappingURL=BranchService-UgKa416x.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BranchService-UgKa416x.js","names":[],"sources":["../src/services/BranchService.ts"],"sourcesContent":["/**\n * BranchService\n *\n * Manages database branching by creating/deleting PostgreSQL databases\n * using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the\n * `rebase.branches` table in the default (main) database, following the\n * same `rebase` schema convention used by entity_history, auth, etc.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { BranchInfo } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { DrizzleClient } from \"../interfaces\";\nimport { BranchRow } from \"../branch-prune\";\nimport { DatabasePoolManager } from \"../databasePoolManager\";\nimport { extractPgError, extractCauseMessage } from \"../utils/pg-error-utils\";\n\n/** Internal prefix applied to branch database names to avoid collisions. */\nconst BRANCH_DB_PREFIX = \"rb_\";\n\n/** `duplicate_database` — the target database name is already taken. */\nconst PG_DUPLICATE_DATABASE = \"42P04\";\n\n/** `object_in_use` — the database still has connections attached. */\nconst PG_OBJECT_IN_USE = \"55006\";\n\n/**\n * Describe a failed branch DDL statement in terms a user can act on.\n *\n * Drizzle reports failures as `Failed query: <sql> params:` and hides the real\n * PostgreSQL error in the `cause` chain, so matching on `err.message` never sees\n * the actual problem. Match on the PG error code instead — it survives wrapping\n * and, unlike the message text, is not locale-dependent.\n */\nfunction describeBranchDdlError(err: unknown, fallbackContext: string): Error {\n const pgError = extractPgError(err);\n\n if (pgError?.code === PG_DUPLICATE_DATABASE) {\n return new Error(`Database \"${fallbackContext}\" already exists on the server. Choose a different branch name.`);\n }\n if (pgError?.code === PG_OBJECT_IN_USE) {\n return new Error(\n `Cannot complete the operation: the database \"${fallbackContext}\" has active connections. ` +\n \"Close other clients or connections and try again.\"\n );\n }\n\n // Unknown failure: surface the real PG message rather than the Drizzle\n // wrapper, which would otherwise show the raw SQL and no reason at all.\n const detail = pgError?.message ?? extractCauseMessage(err);\n if (detail) return new Error(detail);\n return err instanceof Error ? err : new Error(String(err));\n}\n\n/** Fully-qualified metadata table in the rebase schema. */\nconst BRANCHES_TABLE = \"rebase.branches\";\n\n/**\n * The connected server cannot branch, so nothing was done.\n *\n * A refusal, not a fault: typed so the transports can forward it intact.\n * `websocket.ts` answers it with its own code and message rather than letting\n * it fall into the generic handler, which suppresses the text in production —\n * and the text is the only part that says what to do instead.\n */\nexport class BranchingUnsupportedError extends Error {\n /** Wire code, forwarded verbatim to the client. */\n readonly code = \"BRANCHING_UNSUPPORTED\";\n\n constructor(message: string) {\n super(message);\n this.name = \"BranchingUnsupportedError\";\n }\n}\n\n/**\n * What to say when the server serves one database.\n *\n * Deliberately the same argument the CLI's `refuseBranchOnManagedDatabase`\n * makes, because it is the same refusal — it just used to be available only to\n * whoever came in through argv. One paragraph, because this one has to fit in a\n * toast as well as a terminal.\n */\nconst BRANCHING_UNSUPPORTED_MESSAGE =\n \"Branching does not work on the managed development database. It copies a database with \"\n + \"CREATE DATABASE ... TEMPLATE, and the managed database is PGlite, which serves exactly one — \"\n + \"the copy would be the original, and every write you meant to sandbox would land in it. \"\n + \"Branching needs a real Postgres: run `rebase dev --docker`, or uncomment DATABASE_URL in .env \"\n + \"to point at your own.\";\n\n/**\n * Validate that a user-provided identifier only contains safe characters.\n * Throws if the value contains characters outside [a-zA-Z0-9_-].\n */\nfunction validateIdentifier(value: string, label: string): void {\n if (!/^[a-zA-Z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);\n }\n}\n\n/**\n * Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it\n * silently. The prefix comes out of the same budget.\n */\nconst MAX_BRANCH_NAME_LENGTH = 63 - BRANCH_DB_PREFIX.length;\n\n/**\n * Check a user-provided branch name, and otherwise leave it exactly as given.\n *\n * This used to strip everything outside [a-zA-Z0-9_], so `my-feature` was\n * quietly created as `myfeature`: the name you typed was not the name `list`\n * gave back. Nothing ever needed that. Every identifier this service builds is\n * double-quoted (see `CREATE DATABASE` below), which is what makes a hyphen\n * safe, and `validateIdentifier` has always accepted hyphens for the `--from`\n * source database — the two disagreed about the same character class.\n *\n * Refusing a name we cannot represent is better than representing a different\n * one, which is also why the length is checked here rather than left to\n * Postgres, whose answer to an over-long identifier is a silent rename.\n */\nfunction assertValidBranchName(name: string): void {\n validateIdentifier(name, \"branch name\");\n if (name.length > MAX_BRANCH_NAME_LENGTH) {\n throw new Error(\n `Branch name \"${name}\" is too long: ${name.length} characters, maximum ${MAX_BRANCH_NAME_LENGTH}. ` +\n \"Postgres truncates identifiers past 63 bytes, which would give the branch a name you did not choose.\"\n );\n }\n}\n\n/**\n * Convert a user-facing branch name to the actual PostgreSQL database name.\n */\nfunction toBranchDbName(name: string): string {\n assertValidBranchName(name);\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/**\n * Where a server's branches are kept: the main database — what a branch is\n * copied from unless told otherwise, and what may never be dropped — and the\n * connection whose `rebase.branches` records them.\n */\nexport interface BranchHome {\n /** The main database's name. */\n database(): Promise<string>;\n /**\n * A connection on the database the registry is in. Asked for at each use:\n * a pool to the main database is closed before it is copied.\n */\n registry(): Promise<DrizzleClient>;\n}\n\nexport class BranchService {\n private readonly home: BranchHome;\n\n /**\n * @param db A connection on the server the branches are made on. Branch\n * DDL runs here.\n * @param home Where branches are kept. Without one, the database the\n * pool manager's connection string names, and `db`'s registry.\n */\n constructor(\n private db: DrizzleClient,\n private poolManager: DatabasePoolManager,\n home?: BranchHome\n ) {\n this.home = home ?? {\n database: async () => poolManager.defaultDatabaseName,\n registry: async () => db\n };\n }\n\n /**\n * Refuse a branch mutation the connected server cannot honour.\n *\n * The check lives here rather than in the CLI's argv parsing because the\n * CLI is not the only caller: Studio reaches `createBranch` over the\n * websocket, and before this it got \"Branch created successfully\" and a\n * detail pane promising isolation for a database that is the parent.\n *\n * Reads only, and only mutations are guarded: `listBranches` on a managed\n * database is an honest empty list, and refusing it would turn the Branches\n * pane into a failure with a \"Try again\" that can never succeed.\n */\n private async assertBranchingSupported(): Promise<void> {\n if (await this.poolManager.servesOneDatabase()) {\n throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);\n }\n }\n\n /**\n * Whether the `rebase.branches` registry exists. A database that has never\n * had a branch made on it may not have it, and reading it there is an\n * empty answer, not an error — nor a reason to create it. The CLI's `list`\n * and `info` ask this rather than running the DDL that creates it.\n */\n async hasBranchRegistry(): Promise<boolean> {\n const result = await (await this.home.registry()).execute(\n sql`SELECT to_regclass('rebase.branches') IS NOT NULL AS present`\n );\n const [row] = result.rows;\n return row !== undefined && Reflect.get(row, \"present\") === true;\n }\n\n /**\n * Ensure the `rebase.branches` metadata table exists in the default database.\n * Idempotent — safe to call on every startup.\n */\n async ensureBranchMetadataTable(): Promise<void> {\n // Create the rebase schema (idempotent — may already exist from auth/history init)\n const registry = await this.home.registry();\n await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await registry.execute(sql.raw(`\n CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (\n name TEXT PRIMARY KEY,\n db_name TEXT NOT NULL UNIQUE,\n parent_db TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB DEFAULT '{}'\n );\n `));\n\n // Not a collection, so no RLS — and it names every branch database on\n // this server. The driver's schema-wide grant reaches it, so revoke.\n await registry.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"branches\")));\n }\n\n /**\n * Create a new branch database by templating the source database.\n *\n * Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy\n * of both schema and data.\n *\n * @param name User-facing branch name (e.g., \"feature_auth\")\n * @param options.source Source database to clone; defaults to the main database.\n */\n async createBranch(\n name: string,\n options?: { source?: string; force?: boolean }\n ): Promise<BranchInfo> {\n await this.assertBranchingSupported();\n\n if (options?.source) {\n validateIdentifier(options.source, \"source database name\");\n }\n\n const dbName = toBranchDbName(name);\n const sourceDb = options?.source || await this.home.database();\n\n // Check if branch already exists\n const existing = await (await this.home.registry()).execute(\n sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`\n );\n if ((existing.rows as unknown[]).length > 0) {\n throw new Error(`Branch \"${name}\" already exists.`);\n }\n\n // Disconnect any idle pools to the source DB so TEMPLATE works.\n // CREATE DATABASE ... TEMPLATE requires no other connections to the template.\n //\n // This only reaches pools inside *this* process. The CLI runs as its own\n // process, so a `rebase dev` holding the database open is untouched by\n // it — which is why the failure below has to name what is connected, and\n // why `--force` exists.\n await this.poolManager.disconnectDatabase(sourceDb);\n\n if (options?.force) {\n const closed = await this.terminateConnections(sourceDb);\n if (closed > 0) {\n // Postgres reports the backend as gone before its slot is\n // actually free, and CREATE DATABASE then fails on a connection\n // that no longer exists. One short settle beats a confusing retry.\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n\n // Create the database using the source as a template.\n // Note: Identifiers must be double-quoted, not parameterized.\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n const safeSourceDb = sourceDb.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(\n sql.raw(`CREATE DATABASE \"${safeDbName}\" TEMPLATE \"${safeSourceDb}\"`)\n );\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n // The template — not the new database — is the one still in use.\n throw new Error(\n `Cannot create branch: the source database \"${sourceDb}\" has active connections.\\n`\n + await this.describeBlockingConnections(sourceDb)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Record metadata in the registry — asked for again, as copying the\n // main database closed any pool on it.\n const now = new Date();\n await (await this.home.registry()).execute(\n sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)\n VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`\n );\n\n return {\n name,\n database: dbName,\n parentDatabase: sourceDb,\n createdAt: now\n };\n }\n\n /**\n * Delete a branch database and remove its metadata.\n * Cannot delete the main/default database.\n */\n async deleteBranch(name: string, options?: { force?: boolean }): Promise<void> {\n await this.assertBranchingSupported();\n\n assertValidBranchName(name);\n\n // Safety, first pass: a request that would target the default database\n // is refused before any metadata is read, so the answer costs nothing\n // and cannot depend on what a row happens to say.\n const mainDatabase = await this.home.database();\n if (toBranchDbName(name) === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Verify the branch exists, and take the database name from the row\n // rather than deriving it again. A branch created before names stopped\n // being stripped is recorded as `rb_myfeature` while `my-feature` now\n // derives `rb_my-feature`; re-deriving would drop a database this row\n // never named — or, if that name happened to exist, somebody else's.\n // The stored value is the only one that is true by construction.\n const existing = await (await this.home.registry()).execute(\n sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`\n );\n const existingRows = existing.rows as Record<string, unknown>[];\n if (existingRows.length === 0) {\n throw new Error(`Branch \"${name}\" not found.`);\n }\n const dbName = existingRows[0].db_name as string;\n\n // Safety, second pass: the first checked a name we derived, this checks\n // the one we are about to drop. They differ for any row written under\n // the old scheme, and it is this one that governs.\n if (dbName === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Disconnect any pools to this branch before dropping. Same limit as\n // `createBranch`: in-process pools only.\n await this.poolManager.disconnectDatabase(dbName);\n\n if (options?.force) {\n const closed = await this.terminateConnections(dbName);\n if (closed > 0) await new Promise((resolve) => setTimeout(resolve, 250));\n }\n\n // Drop the database\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${safeDbName}\"`));\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n throw new Error(\n `Cannot delete branch \"${name}\": the database has active connections.\\n`\n + await this.describeBlockingConnections(dbName)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Remove metadata\n await (await this.home.registry()).execute(\n sql`DELETE FROM rebase.branches WHERE name = ${name}`\n );\n }\n\n /**\n * Who is holding the database open, in words a developer can act on.\n *\n * `CREATE DATABASE ... TEMPLATE` and `DROP DATABASE` both require that no\n * other session is connected, and the one thing the old message could not\n * say is the only thing worth knowing: *which* session. \"Close other\n * clients or connections and try again\" is advice you cannot follow when\n * you do not know there is a `rebase dev` in another terminal — and that is\n * the common case, because wanting a branch and running the app are the\n * same moment.\n *\n * `application_name` is the useful column: node-postgres sends none by\n * default, so a Rebase process shows as `(unnamed)` while DBeaver, pgAdmin\n * and psql all name themselves. Counting by it separates \"my own dev\n * server\" from \"the GUI I forgot about\" without any guessing.\n *\n * Best-effort by construction: this runs while reporting a failure, and a\n * diagnostic that throws would replace a real error with its own.\n */\n private async describeBlockingConnections(dbName: string): Promise<string> {\n try {\n const result = await this.db.execute(sql`\n SELECT coalesce(nullif(application_name, ''), '(unnamed)') AS app,\n count(*)::int AS n\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n GROUP BY 1\n ORDER BY 2 DESC, 1\n `);\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) {\n // The blocker went away between the failure and this query, or\n // it is a session Postgres does not attribute to this database.\n return \" Retry — whatever was connected has since disconnected.\";\n }\n\n const listed = rows\n .map((row) => ` ${row.n} × ${String(row.app)}`)\n .join(\"\\n\");\n\n return ` Connected right now:\\n${listed}\\n`\n + \" A running `rebase dev` is the usual one — stop it, or re-run with --force to\\n\"\n + \" disconnect them for you.\";\n } catch {\n return \" Close other clients and connections, or re-run with --force.\";\n }\n }\n\n /**\n * Disconnect every other session on a database, and say how many.\n *\n * `pg_terminate_backend` rather than `pg_cancel_backend`: cancelling a\n * query leaves the session connected, and a connected session is exactly\n * what blocks the operation.\n *\n * The current backend is excluded — terminating the connection running this\n * statement would abort the command that asked for it.\n */\n private async terminateConnections(dbName: string): Promise<number> {\n const result = await this.db.execute(sql`\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n `);\n\n return (result.rows as unknown[]).length;\n }\n\n /**\n * Everything prune needs to decide, in two queries.\n *\n * Reads the rows and the server's database list rather than joining them,\n * so the *disagreements* between the two are visible — a row whose database\n * is gone and a database with no row are the two things prune exists to\n * find, and a join hides both.\n */\n async pruneCandidates(): Promise<{ rows: BranchRow[]; databases: string[] }> {\n const rowResult = await (await this.home.registry()).execute(sql.raw(`\n SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at\n `));\n const rows = (rowResult.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n dbName: row.db_name as string,\n createdAt: new Date(row.created_at as string)\n }));\n\n const dbResult = await this.db.execute(sql`\n SELECT datname FROM pg_database WHERE NOT datistemplate\n `);\n const databases = (dbResult.rows as Record<string, unknown>[]).map((row) => String(row.datname));\n\n return { rows, databases };\n }\n\n /** Remove a metadata row whose database is already gone. */\n async forgetBranchRow(name: string): Promise<void> {\n await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);\n }\n\n /**\n * Drop a database by name, with no metadata row required.\n *\n * `deleteBranch` deliberately takes its database name from the row, because\n * the row is the only value that is true by construction. Prune has the\n * opposite job — the cases it handles are exactly the ones where row and\n * database disagree — so it needs to name a database directly. Guarded the\n * same way regardless: the main database is never droppable.\n */\n async dropDatabase(dbName: string): Promise<void> {\n validateIdentifier(dbName, \"database name\");\n if (dbName === await this.home.database()) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n await this.poolManager.disconnectDatabase(dbName);\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${dbName.replace(/\"/g, '\"\"')}\"`));\n } catch (err) {\n throw describeBranchDdlError(err, dbName);\n }\n }\n\n /**\n * List all branches recorded in the metadata table.\n * Optionally fetches database sizes from pg_database.\n */\n async listBranches(): Promise<BranchInfo[]> {\n const result = await (await this.home.registry()).execute(sql.raw(`\n SELECT \n b.name,\n b.db_name,\n b.parent_db,\n b.created_at,\n pg_database_size(b.db_name) as size_bytes\n FROM ${BRANCHES_TABLE} b\n JOIN pg_database d ON d.datname = b.db_name\n ORDER BY b.created_at DESC\n `));\n\n return (result.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined\n }));\n }\n\n /**\n * Get info about a specific branch.\n */\n async getBranchInfo(name: string): Promise<BranchInfo | undefined> {\n assertValidBranchName(name);\n\n const result = await (await this.home.registry()).execute(sql`\n SELECT\n b.name,\n b.db_name,\n b.parent_db,\n b.created_at\n FROM rebase.branches b\n WHERE b.name = ${name}\n `);\n\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) return undefined;\n\n const row = rows[0];\n\n // Attempt to get size — may fail if the DB was externally dropped.\n // Same reason as `deleteBranch`: the size of a re-derived name is the\n // size of some other database, or of nothing.\n let sizeBytes: number | undefined;\n try {\n const dbName = row.db_name as string;\n const sizeResult = await this.db.execute(\n sql`SELECT pg_database_size(${dbName}) as size_bytes`\n );\n const sizeRows = sizeResult.rows as Record<string, unknown>[];\n if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {\n sizeBytes = Number(sizeRows[0].size_bytes);\n }\n } catch {\n // Database might not exist anymore\n }\n\n return {\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { BranchService } from "./services/BranchService.js";
|
|
|
3
3
|
import { RealtimeService, type SubscriptionAuthContext } from "./services/realtimeService.js";
|
|
4
4
|
import { DatabasePoolManager } from "./databasePoolManager.js";
|
|
5
5
|
import { DrizzleClient } from "./interfaces.js";
|
|
6
|
-
import { DatabaseAdmin, SQLAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, SqlScriptResult, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
|
|
6
|
+
import { DatabaseAdmin, SQLAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseCallContext, RebaseServerClient, RebaseSdkData, RestFetchService, BatchWriteProps, SaveManyProps, SaveProps, SqlCursor, SqlScriptResult, UpdateManyProps, UpdateRelationPivotProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types";
|
|
7
7
|
import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry.js";
|
|
8
8
|
import { HistoryService } from "./history/HistoryService.js";
|
|
9
9
|
/**
|
|
@@ -72,6 +72,8 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
72
72
|
poolManager?: DatabasePoolManager | undefined;
|
|
73
73
|
key: string;
|
|
74
74
|
initialised: boolean;
|
|
75
|
+
/** `save` and `delete` read the row they address and run a caller's precondition on it. */
|
|
76
|
+
readonly checksRowPreconditions = true;
|
|
75
77
|
dataService: DataService;
|
|
76
78
|
realtimeService: RealtimeService;
|
|
77
79
|
historyService?: HistoryService;
|
|
@@ -108,6 +110,13 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
108
110
|
* migrations, `dataAsAdmin`).
|
|
109
111
|
*/
|
|
110
112
|
rlsUserRole?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The direct (non-pooled) URL {@link listen} holds its `LISTEN` on — this
|
|
115
|
+
* source's, set by the bootstrapper. Never read from `DATABASE_DIRECT_URL`
|
|
116
|
+
* here: that variable is the default source's, and a second database
|
|
117
|
+
* listening on it hears none of its own notifications.
|
|
118
|
+
*/
|
|
119
|
+
listenConnectionString?: string;
|
|
111
120
|
/**
|
|
112
121
|
* When true, realtime notifications are deferred until after the
|
|
113
122
|
* wrapping transaction commits. Set by `withAuth` → `withTransaction`.
|
|
@@ -126,6 +135,20 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
126
135
|
* allowing test spies applied after construction to take effect.
|
|
127
136
|
*/
|
|
128
137
|
get admin(): DatabaseAdmin;
|
|
138
|
+
/**
|
|
139
|
+
* `SQLAdmin.listen`: a `LISTEN` on a connection of its own, through the
|
|
140
|
+
* listener CDC and the channel bus use, which reconnects and heartbeats.
|
|
141
|
+
*
|
|
142
|
+
* The connection is the one the realtime listener uses: this source's
|
|
143
|
+
* direct URL ({@link listenConnectionString}) when the bootstrapper
|
|
144
|
+
* resolved one — a transaction-mode pooler such as PgBouncer does not keep
|
|
145
|
+
* a `LISTEN` — else the pool's own URL. The in-process development
|
|
146
|
+
* database has neither, and is refused, as is a first connect that fails:
|
|
147
|
+
* the caller polls instead.
|
|
148
|
+
*/
|
|
149
|
+
listen(channel: string, onNotify: (payload: string) => void, options?: {
|
|
150
|
+
onReconnect?: () => void;
|
|
151
|
+
}): Promise<() => Promise<void>>;
|
|
129
152
|
/**
|
|
130
153
|
* The catalogue-reading shim the schema planner wants.
|
|
131
154
|
*
|
|
@@ -190,6 +213,13 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
190
213
|
* collection's afterRead (so `post.author.email` is masked by the authors
|
|
191
214
|
* collection, not left raw).
|
|
192
215
|
*/
|
|
216
|
+
/**
|
|
217
|
+
* One stored row as `restFetchService.fetchOneForRest` serves it: through
|
|
218
|
+
* `afterRead`, on a copy, so the row the write itself goes on with (its
|
|
219
|
+
* previous values, its history) stays the stored one. A precondition
|
|
220
|
+
* compares against what the caller was served — an `ETag` is a hash of that.
|
|
221
|
+
*/
|
|
222
|
+
private asRestServes;
|
|
193
223
|
applyAfterReadForRest(rows: Record<string, unknown>[], path: string): Promise<Record<string, unknown>[]>;
|
|
194
224
|
fetchCollection<M extends Record<string, unknown>>({ path, collection, startAfter, ...query }: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
|
|
195
225
|
/**
|
|
@@ -208,7 +238,7 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
208
238
|
* `null` when it is not there — deleted, never created, or not readable by
|
|
209
239
|
* the subscriber. Reads as {@link listenCollection} does.
|
|
210
240
|
*/
|
|
211
|
-
listenOne<M extends Record<string, unknown>>({ path, id, onUpdate, onError }: ListenOneProps<M>, authContext?: SubscriptionAuthContext | undefined): () => void;
|
|
241
|
+
listenOne<M extends Record<string, unknown>>({ path, id, include, onUpdate, onError }: ListenOneProps<M>, authContext?: SubscriptionAuthContext | undefined): () => void;
|
|
212
242
|
/**
|
|
213
243
|
* How many membership rows a single write will read to answer "which
|
|
214
244
|
* tenants is this caller in".
|
|
@@ -253,6 +283,19 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
253
283
|
* path, such a row is absent, so it is read from its own collection.
|
|
254
284
|
*/
|
|
255
285
|
private storedRowPath;
|
|
286
|
+
/**
|
|
287
|
+
* The row an update addresses, as this caller reads it there: what a
|
|
288
|
+
* caller's precondition (the REST routes' 404 and `If-Match`) judges.
|
|
289
|
+
*
|
|
290
|
+
* `stored` is the row read from {@link storedRowPath}. Off a many-to-many
|
|
291
|
+
* path that is the address itself. Through one, the row is judged as a
|
|
292
|
+
* member of the parent's set when it is one. When it is not yet, this
|
|
293
|
+
* update is the link that makes it one (the persistence layer writes the
|
|
294
|
+
* junction row), so the row is judged as it is stored — provided the caller
|
|
295
|
+
* can read the parent it is being linked to. A parent that is not there, or
|
|
296
|
+
* that their policies hide, is no set to join, and the row reads as absent.
|
|
297
|
+
*/
|
|
298
|
+
private rowAnUpdateAddresses;
|
|
256
299
|
/**
|
|
257
300
|
* The stored row an upsert's key names, if this caller can address it —
|
|
258
301
|
* with its address and the values to update it with.
|
|
@@ -293,6 +336,17 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
293
336
|
* Rows are applied in order, so a batch that touches the same key twice ends
|
|
294
337
|
* with the last write winning, exactly as separate calls would.
|
|
295
338
|
*/
|
|
339
|
+
/**
|
|
340
|
+
* Run a batch in one transaction of its own, on a driver bound to it, and
|
|
341
|
+
* tell realtime subscribers about its rows only once it has committed.
|
|
342
|
+
*
|
|
343
|
+
* Inside a request's transaction the events already wait for that one to
|
|
344
|
+
* commit, and the batch adds to its list. On the server's own driver they
|
|
345
|
+
* did not wait at all: each row was announced as it was written, before
|
|
346
|
+
* the COMMIT — a subscriber's refetch found nothing yet, and a batch that
|
|
347
|
+
* then rolled back had announced rows that never existed.
|
|
348
|
+
*/
|
|
349
|
+
private inBatchTransaction;
|
|
296
350
|
private bindToTransaction;
|
|
297
351
|
/**
|
|
298
352
|
* The callback context for work already running inside `tx` on behalf of
|
|
@@ -357,6 +411,21 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
357
411
|
batchWrite<M extends Record<string, unknown>>({ operations }: BatchWriteProps<M>): Promise<(Record<string, unknown> | null)[]>;
|
|
358
412
|
delete<M extends Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;
|
|
359
413
|
private deleteInFrame;
|
|
414
|
+
/**
|
|
415
|
+
* The child lists a save rewrote through the parent, as notifications —
|
|
416
|
+
* when nothing else will announce them.
|
|
417
|
+
*
|
|
418
|
+
* `save("posts", "p-1", { tags: [...] })` rewrites the junction, and
|
|
419
|
+
* `{ comments: [...] }` on a `hasMany` re-points the children's foreign
|
|
420
|
+
* keys: either way the contents of `posts/p-1/<relation>` changed, and the
|
|
421
|
+
* row notification names the parent, whose paths land on `posts`. Under
|
|
422
|
+
* change capture the junction's or the children's own triggers announce
|
|
423
|
+
* it. Without capture these did not reach anyone: a live list of a post's
|
|
424
|
+
* tags kept the old set. Addressed to every row of the list, which also
|
|
425
|
+
* reaches the target collection's own lists — a `hasMany` changed those
|
|
426
|
+
* rows' foreign keys.
|
|
427
|
+
*/
|
|
428
|
+
private childListNotifications;
|
|
360
429
|
deleteAll(path: string): Promise<void>;
|
|
361
430
|
updateRelationPivot({ path, targetId, pivot }: UpdateRelationPivotProps): Promise<void>;
|
|
362
431
|
checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise<boolean>;
|
|
@@ -388,6 +457,7 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
388
457
|
role?: string;
|
|
389
458
|
params?: unknown[];
|
|
390
459
|
isolateSession?: boolean;
|
|
460
|
+
maxResultBytes?: number;
|
|
391
461
|
}): Promise<Record<string, unknown>[]>;
|
|
392
462
|
/**
|
|
393
463
|
* Run a script a person wrote — the Studio console — and describe its
|
|
@@ -396,7 +466,19 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
396
466
|
runSqlScript(sqlText: string, options?: {
|
|
397
467
|
database?: string;
|
|
398
468
|
role?: string;
|
|
469
|
+
signal?: AbortSignal;
|
|
399
470
|
}): Promise<SqlScriptResult>;
|
|
471
|
+
/**
|
|
472
|
+
* Read the whole result of one query a person wrote, a batch at a time —
|
|
473
|
+
* see `SQLAdmin.openSqlCursor` and `openSqlCursorOnPool`. The database's
|
|
474
|
+
* refusal of the query (a write, a second statement, a syntax error, a
|
|
475
|
+
* role it may not take) is the caller's to read, as on the socket.
|
|
476
|
+
*/
|
|
477
|
+
openSqlCursor(sqlText: string, options?: {
|
|
478
|
+
database?: string;
|
|
479
|
+
role?: string;
|
|
480
|
+
batchSize?: number;
|
|
481
|
+
}): Promise<SqlCursor>;
|
|
400
482
|
/**
|
|
401
483
|
* `SET ROLE` for the rest of a session of a script's own — not `SET LOCAL`
|
|
402
484
|
* inside a transaction, which a `COMMIT` in the script ends, and every
|
|
@@ -426,6 +508,10 @@ export declare class PostgresBackendDriver implements DataDriver {
|
|
|
426
508
|
* `onOwnSession`. `fn` is then handed no role: there is nothing left to
|
|
427
509
|
* switch. A handle that is already one session (PGlite in process, a
|
|
428
510
|
* single client) is reset in place, and `fn` switches the role itself.
|
|
511
|
+
*
|
|
512
|
+
* With `maxResultBytes`, a result past it is refused — see
|
|
513
|
+
* `readWithinBudget`: on a pool the read stops there, in process the rows
|
|
514
|
+
* are measured once read.
|
|
429
515
|
*/
|
|
430
516
|
private onIsolatedSession;
|
|
431
517
|
private executeSqlOn;
|
|
@@ -472,6 +558,8 @@ export declare class AuthenticatedPostgresBackendDriver implements DataDriver {
|
|
|
472
558
|
delegate: PostgresBackendDriver;
|
|
473
559
|
key: string;
|
|
474
560
|
initialised: boolean;
|
|
561
|
+
/** Its delegate's, run inside this caller's transaction. */
|
|
562
|
+
readonly checksRowPreconditions = true;
|
|
475
563
|
user: User;
|
|
476
564
|
data: RebaseSdkData;
|
|
477
565
|
constructor(delegate: PostgresBackendDriver, user: User);
|
|
@@ -480,6 +568,20 @@ export declare class AuthenticatedPostgresBackendDriver implements DataDriver {
|
|
|
480
568
|
*/
|
|
481
569
|
admin: DatabaseAdmin;
|
|
482
570
|
get restFetchService(): RestFetchService;
|
|
571
|
+
/**
|
|
572
|
+
* Several reads in one read-only transaction — see
|
|
573
|
+
* {@link DataDriver.readTogether}. The same transaction every read on this
|
|
574
|
+
* driver opens for itself, with the same identity, role and field viewer;
|
|
575
|
+
* `work` is handed the transaction's driver, whose reads run on it directly
|
|
576
|
+
* instead of each opening another.
|
|
577
|
+
*
|
|
578
|
+
* `REPEATABLE READ`, because the point of reading together is reading one
|
|
579
|
+
* snapshot: under the default `READ COMMITTED` each statement takes its
|
|
580
|
+
* own, and a count could still describe a different set from the page
|
|
581
|
+
* beside it. A read-only transaction cannot fail serialization at this
|
|
582
|
+
* level, so it costs nothing.
|
|
583
|
+
*/
|
|
584
|
+
readTogether<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;
|
|
483
585
|
private withTransaction;
|
|
484
586
|
fetchCollection<M extends Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;
|
|
485
587
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Implements the `BackendBootstrapper` interface for PostgreSQL.
|
|
5
5
|
*/
|
|
6
6
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
7
|
-
import { BackendBootstrapper, CollectionConfig, type RealtimeChannelsConfig } from "@rebasepro/types";
|
|
7
|
+
import { BackendBootstrapper, CollectionConfig, InitializedDriver, type DatabaseAdapterInitConfig, type RealtimeChannelsConfig } from "@rebasepro/types";
|
|
8
8
|
import { PostgresBackendDriver } from "./PostgresBackendDriver.js";
|
|
9
9
|
import { RealtimeService } from "./services/realtimeService.js";
|
|
10
10
|
import { DatabasePoolManager } from "./databasePoolManager.js";
|
|
@@ -14,7 +14,27 @@ import { type UnknownFilterFieldsMode } from "./utils/drizzle-conditions.js";
|
|
|
14
14
|
export interface PostgresDriverConfig {
|
|
15
15
|
connectionString?: string;
|
|
16
16
|
adminConnectionString?: string;
|
|
17
|
+
/**
|
|
18
|
+
* This database's read replica: reads are routed there, writes stay on
|
|
19
|
+
* the primary. Unset, the boot ({@link postgresDriverConfigFromEnv}) fills
|
|
20
|
+
* it from `DATABASE_READ_URL` for the default source only; any other
|
|
21
|
+
* source has none unless its config names one — the managed runtime
|
|
22
|
+
* resolves `DATABASE_READ_URL__<KEY>` into this for each source.
|
|
23
|
+
*/
|
|
17
24
|
readConnectionString?: string;
|
|
25
|
+
/**
|
|
26
|
+
* A direct (non-pooled) URL to this database, for the connections that
|
|
27
|
+
* hold a `LISTEN`: change capture, cross-instance realtime, the Postgres
|
|
28
|
+
* channel bus and `admin.listen`. A transaction-mode pooler such as
|
|
29
|
+
* PgBouncer does not keep a `LISTEN`, so behind one this has to name the
|
|
30
|
+
* database itself.
|
|
31
|
+
*
|
|
32
|
+
* Unset, the boot fills it from `DATABASE_DIRECT_URL` for the default
|
|
33
|
+
* source only, and any other source listens on its own
|
|
34
|
+
* `connectionString` — see {@link resolveSourceUrls}. The managed runtime
|
|
35
|
+
* resolves `DATABASE_DIRECT_URL__<KEY>` into this for each source.
|
|
36
|
+
*/
|
|
37
|
+
directConnectionString?: string;
|
|
18
38
|
connection?: unknown;
|
|
19
39
|
/**
|
|
20
40
|
* The project's generated Drizzle module, for **diagnostics only**.
|
|
@@ -37,13 +57,17 @@ export interface PostgresDriverConfig {
|
|
|
37
57
|
*/
|
|
38
58
|
introspectionSchema?: string;
|
|
39
59
|
/**
|
|
40
|
-
* Realtime options,
|
|
60
|
+
* Realtime options, among them:
|
|
41
61
|
*
|
|
42
62
|
* - `channels` — retention. Without rules no channel keeps any history and
|
|
43
63
|
* broadcast stays fire-and-forget. See {@link ChannelRetentionRule}.
|
|
44
64
|
* - `bus` — the cross-instance transport for channel broadcast and
|
|
45
65
|
* presence. Defaults to in-process only, which is correct for a single
|
|
46
66
|
* instance and wrong for two. See {@link ChannelBusConfig}.
|
|
67
|
+
* - `cdc` — how changes are captured (`auto` by default).
|
|
68
|
+
*
|
|
69
|
+
* At boot each has an environment variable that wins over it — see
|
|
70
|
+
* {@link postgresDriverConfigFromEnv}.
|
|
47
71
|
*/
|
|
48
72
|
realtime?: RealtimeChannelsConfig;
|
|
49
73
|
/**
|
|
@@ -65,6 +89,13 @@ export interface PostgresDriverInternals {
|
|
|
65
89
|
realtimeService: RealtimeService;
|
|
66
90
|
driver: PostgresBackendDriver;
|
|
67
91
|
poolManager?: DatabasePoolManager;
|
|
92
|
+
/**
|
|
93
|
+
* Whether the driver was opened with provisioning (`provision`, default
|
|
94
|
+
* true). The hooks that run after it — auth, history, the final security
|
|
95
|
+
* posture — read it, so `provision: false` means no DDL anywhere on the
|
|
96
|
+
* driver's path, not only while it opens.
|
|
97
|
+
*/
|
|
98
|
+
provisioned: boolean;
|
|
68
99
|
/**
|
|
69
100
|
* Attach CDC triggers to tables that did not exist when the driver
|
|
70
101
|
* bootstrapped. Only set when database-level capture is actually active.
|
|
@@ -118,6 +149,27 @@ export declare function describeSchemaDriftCause(provisioning: {
|
|
|
118
149
|
attempted: boolean;
|
|
119
150
|
reason?: string;
|
|
120
151
|
} | undefined): string[];
|
|
152
|
+
/**
|
|
153
|
+
* The URLs one source's driver opens besides its pool: the read replica, and
|
|
154
|
+
* the direct connection its `LISTEN`s are held on.
|
|
155
|
+
*
|
|
156
|
+
* The bare `DATABASE_READ_URL` and `DATABASE_DIRECT_URL` belong to the default
|
|
157
|
+
* source. Both used to be read here for every source, so a second database
|
|
158
|
+
* (`database("analytics")`) held its change-capture `LISTEN` on the default
|
|
159
|
+
* database — where none of its notifications are sent, so its realtime feed
|
|
160
|
+
* was silent — and read through the default's replica. A keyed source gets
|
|
161
|
+
* the URLs its config names, and otherwise its own connection and no replica.
|
|
162
|
+
*
|
|
163
|
+
* The default source still falls back to the environment: an application that
|
|
164
|
+
* builds this adapter itself passes a `connectionString` and nothing else, and
|
|
165
|
+
* has always had those two variables honoured.
|
|
166
|
+
*
|
|
167
|
+
* Exported for its own test; the caller needs a live database.
|
|
168
|
+
*/
|
|
169
|
+
export declare function resolveSourceUrls(pgConfig: Pick<PostgresDriverConfig, "connectionString" | "readConnectionString" | "directConnectionString">, sourceKey: string, env?: Record<string, string | undefined>): {
|
|
170
|
+
readUrl?: string;
|
|
171
|
+
directUrl?: string;
|
|
172
|
+
};
|
|
121
173
|
/**
|
|
122
174
|
* Is this the local database `rebase init` scaffolds — i.e. the one case where
|
|
123
175
|
* "you are connected as a superuser" is not news?
|
|
@@ -144,4 +196,51 @@ export declare function describeSchemaDriftCause(provisioning: {
|
|
|
144
196
|
* ships `NODE_ENV=development` and some deployments inherit it.
|
|
145
197
|
*/
|
|
146
198
|
export declare function isScaffoldedLocalDatabase(connectionString: string | undefined): boolean;
|
|
199
|
+
/**
|
|
200
|
+
* `config` with the environment read into it, the way the server's boot reads
|
|
201
|
+
* it. The one place this driver reads environment variables:
|
|
202
|
+
*
|
|
203
|
+
* | variable | setting |
|
|
204
|
+
* |----------------------------------------|----------------------------------------|
|
|
205
|
+
* | `DATABASE_READ_URL` | `readConnectionString`, when unset (*) |
|
|
206
|
+
* | `DATABASE_DIRECT_URL` | `directConnectionString`, when unset (*)|
|
|
207
|
+
* | `REALTIME_CDC` | `realtime.cdc` |
|
|
208
|
+
* | `REALTIME_CHANNEL_BUS` | `realtime.bus` (a named transport) |
|
|
209
|
+
* | `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET`| `realtime.maxSubscriptionsPerSocket` |
|
|
210
|
+
* | `REALTIME_MAX_SOCKETS_PER_USER` | `realtime.maxSocketsPerUser` |
|
|
211
|
+
* | `REALTIME_REFETCH_MAX_WAIT_MS` | `realtime.refetchMaxWaitMs` |
|
|
212
|
+
* | `REALTIME_RECOUNT_INTERVAL_MS` | `realtime.recountIntervalMs` |
|
|
213
|
+
* | `REALTIME_RECOUNT_MIN_ROWS` | `realtime.recountMinRows` |
|
|
214
|
+
*
|
|
215
|
+
* A `REALTIME_*` variable that is set wins over the option, so a deployment
|
|
216
|
+
* can change it without a rebuild; the URLs fill in only what the config left
|
|
217
|
+
* empty. A mistyped number throws, naming the variable.
|
|
218
|
+
*
|
|
219
|
+
* (*) For the default source only. The bare URL variables name the default
|
|
220
|
+
* database; read for a second source (`database("analytics")`), they put its
|
|
221
|
+
* change-capture `LISTEN` on the default database, where none of its
|
|
222
|
+
* notifications arrive, and its reads on the default's replica. A keyed
|
|
223
|
+
* source's URLs come from its own config — the managed runtime passes
|
|
224
|
+
* `DATABASE_*_URL__<KEY>` there — see {@link resolveSourceUrls}.
|
|
225
|
+
*
|
|
226
|
+
* `createPostgresAdapter` applies this when the server boots, and
|
|
227
|
+
* {@link openPostgresDriver} does not — a script that opens the driver itself
|
|
228
|
+
* gets exactly the config it passed.
|
|
229
|
+
*/
|
|
230
|
+
export declare function postgresDriverConfigFromEnv(config: PostgresDriverConfig, env?: NodeJS.ProcessEnv, sourceKey?: string): PostgresDriverConfig;
|
|
231
|
+
/**
|
|
232
|
+
* Open the Postgres data driver from explicit settings.
|
|
233
|
+
*
|
|
234
|
+
* What `createPostgresAdapter` runs when the server boots, once it has read
|
|
235
|
+
* the environment into `pgConfig` (see {@link postgresDriverConfigFromEnv});
|
|
236
|
+
* this function reads no environment variable itself. `config` says what the
|
|
237
|
+
* caller wants opened: which collections, whether opening may change the
|
|
238
|
+
* database (`provision`, default true), and what part this process takes in
|
|
239
|
+
* realtime (`realtime`, or `false` for none at all).
|
|
240
|
+
*
|
|
241
|
+
* `openRebase` is this with `provision: false` and `realtime: false`.
|
|
242
|
+
*/
|
|
243
|
+
export declare function openPostgresDriver(pgConfig: PostgresDriverConfig, config: DatabaseAdapterInitConfig): Promise<InitializedDriver & {
|
|
244
|
+
internals: PostgresDriverInternals;
|
|
245
|
+
}>;
|
|
147
246
|
export declare function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): BackendBootstrapper;
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import type { CollectionConfig } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* One statement that drops each named index — but only one that covers a
|
|
5
|
+
* single column that a valid, unique, non-partial btree on the same table also
|
|
6
|
+
* leads with. That is the UNIQUE (token_hash) constraint's index, which serves
|
|
7
|
+
* every `token_hash = $1` lookup; a table that somehow lacks it keeps its
|
|
8
|
+
* plain index, so no lookup is ever left without one.
|
|
9
|
+
*
|
|
10
|
+
* `DROP INDEX` takes the table's ACCESS EXCLUSIVE lock, on the tables every
|
|
11
|
+
* sign-in and token refresh touches, so it runs under the boot lock budget
|
|
12
|
+
* (`lock_timeout`, transaction-local to this statement — see `boot-ddl`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function dropDuplicateTokenHashIndexesSql(schema: string, names: readonly string[]): string;
|
|
3
15
|
/**
|
|
4
16
|
* Auto-create auth tables if they don't exist.
|
|
5
17
|
*
|
package/dist/auth/services.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ export declare class UserService implements UserRepository {
|
|
|
84
84
|
updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null>;
|
|
85
85
|
deleteUser(id: string): Promise<void>;
|
|
86
86
|
listUsers(): Promise<UserData[]>;
|
|
87
|
+
/**
|
|
88
|
+
* @see UserRepository.firstUserIds — an unordered `LIMIT` read of the
|
|
89
|
+
* primary key, not `listUsersPaginated`'s count and sort of every row.
|
|
90
|
+
*/
|
|
91
|
+
firstUserIds(limit: number): Promise<string[]>;
|
|
87
92
|
listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
|
|
88
93
|
/**
|
|
89
94
|
* Update user's password hash; `null` removes the password.
|
|
@@ -338,6 +343,7 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
338
343
|
deleteUser(id: string): Promise<void>;
|
|
339
344
|
listUsers(): Promise<UserData[]>;
|
|
340
345
|
listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult>;
|
|
346
|
+
firstUserIds(limit: number): Promise<string[]>;
|
|
341
347
|
updatePassword(id: string, passwordHash: string | null): Promise<void>;
|
|
342
348
|
setEmailVerified(id: string, verified: boolean): Promise<void>;
|
|
343
349
|
setVerificationToken(id: string, token: string | null): Promise<void>;
|
|
@@ -3,9 +3,82 @@
|
|
|
3
3
|
* `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the
|
|
4
4
|
* backup surface stays self-contained.
|
|
5
5
|
*/
|
|
6
|
-
import arg from "arg";
|
|
6
|
+
import type arg from "arg";
|
|
7
|
+
import type { StorageController } from "@rebasepro/server";
|
|
8
|
+
import { BackupDestination } from "./pg-tools.js";
|
|
9
|
+
/**
|
|
10
|
+
* The controller for an object-storage destination, or `null` for a local one.
|
|
11
|
+
*
|
|
12
|
+
* Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the
|
|
13
|
+
* Backups panel's routes use — so the CLI and the panel read a bucket the same
|
|
14
|
+
* way. This file had its own copy, and the two had already drifted once
|
|
15
|
+
* (`S3_FORCE_PATH_STYLE` read as `=== "true"`, so a MinIO backup addressed the
|
|
16
|
+
* bucket as a host).
|
|
17
|
+
*
|
|
18
|
+
* Exported for its test.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveStorageForDestination(dest: BackupDestination, env: Record<string, string | undefined>): Promise<StorageController | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The directory `rebase` was run from, which `rebase` puts here for the driver.
|
|
23
|
+
*
|
|
24
|
+
* The driver runs in the project's `backend/`, so a relative path resolved
|
|
25
|
+
* against its own working directory named a different place from the one the
|
|
26
|
+
* user typed it in: `rebase db backup --out ./backups` wrote to `<root>/backups`
|
|
27
|
+
* (the CLI resolves explicit paths itself) while `rebase db backups list`
|
|
28
|
+
* looked in `<root>/backend/backups`. Spelled the same in
|
|
29
|
+
* `packages/cli/src/commands/db.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const INVOCATION_CWD_ENV = "REBASE_INVOCATION_CWD";
|
|
32
|
+
/** Where a relative path the user typed is relative to: where they ran `rebase`. */
|
|
33
|
+
export declare function invocationDir(env?: Readonly<Record<string, string | undefined>>): string;
|
|
34
|
+
/**
|
|
35
|
+
* Where `db backup` writes and `db backups list` reads, by one rule.
|
|
36
|
+
*
|
|
37
|
+
* - `--out` is relative to where `rebase` was run, like any path typed on a
|
|
38
|
+
* command line.
|
|
39
|
+
* - `BACKUP_DESTINATION` is relative to the backend directory, because the
|
|
40
|
+
* server reads the same variable there: the Backups panel and the scheduled
|
|
41
|
+
* backup have to find what the CLI wrote.
|
|
42
|
+
* - With neither, `./backups` where `rebase` was run, as the help says.
|
|
43
|
+
*
|
|
44
|
+
* An `s3://` or `gs://` destination is returned as given.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveBackupLocation(opts: {
|
|
47
|
+
flag?: string;
|
|
48
|
+
configured?: string;
|
|
49
|
+
invocationDir: string;
|
|
50
|
+
backendDir: string;
|
|
51
|
+
}): string;
|
|
52
|
+
/**
|
|
53
|
+
* Parse a `rebase db backup …` line. An undeclared flag is refused (see
|
|
54
|
+
* `parseCommandFlags`), and so is a positional: `rebase db backup ./backups`
|
|
55
|
+
* means `--out ./backups`, and taken as nothing it wrote to the default
|
|
56
|
+
* directory.
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseBackupLine(rawArgs: string[]): arg.Result<{
|
|
59
|
+
"--database-url": StringConstructor;
|
|
60
|
+
"--docker": BooleanConstructor;
|
|
61
|
+
"--debug": BooleanConstructor;
|
|
62
|
+
"--help": BooleanConstructor;
|
|
63
|
+
"-h": string;
|
|
64
|
+
} & {
|
|
65
|
+
"--out": StringConstructor;
|
|
66
|
+
"--output": string;
|
|
67
|
+
"--exclude-schema": [StringConstructor];
|
|
68
|
+
"--no-owner": BooleanConstructor;
|
|
69
|
+
"--enable-row-security": BooleanConstructor;
|
|
70
|
+
"--row-security-role": StringConstructor;
|
|
71
|
+
"-o": string;
|
|
72
|
+
}>;
|
|
7
73
|
export declare function backupCommand(rawArgs: string[]): Promise<void>;
|
|
8
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Parse a `rebase db restore …` line; the one positional is the backup.
|
|
76
|
+
*
|
|
77
|
+
* Strict, because this is the command where a typo costs most: parsed
|
|
78
|
+
* permissively, `--targt-db x` became a second positional, the target fell
|
|
79
|
+
* back to DATABASE_URL's own database, and the restore ran against the live
|
|
80
|
+
* one.
|
|
81
|
+
*/
|
|
9
82
|
export declare function parseRestoreLine(rawArgs: string[]): arg.Result<{
|
|
10
83
|
"--database-url": StringConstructor;
|
|
11
84
|
"--docker": BooleanConstructor;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StorageController } from "@rebasepro/server";
|
|
2
|
+
import { resolvePgBinary } from "./pg-binary.js";
|
|
2
3
|
import { BackupDestination, type RowSecurityIdentity, VersionCompatibility } from "./pg-tools.js";
|
|
3
4
|
import { BackupObject, RetentionOptions } from "./retention.js";
|
|
4
5
|
/**
|
|
@@ -11,8 +12,7 @@ export declare class BackupToolError extends Error {
|
|
|
11
12
|
readonly hint?: string | undefined;
|
|
12
13
|
constructor(message: string, hint?: string | undefined);
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore" | "pg_dumpall", env?: Record<string, string | undefined>): string | null;
|
|
15
|
+
export { resolvePgBinary };
|
|
16
16
|
/** Run `<bin> --version` and extract the major version. */
|
|
17
17
|
export declare function detectToolMajor(bin: string): Promise<number | null>;
|
|
18
18
|
/** Query the server for its major version via `server_version_num`. */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type PgTool = "pg_dump" | "pg_restore" | "pg_dumpall";
|
|
2
|
+
/** The variable that points at `tool` when it is not on the PATH. */
|
|
3
|
+
export declare function pgToolVariable(tool: PgTool): string;
|
|
4
|
+
/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
|
|
5
|
+
export declare function resolvePgBinary(tool: PgTool, env?: Record<string, string | undefined>): string | null;
|