@rebasepro/server-postgres 0.17.3 → 0.18.1
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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/BranchService-ByLfM_Tt.js +360 -0
- package/dist/BranchService-ByLfM_Tt.js.map +1 -0
- package/dist/__vite-browser-external-BnuHet1e.js +46 -0
- package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
- package/dist/arg-Dni7MzLB.js +87 -0
- package/dist/arg-Dni7MzLB.js.map +1 -0
- package/dist/auth-users-columns-D2LBFrMH.js +116 -0
- package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/backup-cli-oM9WQVlV.js +408 -0
- package/dist/backup-cli-oM9WQVlV.js.map +1 -0
- package/dist/backup-service-Br3tqwsy.js +715 -0
- package/dist/backup-service-Br3tqwsy.js.map +1 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/cli-output-CNdMql-L.js +47 -0
- package/dist/cli-output-CNdMql-L.js.map +1 -0
- package/dist/cli.js +2020 -0
- package/dist/cli.js.map +1 -0
- package/dist/collection-index-BHgT_TKd.js +276 -0
- package/dist/collection-index-BHgT_TKd.js.map +1 -0
- package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
- package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
- package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
- package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
- package/dist/ensure-tables-CIojK-Rx.js +650 -0
- package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
- package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +7423 -1133
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
- package/dist/policy-drift-kNJ6X2Uj.js +313 -0
- package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
- package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
- package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
- package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema.js +24 -0
- package/dist/schema/generate-drizzle-schema.js.map +1 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl.js +138 -0
- package/dist/schema/generate-postgres-ddl.js.map +1 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
package/src/backup/pg-tools.ts
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for the backup/restore commands.
|
|
3
|
-
*
|
|
4
|
-
* Everything in this file is side-effect free so it can be unit-tested
|
|
5
|
-
* without a live Postgres server, matching the constraint that CI must
|
|
6
|
-
* not require a database.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { forLibpq } from "../utils/connection-string";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A parsed backup destination. `--out` (and the scheduled-backup config)
|
|
13
|
-
* accepts either a local filesystem path or an object-storage URL.
|
|
14
|
-
*/
|
|
15
|
-
export type BackupDestination =
|
|
16
|
-
| { kind: "local"; path: string }
|
|
17
|
-
| { kind: "s3"; bucket: string; prefix: string }
|
|
18
|
-
| { kind: "gcs"; bucket: string; prefix: string };
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract the database name from a Postgres connection string.
|
|
22
|
-
* Returns `null` when the URL has no database path (e.g. bare host).
|
|
23
|
-
*/
|
|
24
|
-
export function parseDbNameFromUrl(connectionString: string): string | null {
|
|
25
|
-
try {
|
|
26
|
-
const parsed = new URL(connectionString);
|
|
27
|
-
const name = parsed.pathname.replace(/^\//, "").trim();
|
|
28
|
-
return name.length > 0 ? name : null;
|
|
29
|
-
} catch {
|
|
30
|
-
// Fall back to a permissive regex for non-URL DSNs.
|
|
31
|
-
const match = connectionString.match(/\/([^/?]+)(\?|$)/);
|
|
32
|
-
return match && match[1] ? match[1] : null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Swap the database name in a connection string, preserving credentials,
|
|
38
|
-
* host, port and query params. Used by `--target-db` / `--create-db` so a
|
|
39
|
-
* restore can target a fresh database instead of clobbering the live one.
|
|
40
|
-
*/
|
|
41
|
-
export function withDatabaseName(connectionString: string, dbName: string): string {
|
|
42
|
-
try {
|
|
43
|
-
const parsed = new URL(connectionString);
|
|
44
|
-
parsed.pathname = `/${dbName}`;
|
|
45
|
-
return parsed.toString();
|
|
46
|
-
} catch {
|
|
47
|
-
return connectionString.replace(/\/([^/?]+)(\?|$)/, `/${dbName}$2`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Parse the major version out of `pg_dump --version` / `pg_restore --version`
|
|
53
|
-
* output, e.g. `"pg_dump (PostgreSQL) 16.2"` → `16`. Handles the pre-10
|
|
54
|
-
* `9.6.x` scheme (returns `9`) and beta strings like `"17beta1"`.
|
|
55
|
-
*/
|
|
56
|
-
export function parsePgToolMajor(versionOutput: string): number | null {
|
|
57
|
-
const match = versionOutput.match(/(\d+)(?:\.(\d+))?/);
|
|
58
|
-
if (!match) return null;
|
|
59
|
-
const major = Number(match[1]);
|
|
60
|
-
if (!Number.isFinite(major)) return null;
|
|
61
|
-
// Postgres 9.x used the first two numbers as the major (9.6, 9.4…).
|
|
62
|
-
if (major === 9 && match[2] !== undefined) {
|
|
63
|
-
return 9;
|
|
64
|
-
}
|
|
65
|
-
return major;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)
|
|
70
|
-
* into a major version (`16`). Also accepts pre-10 encodings like `90603`
|
|
71
|
-
* → `9`.
|
|
72
|
-
*/
|
|
73
|
-
export function serverVersionNumToMajor(versionNum: number | string): number | null {
|
|
74
|
-
const num = typeof versionNum === "string" ? Number(versionNum) : versionNum;
|
|
75
|
-
if (!Number.isFinite(num) || num <= 0) return null;
|
|
76
|
-
if (num < 100000) {
|
|
77
|
-
// 9.x scheme: 90603 = 9.6.3
|
|
78
|
-
return Math.floor(num / 10000);
|
|
79
|
-
}
|
|
80
|
-
return Math.floor(num / 10000);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface VersionCompatibility {
|
|
84
|
-
compatible: boolean;
|
|
85
|
-
reason?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* pg_dump / pg_restore must be **at least** as new as the server they talk
|
|
90
|
-
* to. A newer client against an older server is supported; an older client
|
|
91
|
-
* against a newer server is not and produces corrupt or rejected output.
|
|
92
|
-
*/
|
|
93
|
-
export function checkToolServerCompatibility(
|
|
94
|
-
toolMajor: number | null,
|
|
95
|
-
serverMajor: number | null
|
|
96
|
-
): VersionCompatibility {
|
|
97
|
-
if (toolMajor === null) {
|
|
98
|
-
return { compatible: false, reason: "Could not determine the client tool version." };
|
|
99
|
-
}
|
|
100
|
-
if (serverMajor === null) {
|
|
101
|
-
return { compatible: false, reason: "Could not determine the Postgres server version." };
|
|
102
|
-
}
|
|
103
|
-
if (toolMajor < serverMajor) {
|
|
104
|
-
return {
|
|
105
|
-
compatible: false,
|
|
106
|
-
reason:
|
|
107
|
-
`Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. ` +
|
|
108
|
-
`pg_dump/pg_restore must be the same major version as the server or newer. ` +
|
|
109
|
-
`Install Postgres ${serverMajor} client tools.`
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return { compatible: true };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Build a deterministic, sortable backup file name:
|
|
117
|
-
* `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`
|
|
118
|
-
*
|
|
119
|
-
* The UTC timestamp is embedded so retention pruning can recover the
|
|
120
|
-
* creation time from the object key alone, without extra metadata.
|
|
121
|
-
*/
|
|
122
|
-
export function buildBackupFilename(dbName: string, date: Date = new Date()): string {
|
|
123
|
-
const iso = date.toISOString(); // 2026-07-14T09:12:03.123Z
|
|
124
|
-
const stamp = iso.replace(/\.\d+Z$/, "Z").replace(/[-:]/g, "");
|
|
125
|
-
const safeDb = dbName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
126
|
-
return `rebase-${safeDb}-${stamp}.dump`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Recover the creation timestamp encoded in a backup file name by
|
|
131
|
-
* {@link buildBackupFilename}. Returns `null` for names that don't match,
|
|
132
|
-
* so foreign objects in a shared prefix are never pruned.
|
|
133
|
-
*/
|
|
134
|
-
export function parseBackupTimestamp(fileName: string): Date | null {
|
|
135
|
-
const base = fileName.split("/").pop() ?? fileName;
|
|
136
|
-
const match = base.match(/-(\d{8})T(\d{6})Z\.dump$/);
|
|
137
|
-
if (!match) return null;
|
|
138
|
-
const [, ymd, hms] = match;
|
|
139
|
-
const iso =
|
|
140
|
-
`${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +
|
|
141
|
-
`T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;
|
|
142
|
-
const date = new Date(iso);
|
|
143
|
-
return Number.isNaN(date.getTime()) ? null : date;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Parse a destination string into a structured {@link BackupDestination}.
|
|
148
|
-
* `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;
|
|
149
|
-
* anything else is treated as a local path.
|
|
150
|
-
*/
|
|
151
|
-
export function parseBackupDestination(out: string): BackupDestination {
|
|
152
|
-
const s3 = out.match(/^s3:\/\/([^/]+)\/?(.*)$/);
|
|
153
|
-
if (s3) {
|
|
154
|
-
return { kind: "s3", bucket: s3[1], prefix: stripTrailingSlash(s3[2]) };
|
|
155
|
-
}
|
|
156
|
-
const gcs = out.match(/^gs:\/\/([^/]+)\/?(.*)$/);
|
|
157
|
-
if (gcs) {
|
|
158
|
-
return { kind: "gcs", bucket: gcs[1], prefix: stripTrailingSlash(gcs[2]) };
|
|
159
|
-
}
|
|
160
|
-
return { kind: "local", path: out };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function stripTrailingSlash(s: string): string {
|
|
164
|
-
return s.replace(/\/+$/, "");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Join a storage prefix and a file name without producing a leading or
|
|
169
|
-
* doubled slash.
|
|
170
|
-
*/
|
|
171
|
-
export function joinStorageKey(prefix: string, fileName: string): string {
|
|
172
|
-
const clean = prefix.replace(/^\/+|\/+$/g, "");
|
|
173
|
-
return clean.length > 0 ? `${clean}/${fileName}` : fileName;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* The identity `pg_dump` reads rows as, when row security is left on.
|
|
178
|
-
*
|
|
179
|
-
* Not optional, and that is the whole design. `pg_dump --enable-row-security`
|
|
180
|
-
* on its own is the dangerous command in this file: it turns the "query would
|
|
181
|
-
* be affected by row-level security policy" *error* into a dump that exits 0
|
|
182
|
-
* and is silently missing every row the dumping role's policies exclude. A
|
|
183
|
-
* backup that looks fine and restores most of your data is worse than one that
|
|
184
|
-
* refused to run.
|
|
185
|
-
*
|
|
186
|
-
* So the flag is unreachable without a subject to evaluate the policies
|
|
187
|
-
* against. Rebase's generated policies read `app.uid` and `app.user_roles`;
|
|
188
|
-
* supplying an admin role satisfies the `admin_full_access` rule and the dump
|
|
189
|
-
* sees everything that rule sees.
|
|
190
|
-
*/
|
|
191
|
-
export interface RowSecurityIdentity {
|
|
192
|
-
/** Written to `app.uid`. Any non-empty value — it is only an audit trail. */
|
|
193
|
-
uid: string;
|
|
194
|
-
/** Written to `app.user_roles`. Must include a role the policies admit. */
|
|
195
|
-
roles: string[];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
|
|
200
|
-
* set a GUC.
|
|
201
|
-
*
|
|
202
|
-
* A backslash escape rather than quoting, which is what libpq's `-c` parsing
|
|
203
|
-
* takes: a space inside a value ends the option otherwise, so a role list is
|
|
204
|
-
* comma-joined and never spaced.
|
|
205
|
-
*/
|
|
206
|
-
export function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {
|
|
207
|
-
const escape = (value: string) => value.replace(/([\\ ])/g, "\\$1");
|
|
208
|
-
return [
|
|
209
|
-
`-c app.uid=${escape(identity.uid)}`,
|
|
210
|
-
`-c app.user_id=${escape(identity.uid)}`,
|
|
211
|
-
`-c app.user_roles=${escape(identity.roles.join(","))}`
|
|
212
|
-
].join(" ");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
|
|
217
|
-
* which is compressed and restorable selectively via `pg_restore`.
|
|
218
|
-
*/
|
|
219
|
-
export function buildPgDumpArgs(opts: {
|
|
220
|
-
connectionString: string;
|
|
221
|
-
outFile: string;
|
|
222
|
-
/** Extra schemas/tables to exclude, e.g. Atlas revision tables. */
|
|
223
|
-
excludeSchemas?: string[];
|
|
224
|
-
/** Number of parallel jobs (directory format only; ignored for -Fc). */
|
|
225
|
-
noOwner?: boolean;
|
|
226
|
-
/**
|
|
227
|
-
* Dump with row security on, as this identity. Omit — which is the default
|
|
228
|
-
* — and `pg_dump` errors rather than skipping rows it cannot see.
|
|
229
|
-
*/
|
|
230
|
-
rowSecurity?: RowSecurityIdentity;
|
|
231
|
-
}): string[] {
|
|
232
|
-
const args = ["--format=custom", "--no-password", `--file=${opts.outFile}`];
|
|
233
|
-
if (opts.noOwner) {
|
|
234
|
-
args.push("--no-owner");
|
|
235
|
-
}
|
|
236
|
-
if (opts.rowSecurity) {
|
|
237
|
-
args.push("--enable-row-security");
|
|
238
|
-
}
|
|
239
|
-
for (const schema of opts.excludeSchemas ?? []) {
|
|
240
|
-
args.push(`--exclude-schema=${schema}`);
|
|
241
|
-
}
|
|
242
|
-
args.push(forLibpq(opts.connectionString));
|
|
243
|
-
return args;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Whether a `pg_dump` failure is the row-security one, and what to do about it.
|
|
248
|
-
*
|
|
249
|
-
* The error text names the table and nothing else, so the first read of it is
|
|
250
|
-
* "why would a backup be affected by RLS at all?" — the answer being that the
|
|
251
|
-
* dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
|
|
252
|
-
* normal state of the `postgres` user on Cloud SQL, RDS and every other managed
|
|
253
|
-
* Postgres. Nothing about that is visible from the message.
|
|
254
|
-
*
|
|
255
|
-
* Returns `null` for any other failure, so the caller reports it unchanged.
|
|
256
|
-
*/
|
|
257
|
-
export function diagnoseRowSecurityDumpFailure(error: unknown): string | null {
|
|
258
|
-
const text = [
|
|
259
|
-
(error as { stderr?: unknown })?.stderr,
|
|
260
|
-
(error as { message?: unknown })?.message
|
|
261
|
-
].map(part => (typeof part === "string" ? part : "")).join("\n");
|
|
262
|
-
|
|
263
|
-
if (!/row-level security policy/i.test(text)) return null;
|
|
264
|
-
|
|
265
|
-
const table = text.match(/for table "([^"]+)"/)?.[1];
|
|
266
|
-
|
|
267
|
-
return [
|
|
268
|
-
`pg_dump cannot read ${table ? `"${table}"` : "one of the tables"} because row-level security applies to it.`,
|
|
269
|
-
"",
|
|
270
|
-
" The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal",
|
|
271
|
-
" state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is",
|
|
272
|
-
" no superuser to hand out.",
|
|
273
|
-
"",
|
|
274
|
-
" Two ways out:",
|
|
275
|
-
"",
|
|
276
|
-
" • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The",
|
|
277
|
-
" dump then contains every row, which is what a backup should mean.",
|
|
278
|
-
"",
|
|
279
|
-
" • Re-run with --enable-row-security to dump as an admin subject instead. Rebase",
|
|
280
|
-
" sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy",
|
|
281
|
-
" admits the dump. Read the warning it prints: the result contains exactly the",
|
|
282
|
-
" rows those policies admit, and any table whose policies do not include an",
|
|
283
|
-
" admin rule comes out short — with no error.",
|
|
284
|
-
"",
|
|
285
|
-
" Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the",
|
|
286
|
-
" settings above it succeeds and silently omits rows."
|
|
287
|
-
].join("\n");
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Assemble the `pg_restore` argument vector for a custom-format dump.
|
|
292
|
-
*/
|
|
293
|
-
export function buildPgRestoreArgs(opts: {
|
|
294
|
-
connectionString: string;
|
|
295
|
-
inputFile: string;
|
|
296
|
-
/** Drop objects before recreating them (destructive but idempotent). */
|
|
297
|
-
clean?: boolean;
|
|
298
|
-
/**
|
|
299
|
-
* Abort on the first error instead of logging and continuing. Defaults
|
|
300
|
-
* ON: a restore that silently skips failed GRANT/RLS statements (because
|
|
301
|
-
* a role is missing) "succeeds" with RLS un-enforced — a security hole.
|
|
302
|
-
* Fail loudly instead so the operator knows the restore is incomplete.
|
|
303
|
-
*/
|
|
304
|
-
exitOnError?: boolean;
|
|
305
|
-
noOwner?: boolean;
|
|
306
|
-
}): string[] {
|
|
307
|
-
const args = ["--format=custom", "--no-password", `--dbname=${forLibpq(opts.connectionString)}`];
|
|
308
|
-
if (opts.clean) {
|
|
309
|
-
args.push("--clean", "--if-exists");
|
|
310
|
-
}
|
|
311
|
-
if (opts.noOwner) {
|
|
312
|
-
args.push("--no-owner");
|
|
313
|
-
}
|
|
314
|
-
// Default to --exit-on-error unless explicitly disabled.
|
|
315
|
-
if (opts.exitOnError !== false) {
|
|
316
|
-
args.push("--exit-on-error");
|
|
317
|
-
}
|
|
318
|
-
args.push(opts.inputFile);
|
|
319
|
-
return args;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Assemble the `pg_restore --list` argument vector. Reading a dump's table
|
|
324
|
-
* of contents parses the whole archive without touching a database, so it is
|
|
325
|
-
* a cheap integrity check that the file isn't truncated or corrupt.
|
|
326
|
-
*/
|
|
327
|
-
export function buildPgRestoreListArgs(inputFile: string): string[] {
|
|
328
|
-
return ["--list", inputFile];
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
|
|
333
|
-
* cluster-wide objects) live outside any single database, so a per-database
|
|
334
|
-
* `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
|
|
335
|
-
* statements in the main dump fail on restore and RLS is silently lost — so
|
|
336
|
-
* every backup captures the globals into a sidecar `.globals.sql`.
|
|
337
|
-
*
|
|
338
|
-
* `--no-role-passwords` keeps role secrets out of the artifact (backups may
|
|
339
|
-
* be shipped off-box); roles are recreated password-less and re-secured by
|
|
340
|
-
* the operator.
|
|
341
|
-
*/
|
|
342
|
-
export function buildPgDumpallGlobalsArgs(opts: {
|
|
343
|
-
connectionString: string;
|
|
344
|
-
outFile: string;
|
|
345
|
-
}): string[] {
|
|
346
|
-
return [
|
|
347
|
-
"--globals-only",
|
|
348
|
-
"--no-role-passwords",
|
|
349
|
-
"--no-password",
|
|
350
|
-
`--file=${opts.outFile}`,
|
|
351
|
-
`--dbname=${forLibpq(opts.connectionString)}`
|
|
352
|
-
];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Derive the globals sidecar path/key for a given `.dump` file. Keeps the
|
|
357
|
-
* two artifacts adjacent so listing, uploading and pruning can find one from
|
|
358
|
-
* the other. A name that doesn't end in `.dump` is returned unchanged with a
|
|
359
|
-
* `.globals.sql` suffix appended.
|
|
360
|
-
*/
|
|
361
|
-
export function globalsFileForDump(dumpPath: string): string {
|
|
362
|
-
return dumpPath.endsWith(".dump")
|
|
363
|
-
? dumpPath.slice(0, -".dump".length) + ".globals.sql"
|
|
364
|
-
: dumpPath + ".globals.sql";
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Split a `pg_dumpall --globals-only` script into individual statements.
|
|
369
|
-
* Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
|
|
370
|
-
* can run independently and a benign "role already exists" on one doesn't
|
|
371
|
-
* abort the rest. Drops `--` comment lines and blank statements.
|
|
372
|
-
*/
|
|
373
|
-
export function splitGlobalsStatements(sql: string): string[] {
|
|
374
|
-
const withoutComments = sql
|
|
375
|
-
.split("\n")
|
|
376
|
-
// `--` comments, and psql meta-commands. pg_dumpall 15+ wraps its output
|
|
377
|
-
// in `\restrict <token>` / `\unrestrict <token>`, which are instructions
|
|
378
|
-
// to psql, not SQL. This replay sends statements over a connection
|
|
379
|
-
// instead, so they arrived at the server as `\restrict …` and came back
|
|
380
|
-
// as `syntax error at or near "\"` — reported to the user as two skipped
|
|
381
|
-
// globals per restore, which reads like roles failed to apply when
|
|
382
|
-
// nothing did. They carry no state worth replaying: dropping them is
|
|
383
|
-
// exactly what a SQL-level consumer should do.
|
|
384
|
-
.filter((line) => {
|
|
385
|
-
const trimmed = line.trim();
|
|
386
|
-
return !trimmed.startsWith("--") && !trimmed.startsWith("\\");
|
|
387
|
-
})
|
|
388
|
-
.join("\n");
|
|
389
|
-
return withoutComments
|
|
390
|
-
.split(";")
|
|
391
|
-
.map((s) => s.trim())
|
|
392
|
-
.filter((s) => s.length > 0);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Resolve the Postgres connection string the backup commands should use,
|
|
397
|
-
* mirroring the precedence the branch command already relies on.
|
|
398
|
-
*/
|
|
399
|
-
export function resolveConnectionString(
|
|
400
|
-
env: Record<string, string | undefined>
|
|
401
|
-
): string | null {
|
|
402
|
-
return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;
|
|
403
|
-
}
|
package/src/backup/retention.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retention pruning for scheduled backups. Pure logic — no I/O — so it can
|
|
3
|
-
* be exhaustively unit-tested without touching a storage backend.
|
|
4
|
-
*/
|
|
5
|
-
import { parseBackupTimestamp } from "./pg-tools";
|
|
6
|
-
|
|
7
|
-
export interface BackupObject {
|
|
8
|
-
/** Storage key / file name. */
|
|
9
|
-
key: string;
|
|
10
|
-
/**
|
|
11
|
-
* Creation time. Optional — when absent it is recovered from the
|
|
12
|
-
* timestamp encoded in the key by `buildBackupFilename`.
|
|
13
|
-
*/
|
|
14
|
-
createdAt?: Date;
|
|
15
|
-
/**
|
|
16
|
-
* Size on disk, when the lister knows it (local destinations always do).
|
|
17
|
-
*
|
|
18
|
-
* Reported so an empty artifact cannot masquerade as a backup in
|
|
19
|
-
* `rebase db backups list`. Deliberately NOT consulted by
|
|
20
|
-
* {@link selectBackupsToPrune}: retention decides by age, and a rule that
|
|
21
|
-
* silently skipped small files would make a genuinely tiny dump immortal.
|
|
22
|
-
* Visibility is the fix here; the artifacts themselves are now removed at
|
|
23
|
-
* the point of failure by `discardPartialDump`.
|
|
24
|
-
*/
|
|
25
|
-
sizeBytes?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface RetentionOptions {
|
|
29
|
-
/** Delete backups older than this many days. `0`/negative disables age pruning. */
|
|
30
|
-
retentionDays?: number;
|
|
31
|
-
/**
|
|
32
|
-
* Always keep at least this many of the most recent backups, even if
|
|
33
|
-
* they are older than `retentionDays`. Guards against wiping every
|
|
34
|
-
* backup after a long outage. Default: 0 (no minimum).
|
|
35
|
-
*/
|
|
36
|
-
keepMinimum?: number;
|
|
37
|
-
/** Reference "now" — injectable for deterministic tests. */
|
|
38
|
-
now?: Date;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Decide which backups to delete under the given retention policy.
|
|
43
|
-
*
|
|
44
|
-
* Rules, applied in order:
|
|
45
|
-
* 1. Objects whose key isn't a recognised rebase backup (no parseable
|
|
46
|
-
* timestamp and no `createdAt`) are never selected — foreign files in a
|
|
47
|
-
* shared bucket are left untouched.
|
|
48
|
-
* 2. The `keepMinimum` newest backups are always retained.
|
|
49
|
-
* 3. Of the remainder, any older than `retentionDays` are selected for
|
|
50
|
-
* deletion.
|
|
51
|
-
*
|
|
52
|
-
* Returns the keys to delete, oldest first.
|
|
53
|
-
*/
|
|
54
|
-
export function selectBackupsToPrune(
|
|
55
|
-
backups: BackupObject[],
|
|
56
|
-
options: RetentionOptions
|
|
57
|
-
): string[] {
|
|
58
|
-
const retentionDays = options.retentionDays ?? 0;
|
|
59
|
-
const keepMinimum = Math.max(0, options.keepMinimum ?? 0);
|
|
60
|
-
const now = options.now ?? new Date();
|
|
61
|
-
|
|
62
|
-
if (retentionDays <= 0) {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Resolve a timestamp for every object; drop the ones we can't date.
|
|
67
|
-
const dated = backups
|
|
68
|
-
.map((b) => {
|
|
69
|
-
const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);
|
|
70
|
-
return createdAt ? { key: b.key, createdAt } : null;
|
|
71
|
-
})
|
|
72
|
-
.filter((b): b is { key: string; createdAt: Date } => b !== null);
|
|
73
|
-
|
|
74
|
-
// Newest first so the first `keepMinimum` are the protected ones.
|
|
75
|
-
dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
76
|
-
|
|
77
|
-
const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;
|
|
78
|
-
|
|
79
|
-
const toDelete = dated
|
|
80
|
-
.slice(keepMinimum)
|
|
81
|
-
.filter((b) => b.createdAt.getTime() < cutoff);
|
|
82
|
-
|
|
83
|
-
// Return oldest first for predictable, readable logs.
|
|
84
|
-
toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
85
|
-
return toDelete.map((b) => b.key);
|
|
86
|
-
}
|