@rebasepro/server-postgres 0.21.0 → 0.21.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/README.md +15 -13
- package/dist/{BranchService-W3DMfcZZ.js → BranchService-CucnFcSE.js} +2 -2
- package/dist/{BranchService-W3DMfcZZ.js.map → BranchService-CucnFcSE.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +26 -10
- package/dist/backup/backup-cron.d.ts +28 -1
- package/dist/{backup-cli-Bp-ou6o2.js → backup-cli-CkjsJEcu.js} +3 -2
- package/dist/backup-cli-CkjsJEcu.js.map +1 -0
- package/dist/{cli-errors-C3g_kHBw.js → cli-errors-Dka89exj.js} +19 -2
- package/dist/cli-errors-Dka89exj.js.map +1 -0
- package/dist/cli.js +15 -8
- package/dist/cli.js.map +1 -1
- package/dist/{doctor-ByFWL_ET.js → doctor-Gzzd8wTq.js} +4 -4
- package/dist/{doctor-ByFWL_ET.js.map → doctor-Gzzd8wTq.js.map} +1 -1
- package/dist/{ensure-collection-policies-CQGHln-y.js → ensure-collection-policies-B4IDFtQ-.js} +2 -2
- package/dist/{ensure-collection-policies-CQGHln-y.js.map → ensure-collection-policies-B4IDFtQ-.js.map} +1 -1
- package/dist/{ensure-collection-tables-Bkvehxdm.js → ensure-collection-tables-Cr2ye5JC.js} +2 -2
- package/dist/{ensure-collection-tables-Bkvehxdm.js.map → ensure-collection-tables-Cr2ye5JC.js.map} +1 -1
- package/dist/{generate-drizzle-schema-vSK-VdYT.js → generate-drizzle-schema-D2MDdJt-.js} +2 -2
- package/dist/{generate-drizzle-schema-vSK-VdYT.js.map → generate-drizzle-schema-D2MDdJt-.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-Dfyf_MRu.js → generate-drizzle-schema-logic-DFa9qy9u.js} +4 -2
- package/dist/{generate-drizzle-schema-logic-Dfyf_MRu.js.map → generate-drizzle-schema-logic-DFa9qy9u.js.map} +1 -1
- package/dist/{generated-schema-staleness-DRQe2BpC.js → generated-schema-staleness-Di9c4ZxN.js} +9 -5
- package/dist/{generated-schema-staleness-DRQe2BpC.js.map → generated-schema-staleness-Di9c4ZxN.js.map} +1 -1
- package/dist/index.es.js +264 -180
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor-cli.js +2 -2
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generated-schema-staleness.d.ts +22 -0
- package/dist/services/realtimeService.d.ts +75 -12
- package/package.json +7 -7
- package/dist/backup-cli-Bp-ou6o2.js.map +0 -1
- package/dist/cli-errors-C3g_kHBw.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as __createRequire } from "module";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { i as diagnoseDbError } from "../cli-errors-
|
|
4
|
+
import { i as diagnoseDbError } from "../cli-errors-Dka89exj.js";
|
|
5
5
|
import { o as validatePolicyPgRoles, s as warnOnAnonymousGrants } from "../rls-enforcement-C6Xk0lA6.js";
|
|
6
6
|
import { t as chalk } from "../source-Br7L7GOI.js";
|
|
7
|
-
import { n as loadCollections, r as runDoctor } from "../doctor-
|
|
7
|
+
import { n as loadCollections, r as runDoctor } from "../doctor-Gzzd8wTq.js";
|
|
8
8
|
import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-B0GDRh7_.js";
|
|
9
9
|
import path from "path";
|
|
10
10
|
//#region src/schema/doctor-policy-checks.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { t as main } from "../generate-drizzle-schema-
|
|
3
|
+
import { t as main } from "../generate-drizzle-schema-D2MDdJt-.js";
|
|
4
4
|
//#region src/schema/bin/generate-drizzle-schema.ts
|
|
5
5
|
/**
|
|
6
6
|
* The built entry point for `generate-drizzle-schema`.
|
|
@@ -88,6 +88,28 @@ export declare function staleVerdict(input: {
|
|
|
88
88
|
generatedExists: boolean;
|
|
89
89
|
/** The legacy foreign-key names found in it, if it was read. */
|
|
90
90
|
stale: LegacyForeignKeyName[];
|
|
91
|
+
/**
|
|
92
|
+
* How the file differs from what the current collections generate.
|
|
93
|
+
*
|
|
94
|
+
* The check this command's own summary promises — "generated schema files
|
|
95
|
+
* that no longer match the collections" — and for a long time the one it did
|
|
96
|
+
* not make. Staleness was computed from {@link findLegacyForeignKeyNames}
|
|
97
|
+
* alone, which finds one specific historical defect: a junction column named
|
|
98
|
+
* the way a previous release derived it. Every other way a file can fall
|
|
99
|
+
* behind its collections — a table added, a column retyped, an enum label
|
|
100
|
+
* dropped from the offer — left the command printing "Nothing stale" about a
|
|
101
|
+
* file that regenerating visibly changes.
|
|
102
|
+
*
|
|
103
|
+
* `projects_database_mode` is the one that found this. The shared database
|
|
104
|
+
* tier was retired, the label came off the collection, and `schema stale`
|
|
105
|
+
* called the file a match for weeks while every `pnpm run build` silently
|
|
106
|
+
* rewrote it.
|
|
107
|
+
*
|
|
108
|
+
* Optional so a caller that has not loaded the collections — the only way to
|
|
109
|
+
* know what they would generate — reports what it does know rather than
|
|
110
|
+
* claiming a clean bill it never checked.
|
|
111
|
+
*/
|
|
112
|
+
differences?: DeclarationDifference[];
|
|
91
113
|
/** Why the comparison could not be made, when it could not. */
|
|
92
114
|
unreadable?: string;
|
|
93
115
|
/** Whether `--fix` was given. */
|
|
@@ -96,6 +96,22 @@ type StoredCollectionRequest = {
|
|
|
96
96
|
/** `SELECT DISTINCT` over the projection. */
|
|
97
97
|
distinct?: boolean;
|
|
98
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* What an in-process listener asks for: the read, and who it reads as.
|
|
101
|
+
* See {@link RealtimeService.startDataDriverSubscription}.
|
|
102
|
+
*/
|
|
103
|
+
export type DataDriverSubscriptionRequest = {
|
|
104
|
+
path: string;
|
|
105
|
+
/** The subscriber. Absent reads as the anonymous user. */
|
|
106
|
+
authContext?: SubscriptionAuthContext;
|
|
107
|
+
onError?: (error: unknown) => void;
|
|
108
|
+
} & ({
|
|
109
|
+
type: "collection";
|
|
110
|
+
collectionRequest: StoredCollectionRequest;
|
|
111
|
+
} | {
|
|
112
|
+
type: "single";
|
|
113
|
+
id: string | number;
|
|
114
|
+
});
|
|
99
115
|
/**
|
|
100
116
|
* A registered subscription, plus the two counters that order its deliveries.
|
|
101
117
|
*
|
|
@@ -123,6 +139,12 @@ type Subscription = {
|
|
|
123
139
|
id?: string | number;
|
|
124
140
|
collectionRequest?: StoredCollectionRequest;
|
|
125
141
|
authContext?: SubscriptionAuthContext;
|
|
142
|
+
/**
|
|
143
|
+
* An in-process listener's error callback, called when a refetch fails.
|
|
144
|
+
* Kept on the subscription so it goes wherever the subscription goes: a
|
|
145
|
+
* replaced or cancelled one takes its listener with it.
|
|
146
|
+
*/
|
|
147
|
+
onError?: (error: unknown) => void;
|
|
126
148
|
/** How many deliveries have been started for this subscription. */
|
|
127
149
|
started: number;
|
|
128
150
|
/** The highest started-sequence that has already reached the subscriber. */
|
|
@@ -285,26 +307,41 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
285
307
|
*
|
|
286
308
|
* The last two are identity, not presence: the map has to still hold *this
|
|
287
309
|
* exact object*, not merely something under this id.
|
|
310
|
+
*
|
|
311
|
+
* A failure goes through the same slot, by `mayReportFailure()`: an error
|
|
312
|
+
* frame from a stale, cancelled or replaced fetch would mark a view that
|
|
313
|
+
* already shows newer rows as failed, or fail another subscription's view.
|
|
314
|
+
* It also answers yes to the delivery that already claimed the slot, since
|
|
315
|
+
* that is the send itself failing (a row that will not serialise) after the
|
|
316
|
+
* check passed and before anything reached the subscriber.
|
|
288
317
|
*/
|
|
289
318
|
private beginDelivery;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Start an in-process listener: register it, then deliver its first rows.
|
|
321
|
+
*
|
|
322
|
+
* The first fetch runs here, as the subscriber and through a delivery slot,
|
|
323
|
+
* the way every refetch after it does. `PostgresBackendDriver.listen*` used
|
|
324
|
+
* to run it themselves, outside this service, and got all three wrong: a
|
|
325
|
+
* listener on `withAuth(user)` read its first rows on the owner connection,
|
|
326
|
+
* so it was handed rows its policies deny; a slow first fetch landed over a
|
|
327
|
+
* newer refetch, and after an unsubscribe; and a row listener never heard a
|
|
328
|
+
* `null`. The subscriber's identity is part of the request because a
|
|
329
|
+
* subscription registered first and stamped with it afterwards has a first
|
|
330
|
+
* fetch that ran before the stamp.
|
|
331
|
+
*
|
|
332
|
+
* No `authContext` reads as the anonymous user, first fetch and refetches
|
|
333
|
+
* alike — see {@link fetchCollectionWithAuth}.
|
|
334
|
+
*/
|
|
335
|
+
startDataDriverSubscription(subscriptionId: string, request: DataDriverSubscriptionRequest, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void): void;
|
|
336
|
+
private deliverFirstToDataDriver;
|
|
300
337
|
/**
|
|
301
338
|
* Subscribe to collection changes (RealtimeProvider interface)
|
|
302
339
|
*/
|
|
303
|
-
subscribeToCollection(subscriptionId: string, config: CollectionSubscriptionConfig, callback?: (rows: Record<string, unknown>[]) => void): void;
|
|
340
|
+
subscribeToCollection(subscriptionId: string, config: CollectionSubscriptionConfig, callback?: (rows: Record<string, unknown>[]) => void, onError?: (error: unknown) => void): void;
|
|
304
341
|
/**
|
|
305
342
|
* Subscribe to single row changes (RealtimeProvider interface)
|
|
306
343
|
*/
|
|
307
|
-
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig, callback?: (row: Record<string, unknown> | null) => void): void;
|
|
344
|
+
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig, callback?: (row: Record<string, unknown> | null) => void, onError?: (error: unknown) => void): void;
|
|
308
345
|
/**
|
|
309
346
|
* Unsubscribe from a subscription (RealtimeProvider interface)
|
|
310
347
|
*/
|
|
@@ -315,6 +352,20 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
315
352
|
private handleMessage;
|
|
316
353
|
private handleCollectionSubscription;
|
|
317
354
|
private handleEntitySubscription;
|
|
355
|
+
/**
|
|
356
|
+
* Answer a subscriber's failed fetch with an error frame, through the slot
|
|
357
|
+
* its rows would have used.
|
|
358
|
+
*
|
|
359
|
+
* The error is always logged (`sanitizeErrorForClient` does that), but the
|
|
360
|
+
* frame goes out only while this fetch holds the newest slot. A failed
|
|
361
|
+
* straggler used to send one regardless. The client routes errors by
|
|
362
|
+
* `subscriptionId`, so it called `onError` for a view already showing newer
|
|
363
|
+
* rows, or for the subscription that replaced this one under the same id.
|
|
364
|
+
*
|
|
365
|
+
* With no slot, the subscribe failed before its fetch began. That part runs
|
|
366
|
+
* synchronously, so nothing else can have answered the request yet.
|
|
367
|
+
*/
|
|
368
|
+
private reportSocketFetchFailure;
|
|
318
369
|
private handleUnsubscribe;
|
|
319
370
|
/**
|
|
320
371
|
* Enhanced notification method that handles nested relation updates.
|
|
@@ -373,6 +424,18 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
373
424
|
* Debounce a collection refetch for a DataDriver callback subscription.
|
|
374
425
|
*/
|
|
375
426
|
private debouncedDriverRefetch;
|
|
427
|
+
/**
|
|
428
|
+
* Tell an in-process listener its refetch failed, through the slot the
|
|
429
|
+
* rows would have used.
|
|
430
|
+
*
|
|
431
|
+
* The two driver-refetch `catch` blocks used to log and stop, so the
|
|
432
|
+
* listener heard nothing and kept its last rows as if they were current.
|
|
433
|
+
* The error goes to it as thrown: this is trusted server code, and the
|
|
434
|
+
* masking on the socket path is for clients. The slot keeps a failed
|
|
435
|
+
* straggler from reporting over newer rows, and keeps a failure away from
|
|
436
|
+
* a subscription that was cancelled or replaced while it ran.
|
|
437
|
+
*/
|
|
438
|
+
private reportDriverFetchFailure;
|
|
376
439
|
/**
|
|
377
440
|
* Fetch a collection with optional RLS auth context.
|
|
378
441
|
* When authContext is provided, the fetch runs inside a transaction
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"execa": "^9.6.1",
|
|
51
51
|
"pg": "^8.22.0",
|
|
52
52
|
"ws": "^8.21.1",
|
|
53
|
-
"@rebasepro/codegen": "0.21.
|
|
54
|
-
"@rebasepro/
|
|
55
|
-
"@rebasepro/
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
53
|
+
"@rebasepro/codegen": "0.21.1",
|
|
54
|
+
"@rebasepro/common": "0.21.1",
|
|
55
|
+
"@rebasepro/types": "0.21.1",
|
|
56
|
+
"@rebasepro/server": "0.21.1",
|
|
57
|
+
"@rebasepro/utils": "0.21.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@electric-sql/pglite": "0.5.6",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"vite": "^8.1.5",
|
|
72
72
|
"vitest": "^4.1.10",
|
|
73
|
-
"@rebasepro/client": "0.21.
|
|
73
|
+
"@rebasepro/client": "0.21.1"
|
|
74
74
|
},
|
|
75
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
76
76
|
"publishConfig": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"backup-cli-Bp-ou6o2.js","names":[],"sources":["../src/backup/backup-cli.ts"],"sourcesContent":["/**\n * CLI handlers for `rebase db backup`, `rebase db restore`, and\n * `rebase db backups list`. Kept out of the main `cli.ts` dispatcher so the\n * backup surface stays self-contained.\n */\nimport arg from \"arg\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport readline from \"readline\";\nimport chalk from \"chalk\";\n// Aliased: `out` is already a local in two of the commands below (the `--out`\n// backup destination).\nimport { out as print, outWarn, outError } from \"../cli-output\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport {\n BackupDestination,\n globalsFileForDump,\n parseBackupDestination,\n parseDbNameFromUrl,\n resolveConnectionString,\n withDatabaseName\n} from \"./pg-tools\";\nimport {\n applyGlobals,\n BackupToolError,\n createDump,\n discardPartialDump,\n ensureDatabaseExists,\n listBackups,\n preflight,\n restoreDump,\n uploadBackup,\n validateDump\n} from \"./backup-service\";\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\n/**\n * Build a StorageController for an object-storage destination from the same\n * `S3_*` env vars the backend uses. Returns `null` for local destinations.\n */\nasync function resolveStorageForDestination(\n dest: BackupDestination,\n env: Record<string, string | undefined>\n): Promise<StorageController | null> {\n if (dest.kind === \"local\") return null;\n if (dest.kind === \"gcs\") {\n const { GCSStorageController } = await import(\"@rebasepro/server\");\n return new GCSStorageController({ type: \"gcs\", bucket: dest.bucket });\n }\n // s3 (also covers R2/MinIO/Hetzner/GCS-interop via S3_ENDPOINT)\n if (!env.S3_ACCESS_KEY_ID || !env.S3_SECRET_ACCESS_KEY) {\n throw new BackupToolError(\n \"S3 destination requires S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in the environment.\",\n \"Set the same S3_* variables your backend uses for storage.\"\n );\n }\n const { S3StorageController } = await import(\"@rebasepro/server\");\n return new S3StorageController({\n type: \"s3\",\n bucket: dest.bucket,\n region: env.S3_REGION || \"auto\",\n accessKeyId: env.S3_ACCESS_KEY_ID,\n secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n endpoint: env.S3_ENDPOINT,\n forcePathStyle: env.S3_FORCE_PATH_STYLE === \"true\"\n });\n}\n\nfunction requireConnection(): string {\n const conn = resolveConnectionString(process.env);\n if (!conn) {\n outError(chalk.red(\"✗ DATABASE_URL is not set. Make sure your .env file is configured.\"));\n process.exit(1);\n }\n return conn;\n}\n\nasync function promptConfirm(question: string): Promise<boolean> {\n // Non-interactive shells (CI, pipes) can't answer — treat as \"no\".\n if (!process.stdin.isTTY) return false;\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n try {\n const answer: string = await new Promise((resolve) => rl.question(question, resolve));\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backup\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupCommand(rawArgs: string[]): Promise<void> {\n const args = arg(\n {\n \"--out\": String,\n // The same alias `build` and `cloud env pull` carry. `--output` is\n // the canonical spelling in this CLI and this command took only\n // `--out`, so `rebase db backup --output ./backups` wrote to\n // ./backups *and* to the default directory — the flag was ignored\n // and its value became a positional.\n \"--output\": \"--out\",\n \"--exclude-schema\": [String],\n \"--no-owner\": Boolean,\n \"--enable-row-security\": Boolean,\n \"--row-security-role\": String,\n \"-o\": \"--out\"\n },\n { argv: rawArgs.slice(2), permissive: true }\n );\n\n if (args._.includes(\"--help\") || rawArgs.includes(\"--help\")) {\n printBackupHelp();\n return;\n }\n\n const connectionString = requireConnection();\n const dbName = parseDbNameFromUrl(connectionString) ?? \"database\";\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n print(\"\");\n print(chalk.bold(\" 💾 Rebase DB Backup\"));\n print(chalk.gray(` Database: ${dbName}`));\n print(chalk.gray(` Destination: ${out}`));\n print(\"\");\n\n // Version pre-flight (doctor-style).\n const pf = await preflight(\"pg_dump\", connectionString);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n print(chalk.gray(` Using pg_dump ${pf.toolMajor} against server ${pf.serverMajor}.`));\n\n // Opt-in, and loud. With row security on, pg_dump stops refusing to read\n // rows it cannot see and starts leaving them out — an exit-0 backup that\n // is quietly short. Anyone choosing that should know they chose it.\n const rowSecurity = args[\"--enable-row-security\"]\n ? { uid: \"rebase-db-backup\", roles: [args[\"--row-security-role\"] || \"admin\"] }\n : undefined;\n\n if (rowSecurity) {\n outWarn(\"\");\n outWarn(chalk.yellow(\" ⚠ Dumping with row-level security ON.\"));\n outWarn(chalk.gray(` Reading as roles [${rowSecurity.roles.join(\", \")}], which satisfies the generated`));\n outWarn(chalk.gray(\" `admin_full_access` policy. This backup contains exactly the rows those\"));\n outWarn(chalk.gray(\" policies admit — a table whose policies have no admin rule comes out short,\"));\n outWarn(chalk.gray(\" and pg_dump will not say so. Prefer granting the dumping role BYPASSRLS.\"));\n outWarn(\"\");\n }\n\n try {\n if (dest.kind === \"local\") {\n // Honour an explicit `…/name.dump` path; otherwise treat it as a\n // directory and auto-name the file.\n const explicitFile = dest.path.endsWith(\".dump\");\n const dump = await createDump({\n connectionString,\n dbName,\n outDir: explicitFile ? path.dirname(dest.path) : dest.path,\n fileName: explicitFile ? path.basename(dest.path) : undefined,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n await assertDumpValid(dump.localFile);\n print(\"\");\n print(chalk.green(` ✓ Backup written to ${dump.localFile} (${formatBytes(dump.sizeBytes)})`));\n if (dump.globalsFile) {\n print(chalk.gray(` ✓ Roles captured to ${dump.globalsFile} (needed so RLS survives a restore).`));\n }\n } else {\n const storage = await resolveStorageForDestination(dest, process.env);\n const dump = await createDump({\n connectionString,\n dbName,\n excludeSchemas: args[\"--exclude-schema\"],\n noOwner: args[\"--no-owner\"],\n inheritStdio: true,\n rowSecurity\n });\n try {\n await assertDumpValid(dump.localFile);\n const uploaded = await uploadBackup(storage!, dump.localFile, dest);\n print(\"\");\n print(chalk.green(` ✓ Backup uploaded to ${uploaded.storageUrl} (${formatBytes(dump.sizeBytes)})`));\n // Upload the roles sidecar next to the dump so a restore can\n // recreate roles the dump's GRANT/RLS statements depend on.\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) {\n const globalsUpload = await uploadBackup(storage!, dump.globalsFile, dest);\n print(chalk.gray(` ✓ Roles uploaded to ${globalsUpload.storageUrl} (needed so RLS survives a restore).`));\n }\n print(chalk.gray(\" Ensure this bucket is private — backups may contain secrets and PII.\"));\n } finally {\n if (fs.existsSync(dump.localFile)) fs.unlinkSync(dump.localFile);\n if (dump.globalsFile && fs.existsSync(dump.globalsFile)) fs.unlinkSync(dump.globalsFile);\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db restore <backup>\n// ─────────────────────────────────────────────────────────────────────────\nexport async function restoreCommand(rawArgs: string[]): Promise<void> {\n const args = arg(\n {\n \"--target-db\": String,\n \"--create-db\": Boolean,\n \"--clean\": Boolean,\n \"--no-owner\": Boolean,\n \"--continue-on-error\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\"\n },\n { argv: rawArgs.slice(2), permissive: true }\n );\n\n const backupArg = args._[0];\n if (!backupArg || rawArgs.includes(\"--help\")) {\n printRestoreHelp();\n if (!backupArg && !rawArgs.includes(\"--help\")) process.exit(1);\n return;\n }\n\n const baseConnection = requireConnection();\n\n // Choose the target connection: an explicit --target-db (or --create-db's\n // implied fresh db) swaps the database name so the live one isn't clobbered.\n const targetDb = args[\"--target-db\"] ?? parseDbNameFromUrl(baseConnection) ?? undefined;\n const targetConnection = args[\"--target-db\"]\n ? withDatabaseName(baseConnection, args[\"--target-db\"])\n : baseConnection;\n\n print(\"\");\n print(chalk.bold(\" ♻️ Rebase DB Restore\"));\n print(chalk.gray(` Source: ${backupArg}`));\n print(chalk.gray(` Target: ${targetDb ?? \"(from DATABASE_URL)\"}`));\n print(\"\");\n\n // Resolve the local file to restore from (download object-storage keys).\n // Also resolve the `.globals.sql` roles sidecar so cluster roles can be\n // recreated before the restore — without them the dump's GRANT/RLS\n // statements fail and RLS is silently lost.\n let localFile: string;\n let globalsSql: string | null = null;\n let cleanupTemp = false;\n try {\n if (/^(s3|gs):\\/\\//.test(backupArg)) {\n const dest = parseBackupDestination(backupArg.replace(/\\/[^/]+$/, \"\"));\n const storage = await resolveStorageForDestination(dest, process.env);\n if (!storage) throw new BackupToolError(\"Could not resolve storage for the given URL.\");\n const key = backupArg.replace(/^(s3|gs):\\/\\/[^/]+\\//, \"\");\n const bucket = backupArg.replace(/^(s3|gs):\\/\\/([^/]+)\\/.*$/, \"$2\");\n const file = await storage.getObject(key, bucket);\n if (!file) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-restore-\"));\n localFile = path.join(tmpDir, path.basename(key));\n fs.writeFileSync(localFile, Buffer.from(await file.arrayBuffer()));\n cleanupTemp = true;\n const globalsObj = await storage.getObject(globalsFileForDump(key), bucket);\n if (globalsObj) globalsSql = Buffer.from(await globalsObj.arrayBuffer()).toString(\"utf-8\");\n } else {\n localFile = path.resolve(backupArg);\n if (!fs.existsSync(localFile)) {\n outError(chalk.red(` ✗ Backup file not found: ${localFile}`));\n process.exit(1);\n }\n const globalsPath = globalsFileForDump(localFile);\n if (fs.existsSync(globalsPath)) globalsSql = fs.readFileSync(globalsPath, \"utf-8\");\n }\n\n // Version pre-flight. Check against the base connection — the server\n // version is identical for every database, and the target may not\n // exist yet when --create-db is used.\n const pf = await preflight(\"pg_restore\", baseConnection);\n if (!pf.compatible) {\n outError(chalk.red(` ✗ ${pf.reason}`));\n process.exit(1);\n }\n\n // `--create-db` needs a name before anything else can be decided.\n if (args[\"--create-db\"] && !targetDb) {\n outError(chalk.red(\" ✗ --create-db requires a resolvable target database name (use --target-db).\"));\n process.exit(1);\n }\n\n // Destructive-action gate. Restores overwrite data; never run without\n // an explicit yes (interactive confirmation or --yes).\n //\n // Ahead of `--create-db`, not after it. Creating the database first\n // meant an aborted run had already changed the cluster, while printing\n // \"No changes were made\" — and it left an empty database behind that a\n // second, confirmed run then reported as \"already exists\". The gate is\n // now the first thing that can stop the command, so its own message is\n // true whichever way the answer goes.\n if (!args[\"--yes\"]) {\n outWarn(chalk.yellow(\n ` ⚠️ This will restore into \"${targetDb ?? \"the target database\"}\" and may overwrite existing data.`\n ));\n const confirmed = await promptConfirm(chalk.yellow(\" Type 'yes' to continue: \"));\n if (!confirmed) {\n print(chalk.gray(\" Aborted. No changes were made.\"));\n process.exit(1);\n }\n }\n\n // Create the target database when requested.\n if (args[\"--create-db\"]) {\n const created = await ensureDatabaseExists(baseConnection, targetDb!);\n print(chalk.gray(created ? ` ✓ Created database \"${targetDb}\".` : ` • Database \"${targetDb}\" already exists.`));\n }\n\n // Recreate cluster roles before restoring so GRANT/RLS statements in\n // the dump apply. Best-effort and idempotent (see applyGlobals).\n if (globalsSql) {\n print(chalk.gray(\" Recreating cluster roles from the backup's roles sidecar…\"));\n const { applied, skipped } = await applyGlobals(\n targetConnection,\n globalsSql,\n (m) => print(chalk.gray(m))\n );\n print(chalk.gray(` ✓ Roles: ${applied} applied, ${skipped} skipped (already present or not permitted).`));\n } else {\n outWarn(chalk.yellow(\" ⚠️ No roles sidecar (.globals.sql) accompanies this backup.\"));\n outWarn(chalk.yellow(\" If the dump grants to roles that don't exist (e.g. rebase_user), the restore\"));\n outWarn(chalk.yellow(\" will fail — recreate those roles first, or use a backup that includes its globals.\"));\n }\n\n await restoreDump({\n connectionString: targetConnection,\n inputFile: localFile,\n clean: args[\"--clean\"],\n noOwner: args[\"--no-owner\"],\n // Fail loudly by default so a skipped GRANT never leaves RLS off.\n exitOnError: !args[\"--continue-on-error\"],\n inheritStdio: true\n });\n\n print(\"\");\n print(chalk.green(` ✓ Restore completed into \"${targetDb ?? \"the target database\"}\".`));\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n } finally {\n if (cleanupTemp && typeof localFile! === \"string\" && fs.existsSync(localFile!)) {\n fs.rmSync(path.dirname(localFile!), { recursive: true, force: true });\n }\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backups list\n// ─────────────────────────────────────────────────────────────────────────\nexport async function backupsCommand(rawArgs: string[]): Promise<void> {\n const action = rawArgs[2];\n if (!action || action === \"--help\") {\n printBackupsHelp();\n return;\n }\n if (action !== \"list\") {\n outError(chalk.red(`Unknown backups action: \"${action}\". Valid: list`));\n process.exit(1);\n }\n\n const args = arg({ \"--out\": String, \"--output\": \"--out\", \"-o\": \"--out\" }, { argv: rawArgs.slice(3), permissive: true });\n const out = args[\"--out\"] || process.env.BACKUP_DESTINATION || path.join(process.cwd(), \"backups\");\n const dest = parseBackupDestination(out);\n\n try {\n const storage = await resolveStorageForDestination(dest, process.env);\n const backups = await listBackups(dest, storage ?? undefined);\n print(\"\");\n if (backups.length === 0) {\n print(chalk.gray(` No backups found at ${out}.`));\n } else {\n print(chalk.bold(` 💾 ${backups.length} backup(s) at ${out}:`));\n print(\"\");\n for (const b of backups) {\n const when = b.createdAt ? b.createdAt.toISOString() : \"unknown date\";\n const name = dest.kind === \"local\" ? path.basename(b.key) : b.key;\n // An empty file is called out rather than listed as a peer of\n // the real ones. Older failures could leave a 0-byte dump here\n // (fixed at the source now), and retention protects the newest\n // by date whatever they contain — so a corpse left in place can\n // hold a `keepMinimum` slot against a backup that matters.\n const empty = b.sizeBytes === 0;\n const size = b.sizeBytes === undefined ? \"\" : ` — ${formatBytes(b.sizeBytes)}`;\n if (empty) {\n print(` ${chalk.red(\"○\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}`)}${chalk.red(\" — EMPTY, not restorable\")}`);\n } else {\n print(` ${chalk.green(\"●\")} ${chalk.bold(name)} ${chalk.gray(`— ${when}${size}`)}`);\n }\n }\n if (backups.some(b => b.sizeBytes === 0)) {\n print(\"\");\n print(chalk.yellow(\" ⚠ Empty files above are leftovers from a failed backup. Delete them:\"));\n print(chalk.gray(\" they count as recent backups for retention but restore nothing.\"));\n }\n }\n print(\"\");\n } catch (err) {\n reportError(err);\n process.exit(1);\n }\n}\n\n/**\n * Verify a freshly written dump; abort the command if it looks corrupt.\n *\n * Discards the artifact on the way out. Refusing to *report* success was never\n * enough on its own: the file stayed on disk, `rebase db backups list` showed\n * it as an ordinary entry, and retention — which ranks by timestamp and never\n * looks at size — would protect it as one of the `keepMinimum` newest while\n * pruning a real backup underneath it. The roles sidecar goes too; the two are\n * uploaded and pruned as a pair, so half a pair is not a backup either.\n */\nasync function assertDumpValid(localFile: string): Promise<void> {\n const check = await validateDump(localFile);\n if (!check.ok) {\n discardPartialDump(globalsFileForDump(localFile));\n discardPartialDump(localFile);\n throw new BackupToolError(\n `The backup failed validation and was discarded: ${check.reason}`,\n \"The dump was corrupt or truncated, so nothing was kept. Investigate before relying on this destination.\"\n );\n }\n}\n\nfunction reportError(err: unknown): void {\n if (err instanceof BackupToolError) {\n outError(chalk.red(` ✗ ${err.message}`));\n if (err.hint) outError(chalk.gray(` ${err.hint}`));\n } else {\n outError(chalk.red(` ✗ ${err instanceof Error ? err.message : String(err)}`));\n }\n}\n\nfunction printBackupHelp(): void {\n print(`\n${chalk.bold(\"rebase db backup\")} — Create a database backup (pg_dump, custom format)\n\n${chalk.green.bold(\"Usage\")}\n rebase db backup [--out <path|s3://bucket/prefix>] [options]\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--out, -o\")} <dest> Local path or s3://…/gs://… URL (default: ./backups)\n ${chalk.blue(\"--exclude-schema\")} <s> Exclude a schema (repeatable)\n ${chalk.blue(\"--no-owner\")} Omit ownership commands from the dump\n ${chalk.blue(\"--enable-row-security\")} Dump as an admin subject instead of failing on RLS\n ${chalk.red(\"(may produce a partial dump — see below)\")}\n ${chalk.blue(\"--row-security-role\")} <r> Role to read as with the flag above (default: admin)\n\n${chalk.green.bold(\"Row-level security\")}\n On a managed Postgres the dumping role usually owns nothing and has no\n BYPASSRLS, so pg_dump refuses:\n\n ERROR: query would be affected by row-level security policy for table \"...\"\n\n That refusal is the safe behaviour. --enable-row-security replaces it by\n reading as an admin subject: Rebase sets app.uid/app.user_roles so the\n generated admin_full_access policy admits the dump. The dump then contains\n exactly what those policies admit ${chalk.red(\"and no error is raised for what they do not\")} —\n a table whose policies lack an admin rule comes out short, silently.\n\n Granting the dumping role BYPASSRLS is the option that keeps a backup\n meaning \"every row\".\n\n${chalk.green.bold(\"Notes\")}\n Backups may contain secrets and PII. Use private storage destinations and\n enable encryption-at-rest. See docs/backups.md.\n`);\n}\n\nfunction printRestoreHelp(): void {\n print(`\n${chalk.bold(\"rebase db restore\")} — Restore a database from a backup (pg_restore)\n\n${chalk.green.bold(\"Usage\")}\n rebase db restore <backup> [options]\n\n${chalk.green.bold(\"Arguments\")}\n <backup> Local .dump file, or s3://…/gs://… object key\n\n${chalk.green.bold(\"Options\")}\n ${chalk.blue(\"--target-db\")} <name> Restore into this database instead of DATABASE_URL's\n ${chalk.blue(\"--create-db\")} Create the target database first if it doesn't exist\n ${chalk.blue(\"--clean\")} Drop existing objects before recreating them\n ${chalk.blue(\"--no-owner\")} Ignore ownership from the dump\n ${chalk.blue(\"--continue-on-error\")} Log and continue past errors ${chalk.red(\"(may leave RLS un-enforced!)\")}\n ${chalk.blue(\"--yes, -y\")} Skip the interactive confirmation ${chalk.red(\"(destructive!)\")}\n\n${chalk.red.bold(\"Warning\")}\n Restore is destructive and never runs automatically. Without --yes it\n requires an interactive 'yes'. Prefer --create-db/--target-db to restore\n into a fresh database rather than overwriting a live one.\n\n By default the restore aborts on the first error (--exit-on-error) so a\n skipped GRANT never silently leaves RLS un-enforced. Roles are recreated\n from the backup's .globals.sql sidecar first; keep that file next to the\n dump. Use --continue-on-error only when you understand the consequences.\n`);\n}\n\nfunction printBackupsHelp(): void {\n print(`\n${chalk.bold(\"rebase db backups\")} — Manage stored backups\n\n${chalk.green.bold(\"Usage\")}\n rebase db backups list [--out <path|s3://bucket/prefix>]\n rebase db backup list [--out <path|s3://bucket/prefix>] ${chalk.gray(\"(the same thing)\")}\n`);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-errors-C3g_kHBw.js","names":[],"sources":["../src/utils/pg-error-utils.ts","../src/cli-errors.ts"],"sourcesContent":["/**\n * Shared PostgreSQL error extraction and user-friendly message formatting.\n *\n * Drizzle wraps native PG errors in a `.cause` chain. These utilities\n * unwrap that chain to get the real PostgreSQL error (identified by a\n * 5-character alphanumeric `code` such as `42P01`) and translate it into\n * a message that is safe and helpful to show to end-users.\n */\n\nimport { logger } from \"@rebasepro/server\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { fieldKeyForColumn, getTableName } from \"@rebasepro/common\";\n\n/**\n * Shape of a deliberate client-facing error — `ApiError` from\n * `@rebasepro/server`, or anything else carrying a 4xx `statusCode`.\n *\n * Matched structurally rather than with `instanceof`: `@rebasepro/server` can\n * be loaded twice (published dist vs. workspace source), which breaks class\n * identity — `PersistService` hedges against the same thing by also accepting\n * `name === \"ApiError\"`.\n */\ninterface ClientFacingError extends Error {\n statusCode?: number;\n code?: string;\n /** See `ApiError.expected` — routine outcomes log at debug, not warn. */\n expected?: boolean;\n}\n\n/**\n * Return the error when it is a deliberate 4xx, otherwise null.\n *\n * A thrown `ApiError` is a decision the server made about the request, not a\n * database failure: its message and code are already written for the client.\n */\nfunction asClientFacingError(error: unknown): ClientFacingError | null {\n if (!(error instanceof Error)) return null;\n const e = error as ClientFacingError;\n if (typeof e.statusCode !== \"number\" || e.statusCode < 400 || e.statusCode >= 500) return null;\n return e;\n}\n\n/** Shape of PostgreSQL errors with diagnostic metadata. */\nexport interface PostgresError extends Error {\n code?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n column?: string;\n table?: string;\n dataType?: string;\n cause?: unknown;\n}\n\n/**\n * Extract the underlying PostgreSQL error from a Drizzle wrapper.\n * Drizzle wraps PG errors in a `cause` property — this function\n * recursively walks the chain until it finds an object with a PG\n * error code (5-char alphanumeric, e.g. `42P01`).\n */\nexport function extractPgError(error: unknown): PostgresError | null {\n if (!error || typeof error !== \"object\") return null;\n if (!(error instanceof Error)) {\n // Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)\n if (\"cause\" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === \"object\") {\n return extractPgError((error as Record<string, unknown>).cause);\n }\n return null;\n }\n\n // Check if the error itself has a PG error code\n if (\"code\" in error && typeof (error as PostgresError).code === \"string\" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {\n return error as PostgresError;\n }\n\n // Check the cause chain (Drizzle wraps PG errors)\n if (error.cause && typeof error.cause === \"object\") {\n return extractPgError(error.cause);\n }\n\n return null;\n}\n\n/**\n * Whether the failure came back from Postgres rather than from building the\n * query — which decides whether a fallback query is worth issuing.\n *\n * Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds\n * RLS). Once a statement raises, that transaction is aborted, and every later\n * statement on it returns `25P02` — \"current transaction is aborted, commands\n * ignored until end of transaction block\". So a retry after a database error\n * cannot succeed, and it replaces a precise diagnosis (\"invalid input syntax\n * for type uuid\") with a generic one. Rethrow instead.\n *\n * A query the driver could not even build — a missing reciprocal relation, say\n * — never reached Postgres, leaves the transaction usable, and is exactly what\n * the fallback paths exist for.\n */\nexport function reachedDatabase(error: unknown): boolean {\n return extractPgError(error) !== null;\n}\n\n/**\n * Walk the error cause chain and return the deepest meaningful message.\n */\nexport function extractCauseMessage(error: unknown): string | null {\n if (!error || typeof error !== \"object\") return null;\n if (!(error instanceof Error)) return null;\n\n if (error.cause && typeof error.cause === \"object\") {\n const deeper = extractCauseMessage(error.cause);\n if (deeper) return deeper;\n // The cause itself has a message\n if (error.cause instanceof Error && error.cause.message) {\n return error.cause.message;\n }\n }\n return null;\n}\n\n/**\n * Codes that mean \"this connection will never work as configured\".\n *\n * A wrong password or a database that does not exist is a settled fact about\n * the connection string, not a transient fault — retrying produces the same\n * answer forever.\n */\nconst UNRECOVERABLE_CONNECT_CODES = new Set([\n \"28P01\", // invalid_password\n \"28000\", // invalid_authorization_specification\n \"3D000\", // invalid_catalog_name — the database does not exist\n \"42501\" // insufficient_privilege\n]);\n\nexport interface ConnectFailure {\n /** True when retrying cannot help: the connection string itself is wrong. */\n fatal: boolean;\n /** The deepest message available — the Postgres one where there is one. */\n reason: string;\n /** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */\n code?: string;\n}\n\n/**\n * Describe a failed connection attempt in terms a developer can act on.\n *\n * The error a caller catches is Drizzle's wrapper: its message is\n * `Failed query: SELECT 1` and its stack runs through drizzle internals, while\n * the sentence that says what is actually wrong — \"password authentication\n * failed for user …\", \"database … does not exist\" — sits in `.cause`. Logging\n * the wrapper, as the bootstrapper used to, tells a developer with a typo in\n * their `DATABASE_URL` nothing at all.\n */\nexport function classifyConnectFailure(error: unknown): ConnectFailure {\n const pgError = extractPgError(error);\n const reason =\n pgError?.message ??\n extractCauseMessage(error) ??\n (error instanceof Error ? error.message : String(error));\n return {\n fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),\n reason,\n code: pgError?.code\n };\n}\n\n/**\n * Detect whether an error is specifically a role-switching permission failure\n * (e.g. \"permission denied to set role\" or \"must be member of role\"),\n * as opposed to a table-level permission denial.\n *\n * This is used by the backend driver to auto-disable role switching when the\n * connection user lacks SET ROLE privileges, rather than surfacing a confusing\n * error to the Studio SQL Editor user.\n */\nexport function isRoleSwitchingPermissionError(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n const msg = pgError.message.toLowerCase();\n return msg.includes(\"set role\") || msg.includes(\"member of role\");\n}\n\n/**\n * Was this `42501` the *caller* being refused by a policy, rather than the\n * server lacking a privilege?\n *\n * Both arrive as `insufficient_privilege`, and they are opposite kinds of\n * problem. A row-level-security refusal is a working access-control system\n * doing its job: the caller asked for something their policies do not permit,\n * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being\n * wrong — the connection role cannot touch the table at all, no policy is\n * involved, and nothing the caller changes about the request will help.\n *\n * Postgres distinguishes them in the message, so this does too:\n *\n * new row violates row-level security policy for table \"notes\" → the caller\n * permission denied for table notes → the server\n *\n * Only writes reach this. A read that RLS excludes is not an error — the rows\n * are filtered and the caller gets an empty page — so the erroring case is\n * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.\n *\n * Matched on the message because that is the only thing carrying the\n * distinction; the SQLSTATE is identical either way. Narrow by design: anything\n * not naming row-level security stays the server's problem, since reporting a\n * genuine privilege misconfiguration as \"forbidden\" would send an operator\n * hunting for a policy bug that does not exist.\n */\nexport function isRowLevelSecurityDenial(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n return pgError.message.toLowerCase().includes(\"row-level security policy\");\n}\n\n/**\n * One constraint the database refused, named in the caller's vocabulary.\n *\n * `field` is the **wire** name, derived from the physical column: `author_id`\n * on the table is `authorId` on the row and in the request body, and telling a\n * caller their `author_id` is missing sends them looking for a key their client\n * has never seen. `code` is the SQLSTATE, which is stable, documented by\n * Postgres and the same in every environment — unlike the message, which is\n * deliberately thinner in production.\n *\n * Deliberately no `value`. The column name is safe to publish: it is already in\n * the OpenAPI, the generated SDK and the caller's own request. The *value* is\n * not — `23505`'s detail reads `Key (email)=(a@b.c) already exists.`, which\n * answers \"is this person registered?\" for any address on a table whose rows\n * RLS is otherwise hiding completely.\n */\nexport interface PgFieldViolation {\n field: string;\n code: string;\n}\n\n/**\n * The physical column names a Postgres error is about.\n *\n * Postgres puts one in `column` for `23502` and nowhere else, so the rest are\n * read out of the two places that do carry them:\n *\n * - `detail` — `Key (author_id)=(9) is not present in table \"authors\".` and\n * `Key (email, tenant_id)=(…) already exists.` Only the parenthesised column\n * list is read; the values after `=` are never touched.\n * - `constraint` — `users_email_key`, `posts_author_id_fkey`. The table prefix\n * and the constraint suffix are stripped, leaving the columns joined by `_`,\n * which is ambiguous for multi-column constraints and is therefore only used\n * when `detail` gave nothing.\n *\n * `22P02` carries neither, because an invalid literal fails during parse rather\n * than against a constraint. Its message names the *type*, and an enum type is\n * generated as `<table>_<column>` — which is the one case worth decoding,\n * because a value outside an enum's labels is the most common 22P02 anyone\n * hits and the least legible one.\n */\nfunction offendingColumns(pgError: PostgresError, collection?: CollectionConfig): string[] {\n if (pgError.column) return [pgError.column];\n\n const detail = pgError.detail;\n if (detail) {\n const keyed = /Key \\(([^)]+)\\)=/.exec(detail);\n if (keyed) {\n return keyed[1].split(\",\").map(part => part.trim().replace(/^\"|\"$/g, \"\")).filter(Boolean);\n }\n }\n\n if (pgError.code === \"22P02\") {\n // `invalid input value for enum posts_status: \"archived\"`\n const enumType = /invalid input value for enum ([^:\\s]+)/i.exec(pgError.message ?? \"\");\n if (enumType && collection) {\n const typeName = enumType[1].replace(/^.*\\./, \"\").replace(/\"/g, \"\");\n const table = getTableName(collection);\n if (typeName.startsWith(`${table}_`)) return [typeName.slice(table.length + 1)];\n }\n return [];\n }\n\n const constraint = pgError.constraint;\n if (constraint && collection) {\n const table = getTableName(collection);\n const stripped = constraint\n .replace(new RegExp(`^${table}_`), \"\")\n .replace(/_(pkey|key|fkey|unique|check|excl|idx)$/, \"\");\n // Only when it names something the collection actually has: a\n // hand-written constraint name is not a column list, and guessing at one\n // would put a field in the response that does not exist.\n if (stripped && stripped !== constraint) {\n const columns = new Set<string>();\n for (const [key, prop] of Object.entries(collection.properties ?? {})) {\n columns.add((prop as { columnName?: string })?.columnName ?? key);\n }\n if (columns.has(stripped)) return [stripped];\n const wire = fieldKeyForColumn(collection, stripped);\n if (collection.properties && wire in collection.properties) return [stripped];\n }\n }\n\n return [];\n}\n\n/**\n * The fields a Postgres error is about, for `details.violations`.\n *\n * Only the four SQLSTATEs that describe *the caller's data*: a missing value, a\n * duplicate, a bad literal, a foreign key pointing at nothing. Everything else\n * — a dropped connection, a missing table, a privilege problem — is the\n * deployment's, and naming a field for it would send the reader to the wrong\n * place.\n *\n * Empty when the collection is unknown or the column cannot be recovered. An\n * empty list is the honest answer; inventing a field is worse than saying\n * nothing, because a form will mark the wrong input red.\n */\nexport function pgFieldViolations(\n pgError: PostgresError,\n collection?: CollectionConfig\n): PgFieldViolation[] {\n const code = pgError.code;\n if (code !== \"23502\" && code !== \"23505\" && code !== \"22P02\" && code !== \"23503\") return [];\n // Without the collection there is no mapping, only a guess. `toWireKey`\n // would get the default case right and silently invent a field for any\n // property carrying an explicit `columnName` — and a form pointed at an\n // input that does not exist is worse off than one shown the raw column.\n if (!collection) return [];\n const columns = offendingColumns(pgError, collection);\n if (columns.length === 0) return [];\n return columns.map(column => ({\n field: fieldKeyForColumn(collection, column),\n code\n }));\n}\n\n/**\n * Translate a raw PostgreSQL error into a user-friendly message.\n *\n * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})\n * @param context - A human-readable context string (e.g. collection slug or path)\n * @returns An object with a `message` safe for the client and the PG `code`.\n */\nexport function pgErrorToFriendlyMessage(\n pgError: PostgresError,\n context: string,\n options: { verbose?: boolean; collection?: CollectionConfig } = {}\n): { message: string; code: string; violations: PgFieldViolation[] } {\n // Postgres's own `DETAIL` and `HINT`, and the raw driver message, are what\n // make these errors useful while you are building — and what makes them an\n // oracle once the server is answering strangers.\n //\n // The sharp one is `23505`: its detail reads `Key (email)=(a@b.c) already\n // exists.`, which answers \"is this person registered?\" for any address, on\n // a table whose rows RLS is otherwise hiding completely. The rest leak\n // physical column and constraint names, which are the map for the next\n // question. So in production the shape of the failure is served — what\n // rule was broken, and where — and the values are not.\n //\n // The full text is not lost: the caller logs the original error. This\n // decides what crosses the wire, not what is recorded.\n const verbose = options.verbose ?? process.env.NODE_ENV !== \"production\";\n\n const detail = verbose ? (pgError.detail as string | undefined) : undefined;\n const hint = verbose ? (pgError.hint as string | undefined) : undefined;\n const constraint = pgError.constraint as string | undefined;\n const column = pgError.column as string | undefined;\n const table = pgError.table as string | undefined;\n const dataType = verbose ? (pgError.dataType as string | undefined) : undefined;\n const rawMessage = pgError.message || \"Unknown database error\";\n const pgMessage = verbose ? rawMessage : \"\";\n const code = pgError.code || \"UNKNOWN\";\n\n // Which *field* the caller has to fix, derived from the column. Safe in\n // production — a column name is already published in the OpenAPI and the\n // generated SDK — where the value that broke the rule is not, and is never\n // included. See `pgFieldViolations`.\n const violations = pgFieldViolations(pgError, options.collection);\n\n const suffix = hint ? ` Hint: ${hint}` : \"\";\n const tableRef = table ?? context;\n\n /** `\": <postgres said>\"`, or nothing when the details are withheld. */\n const said = pgMessage ? `: ${pgMessage}` : \"\";\n\n switch (pgError.code) {\n case \"23503\": // foreign_key_violation\n return {\n message: detail\n ? `Foreign key constraint violated: ${detail}${suffix}`\n : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"23505\": // unique_violation\n return {\n message: detail\n ? `Duplicate value: ${detail}${suffix}`\n : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"23502\": {\n // not_null_violation. The message names the *field* when the\n // collection is known, because that is the key the caller sent and\n // the only one they can act on: told `author_id` is missing, they go\n // looking for a key their generated client has never mentioned.\n const named = violations[0]?.field ?? column ?? \"unknown\";\n return {\n message: `Missing required field: \"${named}\" in \"${tableRef}\" cannot be empty.${suffix}`,\n code,\n violations\n };\n }\n case \"23514\": // check_violation\n return {\n message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code,\n violations\n };\n case \"22P02\": // invalid_text_representation (e.g. invalid UUID, wrong enum value)\n return {\n message: `Invalid data format in \"${context}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"22001\": // string_data_right_truncation (value too long)\n return {\n message: `Value too long for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"22003\": // numeric_value_out_of_range\n return {\n message: `Numeric value out of range for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code,\n violations\n };\n case \"42703\": // undefined_column\n return {\n message: `Unknown column in \"${tableRef}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code,\n violations\n };\n case \"42P01\": // undefined_table\n return {\n message: `Table not found for \"${context}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code,\n violations\n };\n case \"42501\": // insufficient_privilege\n // Two unrelated failures share this SQLSTATE, and the old message\n // named both causes because it could not tell them apart — which\n // meant it was half wrong whichever one had happened, and sent the\n // reader to check the other. Postgres says which in its own message.\n return rawMessage.toLowerCase().includes(\"row-level security policy\")\n ? {\n // The caller. Their policies do not permit this row; the\n // deployment is working exactly as configured.\n message: `Not permitted to write this row in \"${tableRef}\": it does not satisfy the row-level security policy.${suffix}`,\n code,\n violations\n }\n : {\n // The deployment. No policy is involved — the connecting\n // role cannot touch the table at all.\n message: `Permission denied on \"${tableRef}\": the database role this server connects as lacks privileges on it.${suffix}`,\n code,\n violations\n };\n case \"28000\": // invalid_authorization_specification\n return {\n message: `Authorization failed for \"${context}\". Check your database credentials.${suffix}`,\n code,\n violations\n };\n default: {\n // Unhandled PG code — still surface the actual database message\n const parts = [`Database error in \"${context}\" [${code}]${said}`];\n if (detail) parts.push(`Detail: ${detail}`);\n if (column) parts.push(`Column: ${column}`);\n if (dataType) parts.push(`Data type: ${dataType}`);\n if (constraint) parts.push(`Constraint: ${constraint}`);\n if (hint) parts.push(`Hint: ${hint}`);\n return { message: parts.join(\". \"), code, violations };\n }\n }\n}\n\n/**\n * Sanitize any error into a message safe and helpful for the client.\n *\n * A deliberate 4xx (`ApiError`) passes through untouched — the server already\n * decided what the client should read. Otherwise the PG error is extracted\n * from the Drizzle cause chain, falling back to a generic message that\n * doesn't leak SQL.\n *\n * @param error - The raw caught error\n * @param context - A human-readable context string (e.g. collection path)\n * @returns An object with `message` (user-friendly) and optional `code`\n * (the `ApiError` code, or the PG SQLSTATE).\n */\nexport function sanitizeErrorForClient(\n error: unknown,\n context: string,\n options: { collection?: CollectionConfig } = {}\n): { message: string; code?: string; violations?: PgFieldViolation[] } {\n // ── A deliberate 4xx is not a database failure ──────────────────\n // Its message and code are written for the client; replacing them with\n // \"Check server logs\" would discard the only diagnosis the caller gets\n // (e.g. the offending filter field and the collection's valid ones).\n // Log level follows the same convention as the HTTP error handler in\n // @rebasepro/server: routine outcomes at debug, everything else at warn.\n const clientError = asClientFacingError(error);\n if (clientError) {\n const line = `[API ${clientError.statusCode} ${clientError.code ?? \"BAD_REQUEST\"}] in \"${context}\": ${clientError.message}`;\n if (clientError.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n return { message: clientError.message, ...(clientError.code && { code: clientError.code }) };\n }\n\n // ── Always log the full, unsanitized error server-side ──────────\n const pgError = extractPgError(error);\n\n if (pgError) {\n logger.error(`[PG ${pgError.code}] Error in \"${context}\"`, {\n code: pgError.code,\n message: pgError.message,\n detail: pgError.detail,\n hint: pgError.hint,\n column: pgError.column,\n table: pgError.table,\n constraint: pgError.constraint,\n dataType: pgError.dataType\n // The outer Drizzle wrapper message used to be logged here \"for\n // full context\": it is `Failed query: <sql>\\nparams: <values>`, so\n // it published the statement and every bound value (an email, a\n // password hash) on every realtime data failure. The SQLSTATE,\n // detail, table, column and constraint above are the diagnostic\n // value; the wrapper added only the leak. `logger` strips the\n // wrapper as well, but the field itself carried nothing else.\n });\n const { message, code, violations } = pgErrorToFriendlyMessage(pgError, context, { collection: options.collection });\n return { message, code, ...(violations.length > 0 && { violations }) };\n }\n\n // No PG error found — log the raw error as-is\n logger.error(`Database error in \"${context}\" (no PG error extracted)`, {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n cause: error instanceof Error && error.cause\n ? (error.cause instanceof Error ? error.cause.message : String(error.cause))\n : undefined\n });\n\n // Try to get the deepest cause message\n const causeMessage = extractCauseMessage(error);\n if (causeMessage) {\n return { message: `Database error in \"${context}\": ${causeMessage}` };\n }\n\n // Last resort — generic message, never leak raw SQL\n return { message: `Could not load data for \"${context}\". Check server logs for details.` };\n}\n","import chalk from \"chalk\";\nimport { outWarn, outError } from \"./cli-output\";\nimport { extractCauseMessage } from \"./utils/pg-error-utils\";\n\n/**\n * Detect whether an error (or AggregateError wrapping multiple attempts)\n * represents an ECONNREFUSED — i.e. the database is simply not running.\n *\n * Handles:\n * - Direct `{ code: \"ECONNREFUSED\" }` errors from Node `net`\n * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts\n * - Drizzle's `cause`-wrapped pg errors\n */\nexport function isEconnrefused(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown; errors?: unknown[] };\n if (e.code === \"ECONNREFUSED\") return true;\n // AggregateError from Node net (dual-stack IPv4 + IPv6)\n if (Array.isArray(e.errors)) {\n return e.errors.some(inner =>\n inner && typeof inner === \"object\" && (inner as { code?: string }).code === \"ECONNREFUSED\"\n );\n }\n // Drizzle wraps the pg error in `cause`\n if (e.cause && typeof e.cause === \"object\") {\n return isEconnrefused(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL authentication failures.\n * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)\n */\nexport function isAuthFailure(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown };\n if (e.code === \"28P01\" || e.code === \"28000\") return true;\n if (e.cause && typeof e.cause === \"object\") {\n return isAuthFailure(e.cause);\n }\n // Also check the message for common pg auth failure text\n if (\"message\" in e && typeof (e as { message?: string }).message === \"string\") {\n const msg = (e as { message: string }).message.toLowerCase();\n if (msg.includes(\"password authentication failed\") || msg.includes(\"no pg_hba.conf entry\")) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Detect the \"SSL is not enabled on the server\" failure — the client attempted\n * an SSL handshake against a Postgres server that doesn't support it (common\n * with a plain local dev database). The fix is `?sslmode=disable` on the URL.\n */\nexport function isSslNotEnabled(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { message?: string; cause?: unknown };\n if (typeof e.message === \"string\" && e.message.toLowerCase().includes(\"ssl is not enabled on the server\")) {\n return true;\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isSslNotEnabled(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL \"cannot drop ... because other objects depend on it\"\n * (error code 2BP01, dependent_objects_still_exist). This is the failure that\n * strands a declarative `db push` half-applied when a collection is removed but\n * an enum type it defined is still referenced by another object.\n */\nexport function isDependencyDropError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; message?: string; cause?: unknown };\n if (e.code === \"2BP01\") return true;\n if (typeof e.message === \"string\") {\n const msg = e.message.toLowerCase();\n if (msg.includes(\"other objects depend on it\") || msg.includes(\"cannot drop type\")) {\n return true;\n }\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isDependencyDropError(e.cause);\n }\n return false;\n}\n\n/**\n * Parse host:port from a DATABASE_URL for display purposes.\n *\n * Exported because every message about a connection has to name the thing it\n * could not reach, and the boot path needs the same rendering the CLI banners\n * use — including the same refusal to print the URL itself, which carries the\n * password.\n */\nexport function parseHostInfo(databaseUrl: string): string {\n try {\n const parsed = new URL(databaseUrl);\n return `${parsed.hostname}:${parsed.port || 5432}`;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * The sentence the operating system actually produced, dug out of the wrappers.\n *\n * `connect ECONNREFUSED 127.0.0.1:5432` is written by `net`, then wrapped by\n * `pg`, then wrapped again by Drizzle as `Failed query: …` — and on a\n * dual-stack host it is not in `.cause` at all but inside the\n * `AggregateError.errors` array of one attempt per resolved address. Printing\n * the banner without it loses the one token every search engine, runbook and\n * colleague recognises.\n */\nexport function deepestErrorMessage(err: unknown): string | null {\n if (!err || typeof err !== \"object\") return null;\n const e = err as { message?: string; code?: string; cause?: unknown; errors?: unknown[] };\n\n if (Array.isArray(e.errors)) {\n for (const inner of e.errors) {\n const deeper = deepestErrorMessage(inner);\n if (deeper) return deeper;\n }\n }\n if (e.cause) {\n const deeper = deepestErrorMessage(e.cause);\n if (deeper) return deeper;\n }\n if (typeof e.message === \"string\" && e.message && !e.message.startsWith(\"Failed query:\")) {\n return e.code ? `${e.message} (${e.code})` : e.message;\n }\n return null;\n}\n\n/** One indented line naming the driver's own reason, or nothing. */\nfunction driverReasonLine(err: unknown): string {\n const reason = err === undefined ? null : deepestErrorMessage(err);\n return reason ? ` The driver said: ${reason}\\n\\n` : \"\";\n}\n\n/**\n * Format a diagnostic banner for ECONNREFUSED errors.\n */\nfunction formatConnectionRefusedBanner(databaseUrl: string, err?: unknown): string {\n const hostInfo = parseHostInfo(databaseUrl);\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n driverReasonLine(err) +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for authentication failures.\n */\nfunction formatAuthFailureBanner(databaseUrl: string, err?: unknown): string {\n const hostInfo = parseHostInfo(databaseUrl);\n let username = \"unknown\";\n try {\n username = new URL(databaseUrl).username || \"unknown\";\n } catch { /* ignore */ }\n\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Authentication failed for user \"${username}\" at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n driverReasonLine(err) +\n ` PostgreSQL rejected the credentials. Common fixes:\\n` +\n `\\n` +\n ` • Check the username and password in DATABASE_URL\\n` +\n ` • Verify the user exists: psql -c \"\\\\du\"\\n` +\n ` • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for \"SSL is not enabled on the server\".\n */\nfunction formatSslNotEnabledBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n const suggestion = databaseUrl.includes(\"?\") ? \"&sslmode=disable\" : \"?sslmode=disable\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ SSL is not enabled on the PostgreSQL server at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The client tried to connect over SSL, but the server does not\\n` +\n ` support it. This is normal for a plain local dev database.\\n` +\n `\\n` +\n ` Fix: append ${chalk.bold(\"sslmode=disable\")} to DATABASE_URL, e.g.\\n` +\n `\\n` +\n ` DATABASE_URL=...${suggestion}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for a dependency-drop failure during `db push`.\n * Explains that the database may be left partially migrated and how to recover.\n */\nfunction formatDependencyDropBanner(): string {\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Schema push failed: a type/table could not be dropped\\n` +\n ` because other objects still depend on it.\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${chalk.yellow(\"The database may now be partially migrated.\")} Atlas applies\\n` +\n ` statements individually, so earlier changes in this push may\\n` +\n ` already be committed while later ones failed.\\n` +\n `\\n` +\n ` This commonly happens when a collection is removed but an enum\\n` +\n ` type it defined is still referenced. To recover:\\n` +\n `\\n` +\n ` 1. Inspect the leftover object named in the error above.\\n` +\n ` 2. Drop it with CASCADE, e.g.:\\n` +\n ` psql \"$DATABASE_URL\" -c 'DROP TYPE \"<name>\" CASCADE;'\\n` +\n ` 3. Re-run: ${chalk.bold.green(\"rebase db push\")}\\n` +\n `\\n` +\n ` Prefer a safe, versioned workflow? Use ${chalk.bold(\"rebase db generate\")}\\n` +\n ` + ${chalk.bold(\"rebase db migrate\")} instead of push for destructive changes.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Pre-flight check: verify that the database is reachable before running\n * a heavy subprocess (Atlas, migrations, etc.).\n *\n * Exits with code 1 and a friendly banner on known failure modes.\n * On unknown errors, logs a warning and allows the caller to proceed.\n */\nexport async function checkDatabaseConnectivity(databaseUrl: string): Promise<void> {\n let client: import(\"pg\").Client | undefined;\n try {\n const { Client } = await import(\"pg\");\n client = new Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: 5000\n });\n await client.connect();\n await client.query(\"SELECT 1\");\n } catch (err: unknown) {\n if (isEconnrefused(err)) {\n outError(formatConnectionRefusedBanner(databaseUrl, err));\n process.exit(1);\n }\n if (isAuthFailure(err)) {\n outError(formatAuthFailureBanner(databaseUrl, err));\n process.exit(1);\n }\n if (isSslNotEnabled(err)) {\n outError(formatSslNotEnabledBanner(databaseUrl));\n process.exit(1);\n }\n // Unknown error — warn but don't block; let the downstream tool surface details\n outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));\n outWarn(chalk.gray(\" Proceeding anyway — the command may fail if the database is unreachable.\"));\n } finally {\n try {\n await client?.end();\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n/**\n * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).\n * Returns a formatted diagnostic string if the error matches a known pattern,\n * or null if unrecognized.\n */\nexport function diagnoseDbError(err: unknown, databaseUrl?: string): string | null {\n if (isEconnrefused(err)) {\n return formatConnectionRefusedBanner(databaseUrl || \"\", err);\n }\n if (isAuthFailure(err)) {\n return formatAuthFailureBanner(databaseUrl || \"\", err);\n }\n if (isSslNotEnabled(err)) {\n return formatSslNotEnabledBanner(databaseUrl || \"\");\n }\n if (isDependencyDropError(err)) {\n return formatDependencyDropBanner();\n }\n return null;\n}\n\n/**\n * Say why the command failed, on the way out.\n *\n * The entry point below used to be `.catch(() => process.exit(1))`, which threw\n * the error away. Every message this file and its services raise — \"Branch\n * \\\"x\\\" already exists.\", \"the source database has active connections\", \"Branch\n * name is too long\" — was written, wrapped in the right PG error code, and then\n * discarded one frame before it reached a terminal. What a developer saw was a\n * header line, no error, and exit 1.\n *\n * Two shapes are deliberately kept quiet:\n *\n * - **A child process that already spoke.** Atlas, `pg_dump` and `psql` run\n * with inherited stdio, so their diagnosis is on the terminal already and\n * execa's wrapper adds only `Command failed with exit code 1: atlas …`.\n * `packages/cli` filters exactly these two phrasings one level up\n * (`runDbCommand`), and this is that filter, for the process that is actually\n * throwing.\n *\n * - **A message that is only a query.** Drizzle reports failures as\n * `Failed query: <sql> params:` and hides the real PostgreSQL error in\n * `cause`, so the wrapper alone tells a reader nothing they can act on. The\n * cause is appended when it says something the message does not.\n *\n * - **An error that has already printed its own diagnosis**, marked\n * `alreadyReported`. `CollectionsPathMissing` is one: it prints the path, what\n * it resolved to and the cwd it resolved against, and then throws so the entry\n * point owns the exit code. Repeating its one-line summary underneath would\n * undo the \"printed once\" this whole path exists for.\n */\nexport function reportCommandFailure(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n\n if ((error as { alreadyReported?: boolean } | null)?.alreadyReported) return;\n if (!message || /Command failed|exited with code/i.test(message)) return;\n\n outError(\"\");\n outError(chalk.red(` ✗ ${message}`));\n\n const cause = extractCauseMessage(error);\n if (cause && !message.includes(cause)) {\n outError(chalk.gray(` ${cause}`));\n }\n outError(\"\");\n}\n\n/* ------------------------------------------------------------------------- *\n * Atlas failures that have a remedy\n *\n * Atlas runs with a teed stderr, so what reaches these is its output as text,\n * not a `pg` error object carrying a `code`. They are deliberately NOT part of\n * `diagnoseDbError`: that one is shared with the boot path, and \"record a\n * baseline with `rebase db migrate`\" is advice for somebody standing at a\n * terminal, not for a container that has just failed to start.\n * ------------------------------------------------------------------------- */\n\n/**\n * `migrate apply` refusing because the database already has the schema.\n *\n * `42710` (duplicate_object) and `42P07` (duplicate_table) are what a migration\n * hits when boot-ensure — or a `db push` — has already provisioned the objects\n * it was going to create. Since boot-ensure provisions *every* production\n * database, this is the normal case rather than the exotic one, and the raw\n * failure (`pq: type \"posts_status\" already exists (42710)`, then `sql/migrate:\n * write revision: … current transaction is aborted`) names no way forward.\n */\nexport function parseAlreadyProvisioned(text: string): { object: string; code: string } | null {\n const withCode = /(?:type|relation|constraint|schema) \"([^\"]+)\" already exists \\((42710|42P07)\\)/.exec(text);\n if (withCode) return { object: withCode[1], code: withCode[2] };\n // Some Atlas builds keep the SQLSTATE on a following line instead.\n const bare = /(?:type|relation|constraint|schema) \"([^\"]+)\" already exists/.exec(text);\n if (bare && /42710|42P07/.test(text)) return { object: bare[1], code: /42P07/.test(text) ? \"42P07\" : \"42710\" };\n return null;\n}\n\n/**\n * What to do about it: tell Atlas the database is already at a version.\n *\n * Atlas's own mechanism (`migrate apply --baseline <version>`), not a ledger of\n * ours — it writes the revision row Atlas reads, so every later `rebase db\n * migrate` is an ordinary one.\n */\nexport function formatBaselineRemedy(version: string | null, object?: string): string {\n const versionToken = version ?? \"<version>\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ This database already has the schema this migration creates\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n (object ? ` ${chalk.gray(`\"${object}\" is already there, so the migration cannot create it.`)}\\n\\n` : \"\") +\n ` Nothing is wrong with the migration — the database was provisioned\\n` +\n ` another way. Every Rebase boot ensures the schema, and ${chalk.bold(\"rebase db push\")}\\n` +\n ` applies it directly, so a database that has ever run either one is\\n` +\n ` ahead of a migration history that was never recorded.\\n` +\n `\\n` +\n ` Record where it already is, then migrate normally:\\n` +\n `\\n` +\n ` ${chalk.bold.green(`rebase db migrate --baseline ${versionToken}`)}\\n` +\n ` ${chalk.bold.green(\"rebase db migrate\")}\\n` +\n `\\n` +\n ` The baseline is the version already applied — the numeric prefix of\\n` +\n ` the migration file describing what is in the database now. Migrations\\n` +\n ` after it run; it and everything before it are marked done.\\n` +\n `\\n` +\n ` On a database nothing has ever booted against, none of this is needed.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** `ALTER TABLE … SET NOT NULL` on a table that already holds rows (23502). */\nexport function parseNotNullViolation(text: string): { table: string; column: string } | null {\n const match = /column \"([^\"]+)\" of relation \"([^\"]+)\" contains null values/.exec(text);\n return match ? { table: match[2], column: match[1] } : null;\n}\n\n/**\n * The three ways out, because there is no fourth.\n *\n * Boot-ensure handles this case — it adds the column nullable and sets NOT NULL\n * only when the table is empty — so a push that dies here is strictly worse\n * than the boot that would have run instead, and the developer deserves to be\n * told which of the three they want rather than left with `pq: … contains null\n * values (23502)` and no next step.\n */\nexport function formatNotNullViolationBanner(\n violation: { table: string; column: string },\n rowCount: number | null\n): string {\n const { table, column } = violation;\n const rows = rowCount === null\n ? ` \"${table}\" already holds rows, and none of them has a value for \"${column}\".\\n`\n : ` \"${table}\" already holds ${rowCount} row${rowCount === 1 ? \"\" : \"s\"}, and they have no value for \"${column}\".\\n`;\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot make \"${table}\".\"${column}\" required: existing rows are null\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n rows +\n ` PostgreSQL will not add the NOT NULL until every one of them does.\\n` +\n `\\n` +\n ` Three ways forward:\\n` +\n `\\n` +\n ` 1. Give the property a default, so the push can backfill:\\n` +\n ` ${chalk.gray(`{ type: \"string\", defaultValue: \"…\", validation: { required: true } }`)}\\n` +\n ` 2. Backfill by hand first, then push again:\\n` +\n ` ${chalk.gray(`psql \"$DATABASE_URL\" -c 'UPDATE \"${table}\" SET \"${column}\" = … WHERE \"${column}\" IS NULL;'`)}\\n` +\n ` 3. Leave it optional — take \\`validation.required\\` off the property.\\n` +\n `\\n` +\n ` ${chalk.gray(\"Nothing was applied for this column. `rebase dev` would have added it\")}\\n` +\n ` ${chalk.gray(\"nullable instead: boot sets NOT NULL only when it is safe.\")}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** Atlas refusing to remove a label from an enum — a renamed `enum` option id. */\nexport function parseEnumLabelDrop(text: string): { label: string; enumType: string } | null {\n const match = /dropping (?:enum )?value \"([^\"]+)\" from enum \"([^\"]+)\" is not supported/.exec(text);\n return match ? { label: match[1], enumType: match[2] } : null;\n}\n\n/**\n * Why the two paths disagree, and what retiring an option id actually costs.\n *\n * Boot-ensure adds enum labels and never removes one, so `rebase dev` accepts\n * the very edit that stops `db push` dead. PostgreSQL has no `ALTER TYPE …\n * DROP VALUE`: a label goes only by rewriting the type, which is a data\n * migration and not a schema push.\n */\nexport function formatEnumLabelDropBanner(drop: { label: string; enumType: string }): string {\n const { label, enumType } = drop;\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ \"${label}\" cannot be removed from the enum \"${enumType}\"\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` PostgreSQL has no ALTER TYPE … DROP VALUE. An option id can be added\\n` +\n ` to an enum but never taken out of one, so renaming an id reads as a\\n` +\n ` removal and a push cannot carry it.\\n` +\n `\\n` +\n ` ${chalk.yellow(\"Nothing was applied.\")} ${chalk.bold(\"rebase dev\")} accepts this same edit — boot adds\\n` +\n ` new labels and never removes one — so the two paths disagree by\\n` +\n ` design, and the old label would simply have stayed behind, unused.\\n` +\n `\\n` +\n ` To keep the id: put \"${label}\" back and change only its label text.\\n` +\n `\\n` +\n ` To retire it for real, it is a data migration:\\n` +\n `\\n` +\n ` 1. Add the new id alongside the old one, and push.\\n` +\n ` 2. Move the rows: ${chalk.gray(`UPDATE … SET … = '<new>' WHERE … = '${label}';`)}\\n` +\n ` 3. Rewrite the type, which is what actually drops the label:\\n` +\n ` ${chalk.gray(\"rebase db generate retire_option && rebase db migrate\")}\\n` +\n ` ${chalk.gray(\"(a migration can CREATE TYPE …, ALTER TABLE … TYPE … USING, DROP TYPE)\")}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** How many rows a table holds, or `null` when we cannot say. */\nasync function countRows(databaseUrl: string | undefined, table: string): Promise<number | null> {\n if (!databaseUrl) return null;\n try {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: databaseUrl, connectionTimeoutMillis: 5000 });\n await client.connect();\n try {\n // Identifier interpolation, and it has to be: the name comes from\n // PostgreSQL's own error text, and `count(*)` takes no parameter in\n // that position. Quoted, with any embedded quote doubled.\n const res = await client.query(`SELECT count(*)::int AS n FROM \"${table.replace(/\"/g, \"\\\"\\\"\")}\"`);\n return (res.rows[0] as { n: number } | undefined)?.n ?? null;\n } finally {\n await client.end();\n }\n } catch {\n return null;\n }\n}\n\n/**\n * A hand-written generated column reading a column the plan wants to retype or\n * drop.\n *\n * Rebase's own generated columns are dropped and rebuilt around the apply — it\n * holds their definition in `search.sql`. This one it does not: PostgreSQL\n * stores a generated expression only in the catalogue, so dropping it to let\n * the apply through would destroy the only copy. Refuse instead, and say what\n * the operator can do about it.\n */\nexport function formatForeignGeneratedColumnBanner(\n conflicts: { schema: string; table: string; column: string; blocking: { column: string; kind: string }[] }[]\n): string {\n const lines = conflicts.map(c => {\n const reads = c.blocking.map(b => `\"${b.column}\"`).join(\", \");\n return ` ${chalk.bold(`${c.schema}.${c.table}.${c.column}`)} reads ${reads}`;\n });\n const first = conflicts[0];\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ A generated column depends on a column this push changes\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n lines.join(\"\\n\") + `\\n` +\n `\\n` +\n ` PostgreSQL refuses to retype or drop a column while a GENERATED\\n` +\n ` ALWAYS AS … STORED expression reads it, and an Atlas apply is one\\n` +\n ` transaction — so this refusal would roll back every other statement\\n` +\n ` in the push with it.\\n` +\n `\\n` +\n ` ${chalk.yellow(\"Nothing was applied.\")} Rebase rebuilds the generated columns it made\\n` +\n ` itself (a collection's ${chalk.bold(\"search\")} block) around the apply. It has no\\n` +\n ` definition for this one, and a dropped generated expression is not\\n` +\n ` recoverable from anywhere but your own migration.\\n` +\n `\\n` +\n ` Drop it, push, and put it back:\\n` +\n `\\n` +\n ` ${chalk.gray(`ALTER TABLE \"${first.schema}\".\"${first.table}\" DROP COLUMN \"${first.column}\";`)}\\n` +\n ` ${chalk.gray(\"rebase db push\")}\\n` +\n ` ${chalk.gray(\"-- then re-create the column with its original expression\")}\\n` +\n `\\n` +\n ` Or declare it through a ${chalk.bold(\"search\")} block, and Rebase will own it.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * The remedy for an Atlas invocation that failed, or `null`.\n *\n * Scoped by the invocation, because the same database state means different\n * things to different subcommands: \"already exists\" under `migrate apply` wants\n * a baseline, while under `schema apply` it is a genuine conflict.\n */\nexport async function diagnoseAtlasFailure(context: {\n domain: string;\n args: string[];\n stderr: string;\n databaseUrl?: string;\n /** The newest migration version on disk, named in the baseline remedy. */\n latestMigrationVersion?: string | null;\n}): Promise<string | null> {\n const { domain, args, stderr, databaseUrl } = context;\n\n if (domain === \"migrate\" && args.includes(\"apply\")) {\n const provisioned = parseAlreadyProvisioned(stderr);\n if (provisioned) {\n return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object);\n }\n }\n\n if (domain === \"schema\" && args.includes(\"apply\")) {\n const enumDrop = parseEnumLabelDrop(stderr);\n if (enumDrop) return formatEnumLabelDropBanner(enumDrop);\n\n const notNull = parseNotNullViolation(stderr);\n if (notNull) return formatNotNullViolationBanner(notNull, await countRows(databaseUrl, notNull.table));\n }\n\n return null;\n}\n"],"mappings":"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"}
|