@rebasepro/server-postgres 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g52d71ee

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/auth/services.d.ts +6 -1
  2. package/dist/backup/backup-service.d.ts +10 -1
  3. package/dist/backup/pg-tools.d.ts +47 -0
  4. package/dist/{backup-service-DLb2drIH.js → backup-service-CD8o_1Sl.js} +136 -10
  5. package/dist/backup-service-CD8o_1Sl.js.map +1 -0
  6. package/dist/{ensure-collection-policies-Dv21KDMJ.js → ensure-collection-policies-ViG8XiPn.js} +2 -2
  7. package/dist/{ensure-collection-policies-Dv21KDMJ.js.map → ensure-collection-policies-ViG8XiPn.js.map} +1 -1
  8. package/dist/{ensure-collection-tables-CT6xHB1d.js → ensure-collection-tables-CBQdOETu.js} +2 -2
  9. package/dist/{ensure-collection-tables-CT6xHB1d.js.map → ensure-collection-tables-CBQdOETu.js.map} +1 -1
  10. package/dist/index.es.js +768 -537
  11. package/dist/index.es.js.map +1 -1
  12. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  13. package/dist/schema/introspect-db-logic.d.ts +94 -5
  14. package/dist/schema/introspect-db-queries.d.ts +119 -0
  15. package/dist/schema/introspect-db-structure.d.ts +263 -0
  16. package/dist/schema/introspect-db-types.d.ts +11 -0
  17. package/dist/services/RelationService.d.ts +24 -1
  18. package/dist/services/channel-bus/index.d.ts +1 -7
  19. package/dist/services/collection-helpers.d.ts +36 -2
  20. package/dist/{src-BkdpiQdw.js → src-DlPBctw_.js} +72 -6
  21. package/dist/src-DlPBctw_.js.map +1 -0
  22. package/dist/utils/connection-string.d.ts +29 -0
  23. package/dist/utils/drizzle-conditions.d.ts +5 -4
  24. package/dist/utils/pg-error-utils.d.ts +16 -0
  25. package/package.json +6 -6
  26. package/src/auth/services.ts +6 -3
  27. package/src/backup/backup-cli.ts +41 -2
  28. package/src/backup/backup-service.ts +38 -5
  29. package/src/backup/pg-tools.ts +96 -3
  30. package/src/cli.ts +11 -4
  31. package/src/collections/validate-relations.ts +15 -0
  32. package/src/data-transformer.ts +9 -3
  33. package/src/schema/generate-drizzle-schema-logic.ts +26 -1
  34. package/src/schema/introspect-db-constraints.ts +385 -0
  35. package/src/schema/introspect-db-inference.ts +18 -8
  36. package/src/schema/introspect-db-logic.ts +364 -68
  37. package/src/schema/introspect-db-queries.ts +326 -0
  38. package/src/schema/introspect-db-structure.ts +670 -0
  39. package/src/schema/introspect-db-types.ts +56 -0
  40. package/src/schema/introspect-db.ts +37 -80
  41. package/src/services/BranchService.ts +66 -28
  42. package/src/services/FetchService.ts +14 -0
  43. package/src/services/PersistService.ts +20 -6
  44. package/src/services/RelationService.ts +211 -45
  45. package/src/services/channel-bus/index.ts +0 -9
  46. package/src/services/collection-helpers.ts +69 -3
  47. package/src/utils/connection-string.ts +58 -0
  48. package/src/utils/drizzle-conditions.ts +31 -6
  49. package/src/utils/pg-error-utils.ts +19 -0
  50. package/dist/backup-service-DLb2drIH.js.map +0 -1
  51. package/dist/src-BkdpiQdw.js.map +0 -1
@@ -23,8 +23,13 @@ export interface AuthSchemaTables {
23
23
  *
24
24
  * Whitespace goes too: a trailing space survives the fold and reproduces the
25
25
  * problem exactly.
26
+ *
27
+ * Re-exported rather than defined here: `@rebasepro/server` and
28
+ * `@rebasepro/server-mongo` write this column too, and a second copy of this
29
+ * rule is the defect it exists to prevent.
26
30
  */
27
- export declare function normalizeEmail<T>(email: T): T | string;
31
+ import { normalizeEmail } from "@rebasepro/common";
32
+ export { normalizeEmail };
28
33
  /**
29
34
  * PostgreSQL implementation of UserRepository.
30
35
  * Handles all user-related database operations using Drizzle ORM.
@@ -1,5 +1,5 @@
1
1
  import type { StorageController } from "@rebasepro/server";
2
- import { BackupDestination, VersionCompatibility } from "./pg-tools";
2
+ import { BackupDestination, type RowSecurityIdentity, VersionCompatibility } from "./pg-tools";
3
3
  import { BackupObject, RetentionOptions } from "./retention";
4
4
  export declare class BackupToolError extends Error {
5
5
  readonly hint?: string | undefined;
@@ -54,6 +54,15 @@ export declare function createDump(opts: {
54
54
  inheritStdio?: boolean;
55
55
  includeGlobals?: boolean;
56
56
  env?: Record<string, string | undefined>;
57
+ /**
58
+ * Dump with row security left on, reading as this identity.
59
+ *
60
+ * The escape hatch for a managed Postgres, where the dumping role owns
61
+ * nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with
62
+ * row security on, `pg_dump` stops erroring on rows it cannot see and
63
+ * simply omits them. See {@link RowSecurityIdentity}.
64
+ */
65
+ rowSecurity?: RowSecurityIdentity;
57
66
  }): Promise<BackupResult>;
58
67
  /**
59
68
  * Cheap integrity check on a freshly written dump: it must be non-empty and
@@ -79,6 +79,36 @@ export declare function parseBackupDestination(out: string): BackupDestination;
79
79
  * doubled slash.
80
80
  */
81
81
  export declare function joinStorageKey(prefix: string, fileName: string): string;
82
+ /**
83
+ * The identity `pg_dump` reads rows as, when row security is left on.
84
+ *
85
+ * Not optional, and that is the whole design. `pg_dump --enable-row-security`
86
+ * on its own is the dangerous command in this file: it turns the "query would
87
+ * be affected by row-level security policy" *error* into a dump that exits 0
88
+ * and is silently missing every row the dumping role's policies exclude. A
89
+ * backup that looks fine and restores most of your data is worse than one that
90
+ * refused to run.
91
+ *
92
+ * So the flag is unreachable without a subject to evaluate the policies
93
+ * against. Rebase's generated policies read `app.uid` and `app.user_roles`;
94
+ * supplying an admin role satisfies the `admin_full_access` rule and the dump
95
+ * sees everything that rule sees.
96
+ */
97
+ export interface RowSecurityIdentity {
98
+ /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */
99
+ uid: string;
100
+ /** Written to `app.user_roles`. Must include a role the policies admit. */
101
+ roles: string[];
102
+ }
103
+ /**
104
+ * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
105
+ * set a GUC.
106
+ *
107
+ * A backslash escape rather than quoting, which is what libpq's `-c` parsing
108
+ * takes: a space inside a value ends the option otherwise, so a role list is
109
+ * comma-joined and never spaced.
110
+ */
111
+ export declare function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string;
82
112
  /**
83
113
  * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
84
114
  * which is compressed and restorable selectively via `pg_restore`.
@@ -90,7 +120,24 @@ export declare function buildPgDumpArgs(opts: {
90
120
  excludeSchemas?: string[];
91
121
  /** Number of parallel jobs (directory format only; ignored for -Fc). */
92
122
  noOwner?: boolean;
123
+ /**
124
+ * Dump with row security on, as this identity. Omit — which is the default
125
+ * — and `pg_dump` errors rather than skipping rows it cannot see.
126
+ */
127
+ rowSecurity?: RowSecurityIdentity;
93
128
  }): string[];
129
+ /**
130
+ * Whether a `pg_dump` failure is the row-security one, and what to do about it.
131
+ *
132
+ * The error text names the table and nothing else, so the first read of it is
133
+ * "why would a backup be affected by RLS at all?" — the answer being that the
134
+ * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
135
+ * normal state of the `postgres` user on Cloud SQL, RDS and every other managed
136
+ * Postgres. Nothing about that is visible from the message.
137
+ *
138
+ * Returns `null` for any other failure, so the caller reports it unchanged.
139
+ */
140
+ export declare function diagnoseRowSecurityDumpFailure(error: unknown): string | null;
94
141
  /**
95
142
  * Assemble the `pg_restore` argument vector for a custom-format dump.
96
143
  */
@@ -25,8 +25,70 @@ import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWater
25
25
  import { Buffer as Buffer$1 } from "node:buffer";
26
26
  import "module";
27
27
  import { fileURLToPath as fileURLToPath$1 } from "url";
28
+ //#region src/utils/connection-string.ts
29
+ /**
30
+ * Connection-string rewrites for the tools Rebase shells out to.
31
+ *
32
+ * Pure and dependency-free: `pg-tools` builds argument vectors without a live
33
+ * server, and these have to be usable from there.
34
+ */
35
+ /**
36
+ * The `sslmode` values libpq accepts. `no-verify` is conspicuously not one.
37
+ */
38
+ var LIBPQ_SSLMODES = /* @__PURE__ */ new Set([
39
+ "disable",
40
+ "allow",
41
+ "prefer",
42
+ "require",
43
+ "verify-ca",
44
+ "verify-full"
45
+ ]);
46
+ /**
47
+ * Make a connection string safe to hand to a libpq program.
48
+ *
49
+ * `sslmode=no-verify` is a node-postgres convention — encrypt, but do not check
50
+ * the certificate — and libpq does not have it. It does not degrade or warn:
51
+ *
52
+ * psql: error: invalid sslmode value: "no-verify"
53
+ *
54
+ * which means a `DATABASE_URL` that works perfectly for the app makes `psql`,
55
+ * `pg_dump`, `pg_restore` and Atlas all refuse to start, with an error that
56
+ * points at the value rather than at the convention it comes from. It has cost
57
+ * this project time more than once.
58
+ *
59
+ * `require` is the honest translation: libpq's `require` encrypts and does not
60
+ * verify the certificate either, which is exactly what `no-verify` asks for.
61
+ * Nothing is relaxed by the rewrite — `verify-ca` and `verify-full` are left
62
+ * alone, so a connection string that asked for verification still gets it.
63
+ *
64
+ * Only `sslmode` is touched, and only when it is a value libpq would reject.
65
+ * Anything unparseable is returned as given: a connection that works
66
+ * unverified beats one this corrupted.
67
+ */
68
+ function forLibpq(connectionString) {
69
+ let url;
70
+ try {
71
+ url = new URL(connectionString);
72
+ } catch {
73
+ return connectionString;
74
+ }
75
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
76
+ const sslmode = url.searchParams.get("sslmode");
77
+ if (sslmode === null || LIBPQ_SSLMODES.has(sslmode)) return connectionString;
78
+ url.searchParams.set("sslmode", "require");
79
+ url.search = Array.from(url.searchParams.entries()).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
80
+ return url.toString();
81
+ }
82
+ //#endregion
28
83
  //#region src/backup/pg-tools.ts
29
84
  /**
85
+ * Pure helpers for the backup/restore commands.
86
+ *
87
+ * Everything in this file is side-effect free so it can be unit-tested
88
+ * without a live Postgres server, matching the constraint that CI must
89
+ * not require a database.
90
+ */
91
+ /**
30
92
  * Extract the database name from a Postgres connection string.
31
93
  * Returns `null` when the URL has no database path (e.g. bare host).
32
94
  */
@@ -156,6 +218,22 @@ function joinStorageKey(prefix, fileName) {
156
218
  return clean.length > 0 ? `${clean}/${fileName}` : fileName;
157
219
  }
158
220
  /**
221
+ * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to
222
+ * set a GUC.
223
+ *
224
+ * A backslash escape rather than quoting, which is what libpq's `-c` parsing
225
+ * takes: a space inside a value ends the option otherwise, so a role list is
226
+ * comma-joined and never spaced.
227
+ */
228
+ function buildRowSecurityPgOptions(identity) {
229
+ const escape = (value) => value.replace(/([\\ ])/g, "\\$1");
230
+ return [
231
+ `-c app.uid=${escape(identity.uid)}`,
232
+ `-c app.user_id=${escape(identity.uid)}`,
233
+ `-c app.user_roles=${escape(identity.roles.join(","))}`
234
+ ].join(" ");
235
+ }
236
+ /**
159
237
  * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),
160
238
  * which is compressed and restorable selectively via `pg_restore`.
161
239
  */
@@ -166,18 +244,56 @@ function buildPgDumpArgs(opts) {
166
244
  `--file=${opts.outFile}`
167
245
  ];
168
246
  if (opts.noOwner) args.push("--no-owner");
247
+ if (opts.rowSecurity) args.push("--enable-row-security");
169
248
  for (const schema of opts.excludeSchemas ?? []) args.push(`--exclude-schema=${schema}`);
170
- args.push(opts.connectionString);
249
+ args.push(forLibpq(opts.connectionString));
171
250
  return args;
172
251
  }
173
252
  /**
253
+ * Whether a `pg_dump` failure is the row-security one, and what to do about it.
254
+ *
255
+ * The error text names the table and nothing else, so the first read of it is
256
+ * "why would a backup be affected by RLS at all?" — the answer being that the
257
+ * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the
258
+ * normal state of the `postgres` user on Cloud SQL, RDS and every other managed
259
+ * Postgres. Nothing about that is visible from the message.
260
+ *
261
+ * Returns `null` for any other failure, so the caller reports it unchanged.
262
+ */
263
+ function diagnoseRowSecurityDumpFailure(error) {
264
+ const text = [error?.stderr, error?.message].map((part) => typeof part === "string" ? part : "").join("\n");
265
+ if (!/row-level security policy/i.test(text)) return null;
266
+ const table = text.match(/for table "([^"]+)"/)?.[1];
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
+ /**
174
290
  * Assemble the `pg_restore` argument vector for a custom-format dump.
175
291
  */
176
292
  function buildPgRestoreArgs(opts) {
177
293
  const args = [
178
294
  "--format=custom",
179
295
  "--no-password",
180
- `--dbname=${opts.connectionString}`
296
+ `--dbname=${forLibpq(opts.connectionString)}`
181
297
  ];
182
298
  if (opts.clean) args.push("--clean", "--if-exists");
183
299
  if (opts.noOwner) args.push("--no-owner");
@@ -210,7 +326,7 @@ function buildPgDumpallGlobalsArgs(opts) {
210
326
  "--no-role-passwords",
211
327
  "--no-password",
212
328
  `--file=${opts.outFile}`,
213
- `--dbname=${opts.connectionString}`
329
+ `--dbname=${forLibpq(opts.connectionString)}`
214
330
  ];
215
331
  }
216
332
  /**
@@ -8676,15 +8792,25 @@ async function createDump(opts) {
8676
8792
  const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "rebase-backup-"));
8677
8793
  fs.mkdirSync(outDir, { recursive: true });
8678
8794
  const localFile = path.join(outDir, fileName);
8679
- await execa(bin, buildPgDumpArgs({
8795
+ const args = buildPgDumpArgs({
8680
8796
  connectionString: opts.connectionString,
8681
8797
  outFile: localFile,
8682
8798
  excludeSchemas: opts.excludeSchemas,
8683
- noOwner: opts.noOwner
8684
- }), {
8685
- stdio: opts.inheritStdio ? "inherit" : "pipe",
8686
- env: { ...env }
8799
+ noOwner: opts.noOwner,
8800
+ rowSecurity: opts.rowSecurity
8687
8801
  });
8802
+ const dumpEnv = { ...env };
8803
+ if (opts.rowSecurity) dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)].filter(Boolean).join(" ");
8804
+ try {
8805
+ await execa(bin, args, {
8806
+ stdio: opts.inheritStdio ? "inherit" : "pipe",
8807
+ env: dumpEnv
8808
+ });
8809
+ } catch (error) {
8810
+ const diagnosis = diagnoseRowSecurityDumpFailure(error);
8811
+ if (!diagnosis) throw error;
8812
+ throw new BackupToolError(diagnosis, "Run `rebase db backup --help` for the flag, and read what it says about partial dumps.");
8813
+ }
8688
8814
  const result = {
8689
8815
  localFile,
8690
8816
  fileName,
@@ -8868,6 +8994,6 @@ async function pruneBackups(dest, options, storage) {
8868
8994
  return toDelete;
8869
8995
  }
8870
8996
  //#endregion
8871
- export { serverVersionNumToMajor as A, globalsFileForDump as C, parseDbNameFromUrl as D, parseBackupTimestamp as E, withDatabaseName as M, parsePgToolMajor as O, checkToolServerCompatibility as S, parseBackupDestination as T, buildBackupFilename as _, detectToolMajor as a, buildPgRestoreArgs as b, listBackups as c, resolvePgBinary as d, restoreDump as f, selectBackupsToPrune as g, require_source as h, createDump as i, splitGlobalsStatements as j, resolveConnectionString as k, preflight as l, validateDump as m, applyGlobals as n, ensureDatabaseExists as o, uploadBackup as p, backup_service_exports as r, getServerVersionMajor as s, BackupToolError as t, pruneBackups as u, buildPgDumpArgs as v, joinStorageKey as w, buildPgRestoreListArgs as x, buildPgDumpallGlobalsArgs as y };
8997
+ export { parsePgToolMajor as A, checkToolServerCompatibility as C, parseBackupDestination as D, joinStorageKey as E, serverVersionNumToMajor as M, splitGlobalsStatements as N, parseBackupTimestamp as O, withDatabaseName as P, buildRowSecurityPgOptions as S, globalsFileForDump as T, buildBackupFilename as _, detectToolMajor as a, buildPgRestoreArgs as b, listBackups as c, resolvePgBinary as d, restoreDump as f, selectBackupsToPrune as g, require_source as h, createDump as i, resolveConnectionString as j, parseDbNameFromUrl as k, preflight as l, validateDump as m, applyGlobals as n, ensureDatabaseExists as o, uploadBackup as p, backup_service_exports as r, getServerVersionMajor as s, BackupToolError as t, pruneBackups as u, buildPgDumpArgs as v, diagnoseRowSecurityDumpFailure as w, buildPgRestoreListArgs as x, buildPgDumpallGlobalsArgs as y };
8872
8998
 
8873
- //# sourceMappingURL=backup-service-DLb2drIH.js.map
8999
+ //# sourceMappingURL=backup-service-CD8o_1Sl.js.map