@rebasepro/server-postgres 0.25.0-canary.ge06257e → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/{BranchService-DNg4txyf.js → BranchService-iXXZeVYD.js} +222 -19
  2. package/dist/BranchService-iXXZeVYD.js.map +1 -0
  3. package/dist/backup/backup-cli.d.ts +33 -1
  4. package/dist/backup/backup-service.d.ts +68 -0
  5. package/dist/backup/pg-binary.d.ts +2 -2
  6. package/dist/backup/pg-tools.d.ts +60 -1
  7. package/dist/{backup-cli-HCEkPcQj.js → backup-cli-B2CAn9fR.js} +105 -14
  8. package/dist/backup-cli-B2CAn9fR.js.map +1 -0
  9. package/dist/{backup-service-D-UpT7DH.js → backup-service-Bb39plam.js} +358 -21
  10. package/dist/backup-service-Bb39plam.js.map +1 -0
  11. package/dist/{boot-ddl-Dnvpmm_T.js → boot-ddl-s_3Hkz94.js} +2 -2
  12. package/dist/{boot-ddl-Dnvpmm_T.js.map → boot-ddl-s_3Hkz94.js.map} +1 -1
  13. package/dist/branch-prune.d.ts +18 -0
  14. package/dist/{cli-console-C_ISGwJo.js → cli-console-DQ_K-sdM.js} +17 -10
  15. package/dist/cli-console-DQ_K-sdM.js.map +1 -0
  16. package/dist/cli-console.d.ts +4 -3
  17. package/dist/{cli-errors-mx4sSjB8.js → cli-errors-DwQnNATs.js} +80 -12
  18. package/dist/{cli-errors-mx4sSjB8.js.map → cli-errors-DwQnNATs.js.map} +1 -1
  19. package/dist/cli-errors.d.ts +12 -1
  20. package/dist/{cli-flags-DUozBFyK.js → cli-flags-D-b0qL9T.js} +3 -2
  21. package/dist/{cli-flags-DUozBFyK.js.map → cli-flags-D-b0qL9T.js.map} +1 -1
  22. package/dist/cli.js +106 -40
  23. package/dist/cli.js.map +1 -1
  24. package/dist/collections/validate-relations.d.ts +8 -2
  25. package/dist/{deferred-index-build-Ca6OqKyR.js → deferred-index-build-Cis_HoGn.js} +6 -6
  26. package/dist/{deferred-index-build-Ca6OqKyR.js.map → deferred-index-build-Cis_HoGn.js.map} +1 -1
  27. package/dist/{doctor-CJyuhV1n.js → doctor-DvJH-IQG.js} +6 -6
  28. package/dist/doctor-DvJH-IQG.js.map +1 -0
  29. package/dist/{ensure-collection-policies-C1ux3V_z.js → ensure-collection-policies-CCqayoUz.js} +54 -8
  30. package/dist/ensure-collection-policies-CCqayoUz.js.map +1 -0
  31. package/dist/{ensure-collection-tables-Do-Bvb64.js → ensure-collection-tables-DvvImXDe.js} +7 -7
  32. package/dist/{ensure-collection-tables-Do-Bvb64.js.map → ensure-collection-tables-DvvImXDe.js.map} +1 -1
  33. package/dist/{ensure-tables-Dl_Eh9ow.js → ensure-tables-Dpgwx5sy.js} +2 -2
  34. package/dist/{ensure-tables-Dl_Eh9ow.js.map → ensure-tables-Dpgwx5sy.js.map} +1 -1
  35. package/dist/{enum-labels-kept-D9eTIWCz.js → enum-labels-kept-CUlUlRkO.js} +3 -3
  36. package/dist/{enum-labels-kept-D9eTIWCz.js.map → enum-labels-kept-CUlUlRkO.js.map} +1 -1
  37. package/dist/{generate-drizzle-schema-Bf8canaR.js → generate-drizzle-schema-C3EAP9HE.js} +3 -3
  38. package/dist/{generate-drizzle-schema-Bf8canaR.js.map → generate-drizzle-schema-C3EAP9HE.js.map} +1 -1
  39. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js → generate-drizzle-schema-logic-DA3zM8zc.js} +3 -3
  40. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js.map → generate-drizzle-schema-logic-DA3zM8zc.js.map} +1 -1
  41. package/dist/{generate-files-D50Pxw1G.js → generate-files-ClmOUHXj.js} +5 -5
  42. package/dist/{generate-files-D50Pxw1G.js.map → generate-files-ClmOUHXj.js.map} +1 -1
  43. package/dist/{generate-next-step-DCd7yiJ2.js → generate-next-step-DCeS4LrJ.js} +22 -14
  44. package/dist/{generate-next-step-DCd7yiJ2.js.map → generate-next-step-DCeS4LrJ.js.map} +1 -1
  45. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js → generate-postgres-ddl-logic-4eUP4t8w.js} +3 -3
  46. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js.map → generate-postgres-ddl-logic-4eUP4t8w.js.map} +1 -1
  47. package/dist/index.es.js +8 -8
  48. package/dist/{introspect-db-logic-j2vYhmdP.js → introspect-db-logic-CI5CU_n0.js} +2 -2
  49. package/dist/{introspect-db-logic-j2vYhmdP.js.map → introspect-db-logic-CI5CU_n0.js.map} +1 -1
  50. package/dist/{open-rebase-D8rytTP0.js → open-rebase-BKA7Nyoe.js} +47 -27
  51. package/dist/{open-rebase-D8rytTP0.js.map → open-rebase-BKA7Nyoe.js.map} +1 -1
  52. package/dist/{pg-error-utils-CdsGYMTm.js → pg-error-utils-Bv5n2PLn.js} +11 -4
  53. package/dist/pg-error-utils-Bv5n2PLn.js.map +1 -0
  54. package/dist/{plan-schema-CsNgK0qi.js → plan-schema-CXOucCw-.js} +2 -2
  55. package/dist/{plan-schema-CsNgK0qi.js.map → plan-schema-CXOucCw-.js.map} +1 -1
  56. package/dist/{policy-drift-CY5P1ZRu.js → policy-drift-vm0BcvcE.js} +2 -2
  57. package/dist/{policy-drift-CY5P1ZRu.js.map → policy-drift-vm0BcvcE.js.map} +1 -1
  58. package/dist/{push-index-prebuild-BDFcfRPo.js → push-index-prebuild-DIequ4bV.js} +4 -4
  59. package/dist/{push-index-prebuild-BDFcfRPo.js.map → push-index-prebuild-DIequ4bV.js.map} +1 -1
  60. package/dist/{push-legacy-renames-D4bvMWAb.js → push-legacy-renames-BqJut4F3.js} +4 -4
  61. package/dist/{push-legacy-renames-D4bvMWAb.js.map → push-legacy-renames-BqJut4F3.js.map} +1 -1
  62. package/dist/{push-policy-plan-DTXE6aVa.js → push-policy-plan-CvX8hiPb.js} +22 -6
  63. package/dist/push-policy-plan-CvX8hiPb.js.map +1 -0
  64. package/dist/{render-ddl-CrfEe__B.js → render-ddl-CQNwZdse.js} +3 -3
  65. package/dist/{render-ddl-CrfEe__B.js.map → render-ddl-CQNwZdse.js.map} +1 -1
  66. package/dist/schema/atlas-argv.d.ts +3 -1
  67. package/dist/schema/doctor-cli.js +4 -4
  68. package/dist/schema/ensure-collection-policies.d.ts +24 -0
  69. package/dist/schema/generate-drizzle-schema.js +1 -1
  70. package/dist/schema/generate-next-step.d.ts +20 -8
  71. package/dist/schema/generate-postgres-ddl.js +2 -2
  72. package/dist/schema/introspect-db.js +5 -4
  73. package/dist/schema/introspect-db.js.map +1 -1
  74. package/dist/schema/push-policy-plan.d.ts +5 -2
  75. package/dist/services/BranchService.d.ts +67 -0
  76. package/dist/{source-DB6xJv3p.js → source-Bv5KPeIx.js} +45 -2
  77. package/dist/source-Bv5KPeIx.js.map +1 -0
  78. package/dist/{websocket-DMeXOoyv.js → websocket-MC0zTBar.js} +3 -3
  79. package/dist/{websocket-DMeXOoyv.js.map → websocket-MC0zTBar.js.map} +1 -1
  80. package/package.json +7 -7
  81. package/dist/BranchService-DNg4txyf.js.map +0 -1
  82. package/dist/backup-cli-HCEkPcQj.js.map +0 -1
  83. package/dist/backup-service-D-UpT7DH.js.map +0 -1
  84. package/dist/cli-console-C_ISGwJo.js.map +0 -1
  85. package/dist/doctor-CJyuhV1n.js.map +0 -1
  86. package/dist/ensure-collection-policies-C1ux3V_z.js.map +0 -1
  87. package/dist/pg-error-utils-CdsGYMTm.js.map +0 -1
  88. package/dist/push-policy-plan-DTXE6aVa.js.map +0 -1
  89. package/dist/source-DB6xJv3p.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgres",
3
- "version": "0.25.0-canary.ge06257e",
3
+ "version": "0.25.0",
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/common": "0.25.0-canary.ge06257e",
54
- "@rebasepro/server": "0.25.0-canary.ge06257e",
55
- "@rebasepro/utils": "0.25.0-canary.ge06257e",
56
- "@rebasepro/types": "0.25.0-canary.ge06257e",
57
- "@rebasepro/codegen": "0.25.0-canary.ge06257e"
53
+ "@rebasepro/codegen": "0.25.0",
54
+ "@rebasepro/server": "0.25.0",
55
+ "@rebasepro/types": "0.25.0",
56
+ "@rebasepro/utils": "0.25.0",
57
+ "@rebasepro/common": "0.25.0"
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.11",
73
- "@rebasepro/client": "0.25.0-canary.ge06257e"
73
+ "@rebasepro/client": "0.25.0"
74
74
  },
75
75
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
76
76
  "publishConfig": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"BranchService-DNg4txyf.js","names":[],"sources":["../src/services/BranchService.ts"],"sourcesContent":["/**\n * BranchService\n *\n * Manages database branching by creating/deleting PostgreSQL databases\n * using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the\n * `rebase.branches` table in the default (main) database, following the\n * same `rebase` schema convention used by entity_history, auth, etc.\n */\n\nimport { sql } from \"drizzle-orm\";\nimport { BranchInfo } from \"@rebasepro/types\";\nimport { revokeInternalTableSql } from \"@rebasepro/common\";\nimport { DrizzleClient } from \"../interfaces\";\nimport { BranchRow } from \"../branch-prune\";\nimport { DatabasePoolManager } from \"../databasePoolManager\";\nimport { extractPgError, extractCauseMessage } from \"../utils/pg-error-utils\";\n\n/** Internal prefix applied to branch database names to avoid collisions. */\nconst BRANCH_DB_PREFIX = \"rb_\";\n\n/** `duplicate_database` — the target database name is already taken. */\nconst PG_DUPLICATE_DATABASE = \"42P04\";\n\n/** `object_in_use` — the database still has connections attached. */\nconst PG_OBJECT_IN_USE = \"55006\";\n\n/**\n * Describe a failed branch DDL statement in terms a user can act on.\n *\n * Drizzle reports failures as `Failed query: <sql> params:` and hides the real\n * PostgreSQL error in the `cause` chain, so matching on `err.message` never sees\n * the actual problem. Match on the PG error code instead — it survives wrapping\n * and, unlike the message text, is not locale-dependent.\n */\nfunction describeBranchDdlError(err: unknown, fallbackContext: string): Error {\n const pgError = extractPgError(err);\n\n if (pgError?.code === PG_DUPLICATE_DATABASE) {\n return new Error(`Database \"${fallbackContext}\" already exists on the server. Choose a different branch name.`);\n }\n if (pgError?.code === PG_OBJECT_IN_USE) {\n return new Error(\n `Cannot complete the operation: the database \"${fallbackContext}\" has active connections. ` +\n \"Close other clients or connections and try again.\"\n );\n }\n\n // Unknown failure: surface the real PG message rather than the Drizzle\n // wrapper, which would otherwise show the raw SQL and no reason at all.\n const detail = pgError?.message ?? extractCauseMessage(err);\n if (detail) return new Error(detail);\n return err instanceof Error ? err : new Error(String(err));\n}\n\n/** Fully-qualified metadata table in the rebase schema. */\nconst BRANCHES_TABLE = \"rebase.branches\";\n\n/**\n * The connected server cannot branch, so nothing was done.\n *\n * A refusal, not a fault: typed so the transports can forward it intact.\n * `websocket.ts` answers it with its own code and message rather than letting\n * it fall into the generic handler, which suppresses the text in production —\n * and the text is the only part that says what to do instead.\n */\nexport class BranchingUnsupportedError extends Error {\n /** Wire code, forwarded verbatim to the client. */\n readonly code = \"BRANCHING_UNSUPPORTED\";\n\n constructor(message: string) {\n super(message);\n this.name = \"BranchingUnsupportedError\";\n }\n}\n\n/**\n * What to say when the server serves one database.\n *\n * Deliberately the same argument the CLI's `refuseBranchOnManagedDatabase`\n * makes, because it is the same refusal — it just used to be available only to\n * whoever came in through argv. One paragraph, because this one has to fit in a\n * toast as well as a terminal.\n */\nconst BRANCHING_UNSUPPORTED_MESSAGE =\n \"Branching does not work on the managed development database. It copies a database with \"\n + \"CREATE DATABASE ... TEMPLATE, and the managed database is PGlite, which serves exactly one — \"\n + \"the copy would be the original, and every write you meant to sandbox would land in it. \"\n + \"Branching needs a real Postgres: run `rebase dev --docker`, or uncomment DATABASE_URL in .env \"\n + \"to point at your own.\";\n\n/**\n * Validate that a user-provided identifier only contains safe characters.\n * Throws if the value contains characters outside [a-zA-Z0-9_-].\n */\nfunction validateIdentifier(value: string, label: string): void {\n if (!/^[a-zA-Z0-9_-]+$/.test(value)) {\n throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);\n }\n}\n\n/**\n * Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it\n * silently. The prefix comes out of the same budget.\n */\nconst MAX_BRANCH_NAME_LENGTH = 63 - BRANCH_DB_PREFIX.length;\n\n/**\n * Check a user-provided branch name, and otherwise leave it exactly as given.\n *\n * This used to strip everything outside [a-zA-Z0-9_], so `my-feature` was\n * quietly created as `myfeature`: the name you typed was not the name `list`\n * gave back. Nothing ever needed that. Every identifier this service builds is\n * double-quoted (see `CREATE DATABASE` below), which is what makes a hyphen\n * safe, and `validateIdentifier` has always accepted hyphens for the `--from`\n * source database — the two disagreed about the same character class.\n *\n * Refusing a name we cannot represent is better than representing a different\n * one, which is also why the length is checked here rather than left to\n * Postgres, whose answer to an over-long identifier is a silent rename.\n */\nfunction assertValidBranchName(name: string): void {\n validateIdentifier(name, \"branch name\");\n if (name.length > MAX_BRANCH_NAME_LENGTH) {\n throw new Error(\n `Branch name \"${name}\" is too long: ${name.length} characters, maximum ${MAX_BRANCH_NAME_LENGTH}. ` +\n \"Postgres truncates identifiers past 63 bytes, which would give the branch a name you did not choose.\"\n );\n }\n}\n\n/**\n * Convert a user-facing branch name to the actual PostgreSQL database name.\n */\nfunction toBranchDbName(name: string): string {\n assertValidBranchName(name);\n return `${BRANCH_DB_PREFIX}${name}`;\n}\n\n/**\n * Where a server's branches are kept: the main database — what a branch is\n * copied from unless told otherwise, and what may never be dropped — and the\n * connection whose `rebase.branches` records them.\n */\nexport interface BranchHome {\n /** The main database's name. */\n database(): Promise<string>;\n /**\n * A connection on the database the registry is in. Asked for at each use:\n * a pool to the main database is closed before it is copied.\n */\n registry(): Promise<DrizzleClient>;\n}\n\nexport class BranchService {\n private readonly home: BranchHome;\n\n /**\n * @param db A connection on the server the branches are made on. Branch\n * DDL runs here.\n * @param home Where branches are kept. Without one, the database the\n * pool manager's connection string names, and `db`'s registry.\n */\n constructor(\n private db: DrizzleClient,\n private poolManager: DatabasePoolManager,\n home?: BranchHome\n ) {\n this.home = home ?? {\n database: async () => poolManager.defaultDatabaseName,\n registry: async () => db\n };\n }\n\n /**\n * Refuse a branch mutation the connected server cannot honour.\n *\n * The check lives here rather than in the CLI's argv parsing because the\n * CLI is not the only caller: Studio reaches `createBranch` over the\n * websocket, and before this it got \"Branch created successfully\" and a\n * detail pane promising isolation for a database that is the parent.\n *\n * Reads only, and only mutations are guarded: `listBranches` on a managed\n * database is an honest empty list, and refusing it would turn the Branches\n * pane into a failure with a \"Try again\" that can never succeed.\n */\n private async assertBranchingSupported(): Promise<void> {\n if (await this.poolManager.servesOneDatabase()) {\n throw new BranchingUnsupportedError(BRANCHING_UNSUPPORTED_MESSAGE);\n }\n }\n\n /**\n * Whether the `rebase.branches` registry exists. A database that has never\n * had a branch made on it may not have it, and reading it there is an\n * empty answer, not an error — nor a reason to create it. The CLI's `list`\n * and `info` ask this rather than running the DDL that creates it.\n */\n async hasBranchRegistry(): Promise<boolean> {\n const result = await (await this.home.registry()).execute(\n sql`SELECT to_regclass('rebase.branches') IS NOT NULL AS present`\n );\n const [row] = result.rows;\n return row !== undefined && Reflect.get(row, \"present\") === true;\n }\n\n /**\n * Ensure the `rebase.branches` metadata table exists in the default database.\n * Idempotent — safe to call on every startup.\n */\n async ensureBranchMetadataTable(): Promise<void> {\n // Create the rebase schema (idempotent — may already exist from auth/history init)\n const registry = await this.home.registry();\n await registry.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n await registry.execute(sql.raw(`\n CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (\n name TEXT PRIMARY KEY,\n db_name TEXT NOT NULL UNIQUE,\n parent_db TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n metadata JSONB DEFAULT '{}'\n );\n `));\n\n // Not a collection, so no RLS — and it names every branch database on\n // this server. The driver's schema-wide grant reaches it, so revoke.\n await registry.execute(sql.raw(revokeInternalTableSql(\"rebase\", \"branches\")));\n }\n\n /**\n * Create a new branch database by templating the source database.\n *\n * Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy\n * of both schema and data.\n *\n * @param name User-facing branch name (e.g., \"feature_auth\")\n * @param options.source Source database to clone; defaults to the main database.\n */\n async createBranch(\n name: string,\n options?: { source?: string; force?: boolean }\n ): Promise<BranchInfo> {\n await this.assertBranchingSupported();\n\n if (options?.source) {\n validateIdentifier(options.source, \"source database name\");\n }\n\n const dbName = toBranchDbName(name);\n const sourceDb = options?.source || await this.home.database();\n\n // Check if branch already exists\n const existing = await (await this.home.registry()).execute(\n sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`\n );\n if ((existing.rows as unknown[]).length > 0) {\n throw new Error(`Branch \"${name}\" already exists.`);\n }\n\n // Disconnect any idle pools to the source DB so TEMPLATE works.\n // CREATE DATABASE ... TEMPLATE requires no other connections to the template.\n //\n // This only reaches pools inside *this* process. The CLI runs as its own\n // process, so a `rebase dev` holding the database open is untouched by\n // it — which is why the failure below has to name what is connected, and\n // why `--force` exists.\n await this.poolManager.disconnectDatabase(sourceDb);\n\n if (options?.force) {\n const closed = await this.terminateConnections(sourceDb);\n if (closed > 0) {\n // Postgres reports the backend as gone before its slot is\n // actually free, and CREATE DATABASE then fails on a connection\n // that no longer exists. One short settle beats a confusing retry.\n await new Promise((resolve) => setTimeout(resolve, 250));\n }\n }\n\n // Create the database using the source as a template.\n // Note: Identifiers must be double-quoted, not parameterized.\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n const safeSourceDb = sourceDb.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(\n sql.raw(`CREATE DATABASE \"${safeDbName}\" TEMPLATE \"${safeSourceDb}\"`)\n );\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n // The template — not the new database — is the one still in use.\n throw new Error(\n `Cannot create branch: the source database \"${sourceDb}\" has active connections.\\n`\n + await this.describeBlockingConnections(sourceDb)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Record metadata in the registry — asked for again, as copying the\n // main database closed any pool on it.\n const now = new Date();\n await (await this.home.registry()).execute(\n sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)\n VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`\n );\n\n return {\n name,\n database: dbName,\n parentDatabase: sourceDb,\n createdAt: now\n };\n }\n\n /**\n * Delete a branch database and remove its metadata.\n * Cannot delete the main/default database.\n */\n async deleteBranch(name: string, options?: { force?: boolean }): Promise<void> {\n await this.assertBranchingSupported();\n\n assertValidBranchName(name);\n\n // Safety, first pass: a request that would target the default database\n // is refused before any metadata is read, so the answer costs nothing\n // and cannot depend on what a row happens to say.\n const mainDatabase = await this.home.database();\n if (toBranchDbName(name) === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Verify the branch exists, and take the database name from the row\n // rather than deriving it again. A branch created before names stopped\n // being stripped is recorded as `rb_myfeature` while `my-feature` now\n // derives `rb_my-feature`; re-deriving would drop a database this row\n // never named — or, if that name happened to exist, somebody else's.\n // The stored value is the only one that is true by construction.\n const existing = await (await this.home.registry()).execute(\n sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`\n );\n const existingRows = existing.rows as Record<string, unknown>[];\n if (existingRows.length === 0) {\n throw new Error(`Branch \"${name}\" not found.`);\n }\n const dbName = existingRows[0].db_name as string;\n\n // Safety, second pass: the first checked a name we derived, this checks\n // the one we are about to drop. They differ for any row written under\n // the old scheme, and it is this one that governs.\n if (dbName === mainDatabase) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n // Disconnect any pools to this branch before dropping. Same limit as\n // `createBranch`: in-process pools only.\n await this.poolManager.disconnectDatabase(dbName);\n\n if (options?.force) {\n const closed = await this.terminateConnections(dbName);\n if (closed > 0) await new Promise((resolve) => setTimeout(resolve, 250));\n }\n\n // Drop the database\n const safeDbName = dbName.replace(/\"/g, '\"\"');\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${safeDbName}\"`));\n } catch (err) {\n const pgError = extractPgError(err);\n if (pgError?.code === PG_OBJECT_IN_USE) {\n throw new Error(\n `Cannot delete branch \"${name}\": the database has active connections.\\n`\n + await this.describeBlockingConnections(dbName)\n );\n }\n throw describeBranchDdlError(err, dbName);\n }\n\n // Remove metadata\n await (await this.home.registry()).execute(\n sql`DELETE FROM rebase.branches WHERE name = ${name}`\n );\n }\n\n /**\n * Who is holding the database open, in words a developer can act on.\n *\n * `CREATE DATABASE ... TEMPLATE` and `DROP DATABASE` both require that no\n * other session is connected, and the one thing the old message could not\n * say is the only thing worth knowing: *which* session. \"Close other\n * clients or connections and try again\" is advice you cannot follow when\n * you do not know there is a `rebase dev` in another terminal — and that is\n * the common case, because wanting a branch and running the app are the\n * same moment.\n *\n * `application_name` is the useful column: node-postgres sends none by\n * default, so a Rebase process shows as `(unnamed)` while DBeaver, pgAdmin\n * and psql all name themselves. Counting by it separates \"my own dev\n * server\" from \"the GUI I forgot about\" without any guessing.\n *\n * Best-effort by construction: this runs while reporting a failure, and a\n * diagnostic that throws would replace a real error with its own.\n */\n private async describeBlockingConnections(dbName: string): Promise<string> {\n try {\n const result = await this.db.execute(sql`\n SELECT coalesce(nullif(application_name, ''), '(unnamed)') AS app,\n count(*)::int AS n\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n GROUP BY 1\n ORDER BY 2 DESC, 1\n `);\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) {\n // The blocker went away between the failure and this query, or\n // it is a session Postgres does not attribute to this database.\n return \" Retry — whatever was connected has since disconnected.\";\n }\n\n const listed = rows\n .map((row) => ` ${row.n} × ${String(row.app)}`)\n .join(\"\\n\");\n\n return ` Connected right now:\\n${listed}\\n`\n + \" A running `rebase dev` is the usual one — stop it, or re-run with --force to\\n\"\n + \" disconnect them for you.\";\n } catch {\n return \" Close other clients and connections, or re-run with --force.\";\n }\n }\n\n /**\n * Disconnect every other session on a database, and say how many.\n *\n * `pg_terminate_backend` rather than `pg_cancel_backend`: cancelling a\n * query leaves the session connected, and a connected session is exactly\n * what blocks the operation.\n *\n * The current backend is excluded — terminating the connection running this\n * statement would abort the command that asked for it.\n */\n private async terminateConnections(dbName: string): Promise<number> {\n const result = await this.db.execute(sql`\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = ${dbName}\n AND pid <> pg_backend_pid()\n `);\n\n return (result.rows as unknown[]).length;\n }\n\n /**\n * Everything prune needs to decide, in two queries.\n *\n * Reads the rows and the server's database list rather than joining them,\n * so the *disagreements* between the two are visible — a row whose database\n * is gone and a database with no row are the two things prune exists to\n * find, and a join hides both.\n */\n async pruneCandidates(): Promise<{ rows: BranchRow[]; databases: string[] }> {\n const rowResult = await (await this.home.registry()).execute(sql.raw(`\n SELECT name, db_name, created_at FROM ${BRANCHES_TABLE} ORDER BY created_at\n `));\n const rows = (rowResult.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n dbName: row.db_name as string,\n createdAt: new Date(row.created_at as string)\n }));\n\n const dbResult = await this.db.execute(sql`\n SELECT datname FROM pg_database WHERE NOT datistemplate\n `);\n const databases = (dbResult.rows as Record<string, unknown>[]).map((row) => String(row.datname));\n\n return { rows, databases };\n }\n\n /** Remove a metadata row whose database is already gone. */\n async forgetBranchRow(name: string): Promise<void> {\n await (await this.home.registry()).execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);\n }\n\n /**\n * Drop a database by name, with no metadata row required.\n *\n * `deleteBranch` deliberately takes its database name from the row, because\n * the row is the only value that is true by construction. Prune has the\n * opposite job — the cases it handles are exactly the ones where row and\n * database disagree — so it needs to name a database directly. Guarded the\n * same way regardless: the main database is never droppable.\n */\n async dropDatabase(dbName: string): Promise<void> {\n validateIdentifier(dbName, \"database name\");\n if (dbName === await this.home.database()) {\n throw new Error(\"Cannot delete the main database.\");\n }\n\n await this.poolManager.disconnectDatabase(dbName);\n try {\n await this.db.execute(sql.raw(`DROP DATABASE \"${dbName.replace(/\"/g, '\"\"')}\"`));\n } catch (err) {\n throw describeBranchDdlError(err, dbName);\n }\n }\n\n /**\n * List all branches recorded in the metadata table.\n * Optionally fetches database sizes from pg_database.\n */\n async listBranches(): Promise<BranchInfo[]> {\n const result = await (await this.home.registry()).execute(sql.raw(`\n SELECT \n b.name,\n b.db_name,\n b.parent_db,\n b.created_at,\n pg_database_size(b.db_name) as size_bytes\n FROM ${BRANCHES_TABLE} b\n JOIN pg_database d ON d.datname = b.db_name\n ORDER BY b.created_at DESC\n `));\n\n return (result.rows as Record<string, unknown>[]).map((row) => ({\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined\n }));\n }\n\n /**\n * Get info about a specific branch.\n */\n async getBranchInfo(name: string): Promise<BranchInfo | undefined> {\n assertValidBranchName(name);\n\n const result = await (await this.home.registry()).execute(sql`\n SELECT\n b.name,\n b.db_name,\n b.parent_db,\n b.created_at\n FROM rebase.branches b\n WHERE b.name = ${name}\n `);\n\n const rows = result.rows as Record<string, unknown>[];\n if (rows.length === 0) return undefined;\n\n const row = rows[0];\n\n // Attempt to get size — may fail if the DB was externally dropped.\n // Same reason as `deleteBranch`: the size of a re-derived name is the\n // size of some other database, or of nothing.\n let sizeBytes: number | undefined;\n try {\n const dbName = row.db_name as string;\n const sizeResult = await this.db.execute(\n sql`SELECT pg_database_size(${dbName}) as size_bytes`\n );\n const sizeRows = sizeResult.rows as Record<string, unknown>[];\n if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {\n sizeBytes = Number(sizeRows[0].size_bytes);\n }\n } catch {\n // Database might not exist anymore\n }\n\n return {\n name: row.name as string,\n database: row.db_name as string,\n parentDatabase: row.parent_db as string,\n createdAt: new Date(row.created_at as string),\n sizeBytes\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"backup-cli-HCEkPcQj.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 type arg from \"arg\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport os from \"os\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\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 { closestWord, parseCommandFlags } from \"../cli-flags\";\nimport { redactConnectionPasswords } from \"../utils/connection-string\";\nimport { promptConfirm } from \"../cli-prompt\";\nimport { checkDatabaseConnectivity } from \"../cli-errors\";\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 findRestoreConflicts,\n listBackups,\n preflight,\n readDumpObjects,\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/** A web stream's chunks, for piping into a file. */\nasync function* webChunks(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {\n const reader = stream.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) return;\n yield value;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * The controller for an object-storage destination, or `null` for a local one.\n *\n * Built by `backupStorageFromEnv` from `@rebasepro/server` — the function the\n * Backups panel's routes use — so the CLI and the panel read a bucket the same\n * way. This file had its own copy, and the two had already drifted once\n * (`S3_FORCE_PATH_STYLE` read as `=== \"true\"`, so a MinIO backup addressed the\n * bucket as a host).\n *\n * Exported for its test.\n */\nexport async function resolveStorageForDestination(\n dest: BackupDestination,\n env: Record<string, string | undefined>\n): Promise<StorageController | null> {\n if (dest.kind === \"local\") return null;\n const { backupStorageFromEnv } = await import(\"@rebasepro/server\");\n try {\n return backupStorageFromEnv(dest, env);\n } catch (error) {\n throw new BackupToolError(\n error instanceof Error ? error.message : String(error),\n \"Set the same S3_* variables your backend uses for storage.\"\n );\n }\n}\n\n/**\n * The directory `rebase` was run from, which `rebase` puts here for the driver.\n *\n * The driver runs in the project's `backend/`, so a relative path resolved\n * against its own working directory named a different place from the one the\n * user typed it in: `rebase db backup --out ./backups` wrote to `<root>/backups`\n * (the CLI resolves explicit paths itself) while `rebase db backups list`\n * looked in `<root>/backend/backups`. Spelled the same in\n * `packages/cli/src/commands/db.ts`.\n */\nexport const INVOCATION_CWD_ENV = \"REBASE_INVOCATION_CWD\";\n\n/** Where a relative path the user typed is relative to: where they ran `rebase`. */\nexport function invocationDir(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const dir = env[INVOCATION_CWD_ENV]?.trim();\n return dir && path.isAbsolute(dir) ? dir : process.cwd();\n}\n\n/**\n * Where `db backup` writes and `db backups list` reads, by one rule.\n *\n * - `--out` is relative to where `rebase` was run, like any path typed on a\n * command line.\n * - `BACKUP_DESTINATION` is relative to the backend directory, because the\n * server reads the same variable there: the Backups panel and the scheduled\n * backup have to find what the CLI wrote.\n * - With neither, `./backups` where `rebase` was run, as the help says.\n *\n * An `s3://` or `gs://` destination is returned as given.\n */\nexport function resolveBackupLocation(opts: {\n flag?: string;\n configured?: string;\n invocationDir: string;\n backendDir: string;\n}): string {\n const flag = opts.flag?.trim();\n if (flag) return isRemoteDestination(flag) ? flag : path.resolve(opts.invocationDir, flag);\n const configured = opts.configured?.trim();\n if (configured) return isRemoteDestination(configured) ? configured : path.resolve(opts.backendDir, configured);\n return path.join(opts.invocationDir, \"backups\");\n}\n\nfunction isRemoteDestination(value: string): boolean {\n return parseBackupDestination(value).kind !== \"local\";\n}\n\n/** {@link resolveBackupLocation} for this process. */\nfunction backupLocation(flag: string | undefined): string {\n return resolveBackupLocation({\n flag,\n configured: process.env.BACKUP_DESTINATION,\n invocationDir: invocationDir(),\n backendDir: process.cwd()\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\n// ─────────────────────────────────────────────────────────────────────────\n// rebase db backup\n// ─────────────────────────────────────────────────────────────────────────\nconst BACKUP_FLAGS = {\n \"--out\": String,\n // The same alias `build` and `cloud env pull` carry. `--output` is the\n // canonical spelling in this CLI, and `rebase db backup --output ./backups`\n // has to write there.\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} satisfies arg.Spec;\n\n/**\n * Parse a `rebase db backup …` line. An undeclared flag is refused (see\n * `parseCommandFlags`), and so is a positional: `rebase db backup ./backups`\n * means `--out ./backups`, and taken as nothing it wrote to the default\n * directory.\n */\nexport function parseBackupLine(rawArgs: string[]) {\n const args = parseCommandFlags(BACKUP_FLAGS, rawArgs.slice(2), \"db backup\");\n const [stray] = args._;\n if (stray !== undefined) {\n throw new Error(\n `\\`rebase db backup\\` takes no argument \"${stray}\". Nothing was run. ` +\n `To choose where the backup goes, pass --out ${stray}.`\n );\n }\n return args;\n}\n\nexport async function backupCommand(rawArgs: string[]): Promise<void> {\n if (rawArgs.includes(\"--help\")) {\n printBackupHelp();\n return;\n }\n const args = parseBackupLine(rawArgs);\n\n const connectionString = requireConnection();\n const dbName = parseDbNameFromUrl(connectionString) ?? \"database\";\n const out = backupLocation(args[\"--out\"]);\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 // A database that is not running is said the way `db push` says it — the\n // host, the port and what to start — before pg_dump's version probe meets\n // it as a bare `connect ECONNREFUSED`.\n await checkDatabaseConnectivity(connectionString);\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// ─────────────────────────────────────────────────────────────────────────\nconst RESTORE_FLAGS = {\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} satisfies arg.Spec;\n\n/**\n * Parse a `rebase db restore …` line; the one positional is the backup.\n *\n * Strict, because this is the command where a typo costs most: parsed\n * permissively, `--targt-db x` became a second positional, the target fell\n * back to DATABASE_URL's own database, and the restore ran against the live\n * one.\n */\nexport function parseRestoreLine(rawArgs: string[]) {\n const args = parseCommandFlags(RESTORE_FLAGS, rawArgs.slice(2), \"db restore\");\n if (args._.length > 1) {\n throw new Error(\n `\\`rebase db restore\\` restores one backup, and was given ${args._.length}: ` +\n `${args._.map(word => `\"${word}\"`).join(\", \")}. Nothing was run.`\n );\n }\n return args;\n}\n\n/**\n * The restore's one question, asked before anything can change.\n *\n * `--yes` skips it. On a terminal it asks for a typed 'yes'. With no terminal\n * nobody can answer, and the refusal says to pass `--yes`, as `db reset`,\n * `db pull` and `db branch prune` do: \"Aborted. No changes were made.\" alone\n * read as though someone had answered no.\n *\n * @returns whether to go on. When not, it has said why.\n */\nexport async function confirmRestore(opts: {\n target: string;\n yes: boolean;\n interactive: boolean;\n ask: (question: string) => Promise<boolean>;\n}): Promise<boolean> {\n if (opts.yes) return true;\n outWarn(chalk.yellow(` ⚠️ This will restore into \"${opts.target}\" and may overwrite existing data.`));\n if (!opts.interactive) {\n outError(chalk.red(\" ✗ Nothing was restored: there is no terminal to confirm on.\"));\n outError(chalk.gray(` Re-run with --yes to restore into \"${opts.target}\".`));\n return false;\n }\n const confirmed = await opts.ask(chalk.yellow(\" Type 'yes' to continue: \"));\n if (!confirmed) print(chalk.gray(\" Aborted. No changes were made.\"));\n return confirmed;\n}\n\n/** How many of the conflicting objects a refusal names before counting the rest. */\nconst CONFLICTS_NAMED = 5;\n\n/**\n * What `db restore` says when the target already holds what the backup\n * creates, and the two ways round it: `--clean` replaces them with the\n * backup's copy, a new database restores beside them.\n */\nexport function describeRestoreConflict(\n target: string,\n backupArg: string,\n conflicts: readonly string[]\n): { message: string; hint: string } {\n const named = conflicts.slice(0, CONFLICTS_NAMED).join(\", \");\n const more = conflicts.length > CONFLICTS_NAMED ? `, and ${conflicts.length - CONFLICTS_NAMED} more` : \"\";\n return {\n message: `\"${target}\" already holds ${named}${more}, which this backup creates. ` +\n \"Without --clean, pg_restore stops at the first of them. Nothing was restored or changed.\",\n hint: [\n `To replace them with the backup's copy: rebase db restore ${backupArg} --clean`,\n `To restore beside them, in a new database: rebase db restore ${backupArg} --create-db --target-db <new name>`\n ].join(\"\\n \")\n };\n}\n\nexport async function restoreCommand(rawArgs: string[]): Promise<void> {\n if (rawArgs.includes(\"--help\")) {\n printRestoreHelp();\n return;\n }\n const args = parseRestoreLine(rawArgs);\n\n const backupArg = args._[0];\n if (!backupArg) {\n printRestoreHelp();\n process.exit(1);\n }\n\n const baseConnection = requireConnection();\n\n // Choose the target connection: an explicit --target-db swaps the database\n // name so the live one isn't clobbered. Without it the target is\n // DATABASE_URL's own database, and `--create-db` only creates it when it is\n // missing.\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 // Before anything is downloaded, and in `db push`'s words: see backupCommand.\n await checkDatabaseConnectivity(baseConnection);\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 tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-restore-\"));\n localFile = path.join(tmpDir, path.basename(key));\n cleanupTemp = true;\n // Streamed to disk: buffered, a dump was held twice in memory and\n // one over 2 GiB could not be written in one call at all.\n const object = storage.getObjectStream\n ? await storage.getObjectStream(key, { bucket })\n : await storage.getObject(key, bucket).then((file) => file && { stream: file.stream() });\n if (!object) throw new BackupToolError(`Backup not found in storage: ${backupArg}`);\n await pipeline(Readable.from(webChunks(object.stream)), fs.createWriteStream(localFile));\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(invocationDir(), backupArg);\n if (!fs.existsSync(localFile)) throw new BackupToolError(`Backup file not found: ${localFile}`);\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 // Thrown, not exited, from here down: an exit skipped the `finally`\n // that removes a dump downloaded from object storage.\n const pf = await preflight(\"pg_restore\", baseConnection);\n if (!pf.compatible) throw new BackupToolError(pf.reason ?? \"pg_restore cannot restore into this server.\");\n\n // `--create-db` needs a name before anything else can be decided.\n if (args[\"--create-db\"] && !targetDb) {\n throw new BackupToolError(\"--create-db requires a resolvable target database name (use --target-db).\");\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 // Before the question and before anything is applied, roles included:\n // without --clean, pg_restore stops at the first object the target\n // already has, and said so in its own words and execa's, after the\n // roles had run. A target that does not exist yet holds nothing.\n if (!args[\"--clean\"]) {\n const conflicts = await findRestoreConflicts(targetConnection, await readDumpObjects(localFile));\n if (conflicts.length > 0) {\n const { message, hint } = describeRestoreConflict(targetDb ?? \"the target database\", backupArg, conflicts);\n throw new BackupToolError(message, hint);\n }\n }\n\n const confirmed = await confirmRestore({\n target: targetDb ?? \"the target database\",\n yes: args[\"--yes\"] === true,\n interactive: process.stdin.isTTY === true,\n ask: promptConfirm\n });\n if (!confirmed) {\n // An exit code, so the `finally` still removes a downloaded dump.\n process.exitCode = 1;\n return;\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 let rolesApplied = 0;\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 rolesApplied = applied;\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 try {\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 } catch (err) {\n if (err instanceof BackupToolError) throw err;\n // pg_restore has printed its own error above; execa's repeats the\n // command line, which says nothing the reader can act on.\n throw new BackupToolError(\n `pg_restore stopped on the error above. What it restored before that stays in \"${targetDb ?? \"the target database\"}\".`,\n rolesApplied > 0\n ? `The ${rolesApplied} cluster role(s) recreated from the backup's sidecar before it stay too.`\n : undefined\n );\n }\n\n print(\"\");\n print(chalk.green(` ✓ Restore completed into \"${targetDb ?? \"the target database\"}\".`));\n print(\"\");\n } catch (err) {\n reportError(err);\n // An exit code, not `process.exit`: exiting here skipped the `finally`\n // below, and a failed restore from object storage left its downloaded\n // dump, every row of the database, in the temp directory.\n process.exitCode = 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// ─────────────────────────────────────────────────────────────────────────\nconst BACKUPS_ACTIONS = [\"list\"] as const;\n\n/** What `db backups <action>` says of an action it does not have: the sentence every `db` command says. */\nexport function unknownBackupsAction(action: string): string {\n const near = closestWord(action, BACKUPS_ACTIONS);\n return near\n ? `Unknown db backups command \"${action}\" — did you mean \\`${near}\\`? Run \\`rebase db backups --help\\` for the rest.`\n : `Unknown db backups command \"${action}\". Run \\`rebase db backups --help\\` for the ones there are.`;\n}\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(`✗ ${unknownBackupsAction(action)}`));\n process.exit(1);\n }\n\n const args = parseCommandFlags(\n { \"--out\": String, \"--output\": \"--out\", \"-o\": \"--out\" },\n rawArgs.slice(3),\n \"db backups list\"\n );\n const out = backupLocation(args[\"--out\"]);\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 await mentionBackendBackups(out, args[\"--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 * Point at `<backend>/backups` when the default listing is empty and that\n * directory holds dumps.\n *\n * Earlier releases resolved the default there, so after an upgrade the\n * backups a project already has are one directory away from the listing.\n */\nasync function mentionBackendBackups(listed: string, flag: string | undefined): Promise<void> {\n if (flag || process.env.BACKUP_DESTINATION?.trim()) return;\n const backendBackups = path.join(process.cwd(), \"backups\");\n if (path.resolve(listed) === backendBackups) return;\n const found = await listBackups({ kind: \"local\", path: backendBackups }).catch(() => []);\n if (found.length === 0) return;\n print(chalk.gray(` ${found.length} backup(s) are in ${backendBackups}, where earlier releases of`));\n print(chalk.gray(` \\`rebase db backup\\` wrote by default. List them with:`));\n print(chalk.gray(` rebase db backups list --out ${path.relative(invocationDir(), backendBackups) || \".\"}`));\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\n/**\n * Say why a backup command failed, with no password in it.\n *\n * What reaches here is often a client tool's failure, and execa's message\n * repeats the tool's command line. The tools get the password through\n * `PGPASSWORD` now, so it is not on that line, but a connection string can\n * still arrive inside a message from elsewhere; this is the last place to\n * stop it reaching a terminal or a CI log.\n */\nfunction reportError(err: unknown): void {\n if (err instanceof BackupToolError) {\n outError(chalk.red(` ✗ ${redactConnectionPasswords(err.message)}`));\n if (err.hint) outError(chalk.gray(` ${redactConnectionPasswords(err.hint)}`));\n } else {\n outError(chalk.red(` ✗ ${redactConnectionPasswords(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 asks for a typed 'yes' on a terminal, and with no terminal to ask on it\n refuses. Prefer --create-db/--target-db to restore into a fresh database\n rather than overwriting a live one. Into a database that already holds\n what the backup creates, it needs --clean, and refuses before changing\n anything without it.\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":"backup-service-D-UpT7DH.js","names":[],"sources":["../src/backup/pg-tools.ts","../src/backup/retention.ts","../src/backup/pg-binary.ts","../src/backup/backup-logic.ts","../src/backup/backup-service.ts"],"sourcesContent":["/**\n * Pure helpers for the backup/restore commands.\n *\n * Everything in this file is side-effect free so it can be unit-tested\n * without a live Postgres server, matching the constraint that CI must\n * not require a database.\n */\n\nimport { forLibpq } from \"../utils/connection-string\";\n\n/**\n * A parsed backup destination. `--out` (and the scheduled-backup config)\n * accepts either a local filesystem path or an object-storage URL.\n */\nexport type BackupDestination =\n | { kind: \"local\"; path: string }\n | { kind: \"s3\"; bucket: string; prefix: string }\n | { kind: \"gcs\"; bucket: string; prefix: string };\n\n/**\n * Extract the database name from a Postgres connection string.\n * Returns `null` when the URL has no database path (e.g. bare host).\n */\nexport function parseDbNameFromUrl(connectionString: string): string | null {\n try {\n const parsed = new URL(connectionString);\n const name = parsed.pathname.replace(/^\\//, \"\").trim();\n return name.length > 0 ? name : null;\n } catch {\n // Fall back to a permissive regex for non-URL DSNs.\n const match = connectionString.match(/\\/([^/?]+)(\\?|$)/);\n return match && match[1] ? match[1] : null;\n }\n}\n\n/**\n * Swap the database name in a connection string, preserving credentials,\n * host, port and query params. Used by `--target-db` / `--create-db` so a\n * restore can target a fresh database instead of clobbering the live one.\n */\nexport function withDatabaseName(connectionString: string, dbName: string): string {\n try {\n const parsed = new URL(connectionString);\n parsed.pathname = `/${dbName}`;\n return parsed.toString();\n } catch {\n return connectionString.replace(/\\/([^/?]+)(\\?|$)/, `/${dbName}$2`);\n }\n}\n\n/**\n * Parse the major version out of `pg_dump --version` / `pg_restore --version`\n * output, e.g. `\"pg_dump (PostgreSQL) 16.2\"` → `16`. Handles the pre-10\n * `9.6.x` scheme (returns `9`) and beta strings like `\"17beta1\"`.\n */\nexport function parsePgToolMajor(versionOutput: string): number | null {\n const match = versionOutput.match(/(\\d+)(?:\\.(\\d+))?/);\n if (!match) return null;\n const major = Number(match[1]);\n if (!Number.isFinite(major)) return null;\n // Postgres 9.x used the first two numbers as the major (9.6, 9.4…).\n if (major === 9 && match[2] !== undefined) {\n return 9;\n }\n return major;\n}\n\n/**\n * Convert `SELECT current_setting('server_version_num')` (e.g. `160002`)\n * into a major version (`16`). Also accepts pre-10 encodings like `90603`\n * → `9`.\n */\nexport function serverVersionNumToMajor(versionNum: number | string): number | null {\n const num = typeof versionNum === \"string\" ? Number(versionNum) : versionNum;\n if (!Number.isFinite(num) || num <= 0) return null;\n if (num < 100000) {\n // 9.x scheme: 90603 = 9.6.3\n return Math.floor(num / 10000);\n }\n return Math.floor(num / 10000);\n}\n\nexport interface VersionCompatibility {\n compatible: boolean;\n reason?: string;\n}\n\n/**\n * pg_dump / pg_restore must be **at least** as new as the server they talk\n * to. A newer client against an older server is supported; an older client\n * against a newer server is not and produces corrupt or rejected output.\n */\nexport function checkToolServerCompatibility(\n toolMajor: number | null,\n serverMajor: number | null\n): VersionCompatibility {\n if (toolMajor === null) {\n return { compatible: false, reason: \"Could not determine the client tool version.\" };\n }\n if (serverMajor === null) {\n return { compatible: false, reason: \"Could not determine the Postgres server version.\" };\n }\n if (toolMajor < serverMajor) {\n return {\n compatible: false,\n reason:\n `Client tool is Postgres ${toolMajor} but the server is Postgres ${serverMajor}. ` +\n `pg_dump/pg_restore must be the same major version as the server or newer. ` +\n `Install Postgres ${serverMajor} client tools.`\n };\n }\n return { compatible: true };\n}\n\n/**\n * Build a deterministic, sortable backup file name:\n * `rebase-<db>-<YYYYMMDD>T<HHMMSS>Z.dump`\n *\n * The UTC timestamp is embedded so retention pruning can recover the\n * creation time from the object key alone, without extra metadata.\n */\nexport function buildBackupFilename(dbName: string, date: Date = new Date()): string {\n const iso = date.toISOString(); // 2026-07-14T09:12:03.123Z\n const stamp = iso.replace(/\\.\\d+Z$/, \"Z\").replace(/[-:]/g, \"\");\n const safeDb = dbName.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return `rebase-${safeDb}-${stamp}.dump`;\n}\n\n/**\n * Recover the creation timestamp encoded in a backup file name by\n * {@link buildBackupFilename}. Returns `null` for names that don't match,\n * so foreign objects in a shared prefix are never pruned.\n */\nexport function parseBackupTimestamp(fileName: string): Date | null {\n const base = fileName.split(\"/\").pop() ?? fileName;\n const match = base.match(/-(\\d{8})T(\\d{6})Z\\.dump$/);\n if (!match) return null;\n const [, ymd, hms] = match;\n const iso =\n `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` +\n `T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;\n const date = new Date(iso);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Parse a destination string into a structured {@link BackupDestination}.\n * `s3://bucket/prefix` and `gs://bucket/prefix` map to object storage;\n * anything else is treated as a local path.\n */\nexport function parseBackupDestination(out: string): BackupDestination {\n const s3 = out.match(/^s3:\\/\\/([^/]+)\\/?(.*)$/);\n if (s3) {\n return { kind: \"s3\", bucket: s3[1], prefix: stripTrailingSlash(s3[2]) };\n }\n const gcs = out.match(/^gs:\\/\\/([^/]+)\\/?(.*)$/);\n if (gcs) {\n return { kind: \"gcs\", bucket: gcs[1], prefix: stripTrailingSlash(gcs[2]) };\n }\n return { kind: \"local\", path: out };\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.replace(/\\/+$/, \"\");\n}\n\n/**\n * Join a storage prefix and a file name without producing a leading or\n * doubled slash.\n */\nexport function joinStorageKey(prefix: string, fileName: string): string {\n const clean = prefix.replace(/^\\/+|\\/+$/g, \"\");\n return clean.length > 0 ? `${clean}/${fileName}` : fileName;\n}\n\n/**\n * The identity `pg_dump` reads rows as, when row security is left on.\n *\n * Not optional, and that is the whole design. `pg_dump --enable-row-security`\n * on its own is the dangerous command in this file: it turns the \"query would\n * be affected by row-level security policy\" *error* into a dump that exits 0\n * and is silently missing every row the dumping role's policies exclude. A\n * backup that looks fine and restores most of your data is worse than one that\n * refused to run.\n *\n * So the flag is unreachable without a subject to evaluate the policies\n * against. Rebase's generated policies read `app.uid` and `app.user_roles`;\n * supplying an admin role satisfies the `admin_full_access` rule and the dump\n * sees everything that rule sees.\n */\nexport interface RowSecurityIdentity {\n /** Written to `app.uid`. Any non-empty value — it is only an audit trail. */\n uid: string;\n /** Written to `app.user_roles`. Must include a role the policies admit. */\n roles: string[];\n}\n\n/**\n * `PGOPTIONS` carrying an identity, for a libpq tool that has no other way to\n * set a GUC.\n *\n * A backslash escape rather than quoting, which is what libpq's `-c` parsing\n * takes: a space inside a value ends the option otherwise, so a role list is\n * comma-joined and never spaced.\n */\nexport function buildRowSecurityPgOptions(identity: RowSecurityIdentity): string {\n const escape = (value: string) => value.replace(/([\\\\ ])/g, \"\\\\$1\");\n return [\n `-c app.uid=${escape(identity.uid)}`,\n `-c app.user_id=${escape(identity.uid)}`,\n `-c app.user_roles=${escape(identity.roles.join(\",\"))}`\n ].join(\" \");\n}\n\n/**\n * Assemble the `pg_dump` argument vector. Uses the custom format (`-Fc`),\n * which is compressed and restorable selectively via `pg_restore`.\n */\nexport function buildPgDumpArgs(opts: {\n connectionString: string;\n outFile: string;\n /** Extra schemas/tables to exclude, e.g. Atlas revision tables. */\n excludeSchemas?: string[];\n /** Number of parallel jobs (directory format only; ignored for -Fc). */\n noOwner?: boolean;\n /**\n * Dump with row security on, as this identity. Omit — which is the default\n * — and `pg_dump` errors rather than skipping rows it cannot see.\n */\n rowSecurity?: RowSecurityIdentity;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--file=${opts.outFile}`];\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n if (opts.rowSecurity) {\n args.push(\"--enable-row-security\");\n }\n for (const schema of opts.excludeSchemas ?? []) {\n args.push(`--exclude-schema=${schema}`);\n }\n args.push(forLibpq(opts.connectionString));\n return args;\n}\n\n/**\n * Whether a `pg_dump` failure is the row-security one, and what to do about it.\n *\n * The error text names the table and nothing else, so the first read of it is\n * \"why would a backup be affected by RLS at all?\" — the answer being that the\n * dumping role is not the tables' owner and has no `BYPASSRLS`, which is the\n * normal state of the `postgres` user on Cloud SQL, RDS and every other managed\n * Postgres. Nothing about that is visible from the message.\n *\n * Returns `null` for any other failure, so the caller reports it unchanged.\n */\nexport function diagnoseRowSecurityDumpFailure(error: unknown): string | null {\n const text = [\n (error as { stderr?: unknown })?.stderr,\n (error as { message?: unknown })?.message\n ].map(part => (typeof part === \"string\" ? part : \"\")).join(\"\\n\");\n\n if (!/row-level security policy/i.test(text)) return null;\n\n const table = text.match(/for table \"([^\"]+)\"/)?.[1];\n\n return [\n `pg_dump cannot read ${table ? `\"${table}\"` : \"one of the tables\"} because row-level security applies to it.`,\n \"\",\n \" The dumping role is neither the table's owner nor `BYPASSRLS`, which is the normal\",\n \" state of the `postgres` user on Cloud SQL, RDS and other managed Postgres — there is\",\n \" no superuser to hand out.\",\n \"\",\n \" Two ways out:\",\n \"\",\n \" • Grant the dumping role BYPASSRLS, or make it the owner, and run this again. The\",\n \" dump then contains every row, which is what a backup should mean.\",\n \"\",\n \" • Re-run with --enable-row-security to dump as an admin subject instead. Rebase\",\n \" sets `app.uid`/`app.user_roles` so the generated `admin_full_access` policy\",\n \" admits the dump. Read the warning it prints: the result contains exactly the\",\n \" rows those policies admit, and any table whose policies do not include an\",\n \" admin rule comes out short — with no error.\",\n \"\",\n \" Do not reach for a bare `pg_dump --enable-row-security` by hand. Without the\",\n \" settings above it succeeds and silently omits rows.\"\n ].join(\"\\n\");\n}\n\n/**\n * Assemble the `pg_restore` argument vector for a custom-format dump.\n */\nexport function buildPgRestoreArgs(opts: {\n connectionString: string;\n inputFile: string;\n /** Drop objects before recreating them (destructive but idempotent). */\n clean?: boolean;\n /**\n * Abort on the first error instead of logging and continuing. Defaults\n * ON: a restore that silently skips failed GRANT/RLS statements (because\n * a role is missing) \"succeeds\" with RLS un-enforced — a security hole.\n * Fail loudly instead so the operator knows the restore is incomplete.\n */\n exitOnError?: boolean;\n noOwner?: boolean;\n}): string[] {\n const args = [\"--format=custom\", \"--no-password\", `--dbname=${forLibpq(opts.connectionString)}`];\n if (opts.clean) {\n args.push(\"--clean\", \"--if-exists\");\n }\n if (opts.noOwner) {\n args.push(\"--no-owner\");\n }\n // Default to --exit-on-error unless explicitly disabled.\n if (opts.exitOnError !== false) {\n args.push(\"--exit-on-error\");\n }\n args.push(opts.inputFile);\n return args;\n}\n\n/**\n * Assemble the `pg_restore --list` argument vector. Reading a dump's table\n * of contents parses the whole archive without touching a database, so it is\n * a cheap integrity check that the file isn't truncated or corrupt.\n */\nexport function buildPgRestoreListArgs(inputFile: string): string[] {\n return [\"--list\", inputFile];\n}\n\n/** The schemas and tables a dump creates, read off its table of contents. */\nexport interface DumpObjects {\n schemas: string[];\n tables: { schema: string; table: string }[];\n}\n\n/** `<id>; <catalog oid> <oid> <description> <namespace> <tag…> <owner>`, the shape of a TOC entry. */\nconst TOC_ENTRY = /^\\d+;\\s+\\d+\\s+\\d+\\s+(SCHEMA|TABLE)\\s+(?!DATA\\s)(\\S+)\\s+(.+)\\s+(\\S+)$/;\n\n/**\n * The schemas and tables in `pg_restore --list` output: the objects a\n * restore without `--clean` fails on when the target already has them.\n *\n * A TOC line ends in the object's owner, after its namespace and its name;\n * a schema's namespace is `-`. The name is what lies between the two, so a\n * name with a space in it (`Odd Name`) reads whole. `TABLE DATA` and every\n * other kind of entry are not objects of their own here.\n */\nexport function parseDumpObjects(list: string): DumpObjects {\n const objects: DumpObjects = { schemas: [], tables: [] };\n for (const line of list.split(\"\\n\")) {\n const entry = TOC_ENTRY.exec(line.trim());\n if (!entry) continue;\n const [, kind, namespace, name] = entry;\n if (kind === \"SCHEMA\") objects.schemas.push(name);\n else objects.tables.push({ schema: namespace, table: name });\n }\n return objects;\n}\n\n/**\n * Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other\n * cluster-wide objects) live outside any single database, so a per-database\n * `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT\n * statements in the main dump fail on restore and RLS is silently lost — so\n * every backup captures the globals into a sidecar `.globals.sql`.\n *\n * `--no-role-passwords` keeps role secrets out of the artifact (backups may\n * be shipped off-box); roles are recreated password-less and re-secured by\n * the operator.\n */\nexport function buildPgDumpallGlobalsArgs(opts: {\n connectionString: string;\n outFile: string;\n}): string[] {\n return [\n \"--globals-only\",\n \"--no-role-passwords\",\n \"--no-password\",\n `--file=${opts.outFile}`,\n `--dbname=${forLibpq(opts.connectionString)}`\n ];\n}\n\n/**\n * Derive the globals sidecar path/key for a given `.dump` file. Keeps the\n * two artifacts adjacent so listing, uploading and pruning can find one from\n * the other. A name that doesn't end in `.dump` is returned unchanged with a\n * `.globals.sql` suffix appended.\n */\nexport function globalsFileForDump(dumpPath: string): string {\n return dumpPath.endsWith(\".dump\")\n ? dumpPath.slice(0, -\".dump\".length) + \".globals.sql\"\n : dumpPath + \".globals.sql\";\n}\n\n/**\n * Split a `pg_dumpall --globals-only` script into individual statements.\n * Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`\n * can run independently and a benign \"role already exists\" on one doesn't\n * abort the rest. Drops `--` comment lines and blank statements.\n */\nexport function splitGlobalsStatements(sql: string): string[] {\n const withoutComments = sql\n .split(\"\\n\")\n // `--` comments, and psql meta-commands. pg_dumpall 15+ wraps its output\n // in `\\restrict <token>` / `\\unrestrict <token>`, which are instructions\n // to psql, not SQL. This replay sends statements over a connection\n // instead, so they arrived at the server as `\\restrict …` and came back\n // as `syntax error at or near \"\\\"` — reported to the user as two skipped\n // globals per restore, which reads like roles failed to apply when\n // nothing did. They carry no state worth replaying: dropping them is\n // exactly what a SQL-level consumer should do.\n .filter((line) => {\n const trimmed = line.trim();\n return !trimmed.startsWith(\"--\") && !trimmed.startsWith(\"\\\\\");\n })\n .join(\"\\n\");\n return withoutComments\n .split(\";\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Resolve the Postgres connection string the backup commands should use,\n * mirroring the precedence the branch command already relies on.\n */\nexport function resolveConnectionString(\n env: Record<string, string | undefined>\n): string | null {\n return env.DATABASE_URL || env.ADMIN_CONNECTION_STRING || null;\n}\n","/**\n * Retention pruning for scheduled backups. Pure logic — no I/O — so it can\n * be exhaustively unit-tested without touching a storage backend.\n */\nimport { parseBackupTimestamp } from \"./pg-tools\";\n\nexport interface BackupObject {\n /** Storage key / file name. */\n key: string;\n /**\n * Creation time. Optional — when absent it is recovered from the\n * timestamp encoded in the key by `buildBackupFilename`.\n */\n createdAt?: Date;\n /**\n * Size on disk, when the lister knows it (local destinations always do).\n *\n * Reported so an empty artifact cannot masquerade as a backup in\n * `rebase db backups list`. Deliberately NOT consulted by\n * {@link selectBackupsToPrune}: retention decides by age, and a rule that\n * silently skipped small files would make a genuinely tiny dump immortal.\n * Visibility is the fix here; the artifacts themselves are now removed at\n * the point of failure by `discardPartialDump`.\n */\n sizeBytes?: number;\n}\n\nexport interface RetentionOptions {\n /** Delete backups older than this many days. `0`/negative disables age pruning. */\n retentionDays?: number;\n /**\n * Always keep at least this many of the most recent backups, even if\n * they are older than `retentionDays`. Guards against wiping every\n * backup after a long outage. Default: 0 (no minimum).\n */\n keepMinimum?: number;\n /** Reference \"now\" — injectable for deterministic tests. */\n now?: Date;\n}\n\n/**\n * Decide which backups to delete under the given retention policy.\n *\n * Rules, applied in order:\n * 1. Objects whose key isn't a recognised rebase backup (no parseable\n * timestamp and no `createdAt`) are never selected — foreign files in a\n * shared bucket are left untouched.\n * 2. The `keepMinimum` newest backups are always retained.\n * 3. Of the remainder, any older than `retentionDays` are selected for\n * deletion.\n *\n * Returns the keys to delete, oldest first.\n */\nexport function selectBackupsToPrune(\n backups: BackupObject[],\n options: RetentionOptions\n): string[] {\n const retentionDays = options.retentionDays ?? 0;\n const keepMinimum = Math.max(0, options.keepMinimum ?? 0);\n const now = options.now ?? new Date();\n\n if (retentionDays <= 0) {\n return [];\n }\n\n // Resolve a timestamp for every object; drop the ones we can't date.\n const dated = backups\n .map((b) => {\n const createdAt = b.createdAt ?? parseBackupTimestamp(b.key);\n return createdAt ? { key: b.key, createdAt } : null;\n })\n .filter((b): b is { key: string; createdAt: Date } => b !== null);\n\n // Newest first so the first `keepMinimum` are the protected ones.\n dated.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n\n const cutoff = now.getTime() - retentionDays * 24 * 60 * 60 * 1000;\n\n const toDelete = dated\n .slice(keepMinimum)\n .filter((b) => b.createdAt.getTime() < cutoff);\n\n // Return oldest first for predictable, readable logs.\n toDelete.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n return toDelete.map((b) => b.key);\n}\n","/**\n * Where the PostgreSQL client tools are, without spawning them.\n *\n * Kept apart from `backup-service.ts`, which pulls in `execa` (ESM-only), so\n * `createBackupCron` can check for the tools when it is registered.\n */\nimport fs from \"fs\";\nimport { resolveLocalBin } from \"../cli-helpers\";\n\nexport type PgTool = \"pg_dump\" | \"pg_restore\" | \"pg_dumpall\";\n\nconst OVERRIDE_VARIABLE: Record<PgTool, string> = {\n pg_dump: \"PG_DUMP_PATH\",\n pg_restore: \"PG_RESTORE_PATH\",\n pg_dumpall: \"PG_DUMPALL_PATH\"\n};\n\n/** The variable that points at `tool` when it is not on the PATH. */\nexport function pgToolVariable(tool: PgTool): string {\n return OVERRIDE_VARIABLE[tool];\n}\n\n/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */\nexport function resolvePgBinary(\n tool: PgTool,\n env: Record<string, string | undefined> = process.env\n): string | null {\n const override = env[OVERRIDE_VARIABLE[tool]];\n if (override && fs.existsSync(override)) return override;\n return resolveLocalBin(tool);\n}\n","/**\n * Pure-ish orchestration for the backup/restore paths, kept free of `execa`\n * and `pg` value imports so it can be unit-tested under jest (the impure\n * edges — spawning processes, opening connections — live in\n * `backup-service.ts`, which is vitest/runtime only).\n *\n * The functions here take their side-effecting dependency as an argument\n * (a statement runner, an object deleter) so tests can inject fakes.\n */\nimport { globalsFileForDump, splitGlobalsStatements } from \"./pg-tools\";\n\n/**\n * Replay a `pg_dumpall --globals-only` script one statement at a time,\n * tolerating per-statement failures. On a same-cluster restore the roles\n * usually already exist (`CREATE ROLE` → \"already exists\") and on a managed\n * provider an `ALTER ROLE <superuser>` may be refused; neither should abort\n * recreation of the roles that *are* missing. Returns how many statements\n * applied vs were skipped.\n *\n * `runStatement` executes one SQL statement and rejects on error.\n */\nexport async function applyGlobalsWith(\n runStatement: (sql: string) => Promise<void>,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n let applied = 0;\n let skipped = 0;\n for (const statement of splitGlobalsStatements(globalsSql)) {\n try {\n await runStatement(statement);\n applied++;\n } catch (err) {\n skipped++;\n const firstLine = statement.split(\"\\n\")[0].slice(0, 80);\n log(` • Skipped global: ${firstLine} (${err instanceof Error ? err.message : String(err)})`);\n }\n }\n return { applied, skipped };\n}\n\n/**\n * Delete each pruned dump together with its `.globals.sql` roles sidecar, so\n * pruning never orphans the roles file. The sidecar is best-effort — older\n * backups predate it, so a missing-sidecar failure is swallowed while a\n * failure deleting the dump itself propagates.\n *\n * `deleteObject` removes one key and rejects if it cannot (e.g. not found).\n */\nexport async function pruneWith(\n keys: string[],\n deleteObject: (key: string) => Promise<void>\n): Promise<void> {\n for (const key of keys) {\n await deleteObject(key);\n try {\n await deleteObject(globalsFileForDump(key));\n } catch {\n // Sidecar may not exist for older backups — ignore.\n }\n }\n}\n\n/**\n * Remove a dump artifact abandoned by a failed tool run.\n *\n * `pg_dump` creates its `--file=` target before it finishes connecting, so any\n * failure — a URL libpq rejects, a dropped connection, a full disk — leaves a\n * 0-byte file behind. That corpse is not inert: `rebase db backups list` used\n * to show it as an ordinary entry, and `selectBackupsToPrune` ranks by\n * timestamp alone, so it occupies a protected `keepMinimum` slot and can push\n * a real backup out of retention.\n *\n * Best-effort on purpose: the caller is already throwing, and a failed unlink\n * must not replace the real diagnosis with an ENOENT/EPERM from the cleanup.\n *\n * Lives here rather than in `backup-service.ts` for the reason at the top of\n * this file — that module's top-level `execa` import makes it unloadable under\n * jest, so anything placed there cannot be unit-tested.\n *\n * `remove` deletes one file and throws if it cannot; `exists` reports presence.\n */\nexport function discardPartialDumpWith(\n exists: (file: string) => boolean,\n remove: (file: string) => void,\n file: string\n): void {\n try {\n if (exists(file)) remove(file);\n } catch {\n // Ignored: see above.\n }\n}\n","/**\n * Backup / restore orchestration: thin, well-typed wrappers around\n * `pg_dump` and `pg_restore`, plus binary detection, version pre-flight,\n * and upload/prune against a storage backend.\n *\n * The pure argument- and policy-logic lives in `pg-tools.ts` and\n * `retention.ts`; this module is the impure edge (spawns processes, talks\n * to Postgres and storage) and is intentionally kept small.\n */\nimport fs from \"fs\";\nimport os from \"os\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport type { StorageController } from \"@rebasepro/server\";\nimport { resolvePgBinary } from \"./pg-binary\";\nimport {\n BackupDestination,\n buildBackupFilename,\n buildPgDumpArgs,\n buildPgDumpallGlobalsArgs,\n buildRowSecurityPgOptions,\n diagnoseRowSecurityDumpFailure,\n type RowSecurityIdentity,\n buildPgRestoreArgs,\n buildPgRestoreListArgs,\n checkToolServerCompatibility,\n type DumpObjects,\n globalsFileForDump,\n joinStorageKey,\n parsePgToolMajor,\n parseBackupTimestamp,\n parseDumpObjects,\n serverVersionNumToMajor,\n VersionCompatibility\n} from \"./pg-tools\";\nimport { BackupObject, RetentionOptions, selectBackupsToPrune } from \"./retention\";\nimport { applyGlobalsWith, discardPartialDumpWith, pruneWith } from \"./backup-logic\";\nimport { splitPassword } from \"../utils/connection-string\";\n\n/**\n * How a libpq tool is invoked for a connection string: the URL its command\n * line carries, and the environment it runs in.\n *\n * The password goes in `PGPASSWORD`, never on the command line, where every\n * process listing could read it and every failure printed it back (execa's\n * error repeats the command). See {@link splitPassword}.\n */\nfunction libpqInvocation(\n connectionString: string,\n env: Record<string, string | undefined>\n): { connectionString: string; env: Record<string, string> } {\n const split = splitPassword(connectionString);\n const toolEnv: Record<string, string> = {};\n for (const [key, value] of Object.entries(env)) {\n if (value !== undefined) toolEnv[key] = value;\n }\n if (split.password !== null) toolEnv.PGPASSWORD = split.password;\n return { connectionString: split.connectionString, env: toolEnv };\n}\n\n/**\n * Remove a dump artifact abandoned by a failed tool run. See\n * {@link discardPartialDumpWith} for why this exists and why the logic lives\n * in the execa-free module.\n */\nexport function discardPartialDump(file: string): void {\n discardPartialDumpWith(\n (f) => fs.existsSync(f),\n (f) => fs.unlinkSync(f),\n file\n );\n}\n\nexport class BackupToolError extends Error {\n constructor(message: string, readonly hint?: string) {\n super(message);\n this.name = \"BackupToolError\";\n }\n}\n\nexport { resolvePgBinary };\n\n/** Run `<bin> --version` and extract the major version. */\nexport async function detectToolMajor(bin: string): Promise<number | null> {\n try {\n const { stdout } = await execa(bin, [\"--version\"]);\n return parsePgToolMajor(stdout);\n } catch {\n return null;\n }\n}\n\n/** Query the server for its major version via `server_version_num`. */\nexport async function getServerVersionMajor(connectionString: string): Promise<number | null> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n const res = await client.query(\"SHOW server_version_num\");\n const raw = res.rows?.[0]?.server_version_num;\n return serverVersionNumToMajor(raw);\n } finally {\n await client.end();\n }\n}\n\nexport interface PreflightResult extends VersionCompatibility {\n bin: string;\n toolMajor: number | null;\n serverMajor: number | null;\n}\n\n/**\n * Verify the requested client tool exists and its major version is\n * compatible with the live server. Throws {@link BackupToolError} — with a\n * doctor-style hint — when the binary is missing.\n */\nexport async function preflight(\n tool: \"pg_dump\" | \"pg_restore\",\n connectionString: string,\n env: Record<string, string | undefined> = process.env\n): Promise<PreflightResult> {\n const bin = resolvePgBinary(tool, env);\n if (!bin) {\n throw new BackupToolError(\n `Could not find the '${tool}' binary.`,\n `Install the PostgreSQL client tools (e.g. 'brew install libpq' or 'apt-get install postgresql-client'), ` +\n `or set ${tool === \"pg_dump\" ? \"PG_DUMP_PATH\" : \"PG_RESTORE_PATH\"} to its full path.`\n );\n }\n const [toolMajor, serverMajor] = await Promise.all([\n detectToolMajor(bin),\n getServerVersionMajor(connectionString)\n ]);\n const compat = checkToolServerCompatibility(toolMajor, serverMajor);\n return { bin, toolMajor, serverMajor, ...compat };\n}\n\nexport interface BackupResult {\n /** Absolute path of the produced dump file on local disk. */\n localFile: string;\n fileName: string;\n sizeBytes: number;\n /**\n * Absolute path of the `.globals.sql` sidecar holding cluster-wide roles\n * (present unless globals capture was disabled or unavailable).\n */\n globalsFile?: string;\n globalsSizeBytes?: number;\n}\n\n/**\n * Produce a custom-format dump on local disk. When `outDir` is omitted the\n * file is written to the OS temp directory (used by the upload path, which\n * cleans it up afterwards).\n *\n * Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via\n * `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements\n * depend on can be recreated on restore. Set `includeGlobals: false` to skip\n * it (e.g. when the caller has no privilege to read cluster globals).\n */\nexport async function createDump(opts: {\n connectionString: string;\n dbName: string;\n outDir?: string;\n fileName?: string;\n excludeSchemas?: string[];\n noOwner?: boolean;\n inheritStdio?: boolean;\n includeGlobals?: boolean;\n env?: Record<string, string | undefined>;\n /**\n * Dump with row security left on, reading as this identity.\n *\n * The escape hatch for a managed Postgres, where the dumping role owns\n * nothing and has no `BYPASSRLS`. Off by default, and deliberately so: with\n * row security on, `pg_dump` stops erroring on rows it cannot see and\n * simply omits them. See {@link RowSecurityIdentity}.\n */\n rowSecurity?: RowSecurityIdentity;\n}): Promise<BackupResult> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_dump\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dump' binary.\",\n \"Install the PostgreSQL client tools or set PG_DUMP_PATH.\"\n );\n }\n\n const fileName = opts.fileName ?? buildBackupFilename(opts.dbName);\n const outDir = opts.outDir ?? fs.mkdtempSync(path.join(os.tmpdir(), \"rebase-backup-\"));\n fs.mkdirSync(outDir, { recursive: true });\n const localFile = path.join(outDir, fileName);\n createOwnerOnly(localFile);\n\n const tool = libpqInvocation(opts.connectionString, env);\n const args = buildPgDumpArgs({\n connectionString: tool.connectionString,\n outFile: localFile,\n excludeSchemas: opts.excludeSchemas,\n noOwner: opts.noOwner,\n rowSecurity: opts.rowSecurity\n });\n\n // `PGOPTIONS` is the only way to set a GUC on a tool that takes no SQL.\n // Built from the same object that added `--enable-row-security`, so the\n // flag cannot travel without the identity that makes it safe.\n const dumpEnv: Record<string, string> = { ...tool.env };\n if (opts.rowSecurity) {\n dumpEnv.PGOPTIONS = [env.PGOPTIONS, buildRowSecurityPgOptions(opts.rowSecurity)]\n .filter(Boolean).join(\" \");\n }\n\n try {\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: dumpEnv\n });\n } catch (error) {\n // pg_dump creates its `--file=` target before it finishes connecting,\n // so any failure here — a URL libpq rejects, a dropped connection, a\n // full disk — leaves a 0-byte file behind that nothing else removed.\n // That corpse is not inert: `rebase db backups list` shows it as an\n // ordinary entry, and `selectBackupsToPrune` ranks by timestamp alone,\n // so it occupies a protected `keepMinimum` slot and can push a real\n // backup out of retention. A missing backup is honest; an empty file\n // that reads as a backup is not.\n discardPartialDump(localFile);\n // The RLS failure names a table and no cause. Replace it with the\n // cause and the two ways out; anything else is re-thrown untouched.\n const diagnosis = diagnoseRowSecurityDumpFailure(error);\n if (!diagnosis) throw error;\n throw new BackupToolError(\n diagnosis,\n \"Run `rebase db backup --help` for the flag, and read what it says about partial dumps.\"\n );\n }\n\n const sizeBytes = fs.existsSync(localFile) ? fs.statSync(localFile).size : 0;\n\n const result: BackupResult = { localFile, fileName, sizeBytes };\n\n if (opts.includeGlobals !== false) {\n const dumpallBin = resolvePgBinary(\"pg_dumpall\", env);\n if (!dumpallBin) {\n throw new BackupToolError(\n \"Could not find the 'pg_dumpall' binary needed to capture cluster roles.\",\n \"Install the PostgreSQL client tools or set PG_DUMPALL_PATH. \" +\n \"To take a role-incomplete backup anyway, pass includeGlobals: false.\"\n );\n }\n const globalsFile = globalsFileForDump(localFile);\n createOwnerOnly(globalsFile);\n try {\n await execa(\n dumpallBin,\n buildPgDumpallGlobalsArgs({ connectionString: tool.connectionString, outFile: globalsFile }),\n { stdio: opts.inheritStdio ? \"inherit\" : \"pipe\", env: tool.env }\n );\n } catch (error) {\n // Same reasoning as the pg_dump catch above, and the dump goes with\n // it: the pair is uploaded and pruned together, so a dump whose\n // roles sidecar is missing restores without the roles its GRANT and\n // RLS statements need — the exact failure the sidecar exists to\n // prevent. Leaving half a pair on disk would look like a backup.\n discardPartialDump(globalsFile);\n discardPartialDump(localFile);\n throw error;\n }\n result.globalsFile = globalsFile;\n result.globalsSizeBytes = fs.existsSync(globalsFile) ? fs.statSync(globalsFile).size : 0;\n }\n\n return result;\n}\n\n/**\n * Create `file` empty and readable by its owner only, before a tool writes it.\n *\n * A dump holds every row, and the `.globals.sql` sidecar every role's password\n * hash. pg_dump and pg_dumpall create their `--file=` target with the\n * process's umask — 0644 on most machines, readable by every user — and keep\n * the mode of a file that is already there. Created 0600 first, the file is\n * never readable by anyone else, not even while the dump runs; chmod-ing it\n * afterwards would leave it open for as long as that takes. A file that\n * already existed is narrowed too.\n */\nfunction createOwnerOnly(file: string): void {\n fs.closeSync(fs.openSync(file, \"w\", 0o600));\n fs.chmodSync(file, 0o600);\n}\n\n/**\n * Cheap integrity check on a freshly written dump: it must be non-empty and\n * `pg_restore --list` must parse its table of contents without error. Used\n * before pruning older backups so a corrupt-but-exit-0 dump never becomes\n * the reason the last good backup is deleted.\n */\nexport async function validateDump(\n localFile: string,\n env: Record<string, string | undefined> = process.env\n): Promise<{ ok: boolean; reason?: string }> {\n if (!fs.existsSync(localFile)) {\n return { ok: false, reason: `Dump file does not exist: ${localFile}` };\n }\n if (fs.statSync(localFile).size === 0) {\n return { ok: false, reason: \"Dump file is empty (0 bytes).\" };\n }\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n // Can't verify without pg_restore; treat as inconclusive-but-fail so\n // pruning doesn't proceed on an unverified dump.\n return { ok: false, reason: \"Could not find 'pg_restore' to verify the dump.\" };\n }\n try {\n await execa(bin, buildPgRestoreListArgs(localFile), {\n stdio: \"pipe\",\n env: { ...(env as Record<string, string>) }\n });\n return { ok: true };\n } catch (err) {\n return { ok: false, reason: `pg_restore --list failed: ${err instanceof Error ? err.message : String(err)}` };\n }\n}\n\n/**\n * Replay a `pg_dumpall --globals-only` script to recreate cluster roles\n * before a restore, so the dump's GRANT/RLS statements (which reference\n * `rebase_user` and any owner roles) actually apply. Runs statement by\n * statement and tolerates per-statement failures — on a same-cluster restore\n * the roles usually already exist (`CREATE ROLE` → \"already exists\"), and on\n * a managed provider an `ALTER ROLE <superuser>` may be refused; neither\n * should abort role recreation. Returns how many statements applied vs were\n * skipped.\n */\nexport async function applyGlobals(\n connectionString: string,\n globalsSql: string,\n log: (message: string) => void = () => {}\n): Promise<{ applied: number; skipped: number }> {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n await client.connect();\n try {\n return await applyGlobalsWith(\n async (sql) => { await client.query(sql); },\n globalsSql,\n log\n );\n } finally {\n await client.end();\n }\n}\n\n/**\n * Restore a custom-format dump into the database named by\n * `connectionString`. Destructive when `clean` is set (drops objects\n * first). Never called automatically — the CLI gates it behind explicit\n * confirmation.\n *\n * Runs with `--exit-on-error` by default: a restore that logs-and-continues\n * past a failed GRANT (because a role was missing) reports success with RLS\n * un-enforced. Callers should recreate roles first (see {@link applyGlobals})\n * and only set `exitOnError: false` deliberately.\n */\nexport async function restoreDump(opts: {\n connectionString: string;\n inputFile: string;\n clean?: boolean;\n noOwner?: boolean;\n exitOnError?: boolean;\n inheritStdio?: boolean;\n env?: Record<string, string | undefined>;\n}): Promise<void> {\n const env = opts.env ?? process.env;\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_restore' binary.\",\n \"Install the PostgreSQL client tools or set PG_RESTORE_PATH.\"\n );\n }\n const tool = libpqInvocation(opts.connectionString, env);\n const args = buildPgRestoreArgs({\n connectionString: tool.connectionString,\n inputFile: opts.inputFile,\n clean: opts.clean,\n noOwner: opts.noOwner,\n exitOnError: opts.exitOnError\n });\n await execa(bin, args, {\n stdio: opts.inheritStdio ? \"inherit\" : \"pipe\",\n env: tool.env\n });\n}\n\n/** The schemas and tables a dump creates, from `pg_restore --list`. See {@link parseDumpObjects}. */\nexport async function readDumpObjects(\n inputFile: string,\n env: Record<string, string | undefined> = process.env\n): Promise<DumpObjects> {\n const bin = resolvePgBinary(\"pg_restore\", env);\n if (!bin) {\n throw new BackupToolError(\n \"Could not find the 'pg_restore' binary.\",\n \"Install the PostgreSQL client tools or set PG_RESTORE_PATH.\"\n );\n }\n const toolEnv = Object.fromEntries(\n Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)\n );\n const { stdout } = await execa(bin, buildPgRestoreListArgs(inputFile), { stdio: \"pipe\", env: toolEnv });\n return parseDumpObjects(stdout);\n}\n\n/**\n * Which of a dump's schemas and tables the target database already holds,\n * as `schema <name>` / `table <schema>.<name>`: what a restore without\n * `--clean` would stop on. `public` is left out of the schemas — every\n * database has it, and pg_restore does not create it. None for a database\n * that does not exist yet.\n */\nexport async function findRestoreConflicts(connectionString: string, objects: DumpObjects): Promise<string[]> {\n const schemas = objects.schemas.filter((schema) => schema !== \"public\");\n if (schemas.length === 0 && objects.tables.length === 0) return [];\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString });\n try {\n await client.connect();\n } catch (err) {\n // 3D000: the database does not exist yet, as `--create-db` finds it.\n if (err instanceof Error && \"code\" in err && err.code === \"3D000\") return [];\n throw err;\n }\n try {\n const existingSchemas = await client.query<{ nspname: string }>(\n \"SELECT nspname FROM pg_namespace WHERE nspname = ANY($1::text[]) ORDER BY nspname\",\n [schemas]\n );\n const existingTables = await client.query<{ nspname: string; relname: string }>(\n `SELECT n.nspname, c.relname\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n JOIN unnest($1::text[], $2::text[]) AS d(schema_name, table_name)\n ON d.schema_name = n.nspname AND d.table_name = c.relname\n WHERE c.relkind IN ('r', 'p')\n ORDER BY n.nspname, c.relname`,\n [objects.tables.map((t) => t.schema), objects.tables.map((t) => t.table)]\n );\n return [\n ...existingSchemas.rows.map((row) => `schema ${row.nspname}`),\n ...existingTables.rows.map((row) => `table ${row.nspname}.${row.relname}`)\n ];\n } finally {\n await client.end();\n }\n}\n\n/**\n * Create a database (if absent) by connecting to the maintenance\n * `postgres` database. Used by `restore --create-db`.\n */\nexport async function ensureDatabaseExists(\n adminConnectionString: string,\n dbName: string\n): Promise<boolean> {\n const { Client } = await import(\"pg\");\n const parsed = new URL(adminConnectionString);\n parsed.pathname = \"/postgres\";\n const client = new Client({ connectionString: parsed.toString() });\n await client.connect();\n try {\n const existing = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [dbName]);\n if (existing.rowCount && existing.rowCount > 0) {\n return false;\n }\n // Identifier can't be parameterised; guard against injection.\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(dbName)) {\n throw new BackupToolError(`Invalid database name: \"${dbName}\"`);\n }\n await client.query(`CREATE DATABASE \"${dbName}\"`);\n return true;\n } finally {\n await client.end();\n }\n}\n\n/**\n * Upload a local dump to object storage under the destination's prefix.\n * Backups may contain secrets/PII, so the object is written with a\n * private/octet-stream content type; bucket-level ACLs must stay private.\n */\nexport async function uploadBackup(\n storage: StorageController,\n localFile: string,\n dest: Extract<BackupDestination, { kind: \"s3\" | \"gcs\" }>\n): Promise<{ key: string; storageUrl: string }> {\n const fileName = path.basename(localFile);\n const key = joinStorageKey(dest.prefix, fileName);\n // Backed by the dump on disk, not read into memory: the built-in\n // controllers write it a chunk (S3: a multipart part) at a time. Read with\n // `readFileSync` and copied into a `File`, a dump was held three or four\n // times over in the process running the cron, and one over 2 GiB could\n // not be read at all (`ERR_FS_FILE_TOO_LARGE`), so it never uploaded.\n const file = new File([await fs.openAsBlob(localFile)], fileName, { type: \"application/octet-stream\" });\n // Not held to the controller's upload limits: `maxFileSize` (50 MB unless\n // set) and `allowedMimeTypes` say what a user may upload, and the CLI and\n // the documented cron build their controller without either. Held to\n // them, every dump over 50 MB failed with PAYLOAD_TOO_LARGE.\n const result = await storage.putObject({\n file,\n key,\n bucket: dest.bucket,\n metadata: { \"rebase-backup\": \"1\" }\n }, { uploadLimits: false });\n return { key, storageUrl: result.storageUrl };\n}\n\n/** Stats in flight at once while listing a local backup folder. */\nconst LOCAL_STAT_CONCURRENCY = 32;\n\n/** `fn` over `items`, at most `limit` at a time; results in input order. */\nasync function mapBounded<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const results = new Array<R>(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n results[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return results;\n}\n\n/**\n * List existing backups at a destination. For local destinations reads the\n * directory; for object storage lists the prefix via the controller.\n */\nexport async function listBackups(\n dest: BackupDestination,\n storage?: StorageController\n): Promise<BackupObject[]> {\n if (dest.kind === \"local\") {\n // Asynchronous throughout, a few stats at a time: retention lists the\n // folder after every scheduled backup, inside the server process, and\n // a year of hourly backups is 8,760 dumps stat'ed one by one.\n const stat = await fs.promises.stat(dest.path).catch(() => null);\n if (!stat) return [];\n const dir = stat.isDirectory() ? dest.path : path.dirname(dest.path);\n const dumps = (await fs.promises.readdir(dir)).filter((f) => f.endsWith(\".dump\"));\n const stats = await mapBounded(dumps, LOCAL_STAT_CONCURRENCY, (f) => fs.promises.stat(path.join(dir, f)));\n return dumps\n .map((f, i) => {\n const createdAt = parseBackupTimestamp(f) ?? stats[i].mtime;\n return { key: path.join(dir, f), createdAt, sizeBytes: stats[i].size };\n })\n .sort((a, b) => (b.createdAt?.getTime() ?? 0) - (a.createdAt?.getTime() ?? 0));\n }\n\n if (!storage) {\n throw new BackupToolError(\n `Listing ${dest.kind} backups requires a configured storage backend.`\n );\n }\n // The server's lister, not a second copy of it: a listing is paged, in\n // ascending key order, and this one read a single page of 1,000 — the\n // OLDEST backups — so `rebase db backups list` stopped showing new ones,\n // and retention ranked a page that held none of the newest. The two copies\n // had already drifted apart once (over the roles sidecar).\n const { listBackupObjects } = await import(\"@rebasepro/server\");\n const listed = await listBackupObjects(dest, storage);\n return listed.map((info) => ({\n key: info.key,\n createdAt: info.createdAt ? new Date(info.createdAt) : undefined\n }));\n}\n\n/**\n * Apply a retention policy to a destination, deleting the backups selected\n * by {@link selectBackupsToPrune}. Returns the keys that were removed.\n */\nexport async function pruneBackups(\n dest: BackupDestination,\n options: RetentionOptions,\n storage?: StorageController\n): Promise<string[]> {\n const backups = await listBackups(dest, storage);\n const toDelete = selectBackupsToPrune(backups, options);\n\n // Delete each dump together with its `.globals.sql` sidecar (pruneWith\n // handles the pairing + best-effort sidecar). The deleter rejects on a\n // missing key so pruneWith can swallow an absent sidecar.\n const deleteObject =\n dest.kind === \"local\"\n ? async (key: string) => {\n if (!fs.existsSync(key)) throw new Error(`not found: ${key}`);\n fs.unlinkSync(key);\n }\n : async (key: string) => {\n if (!storage) throw new BackupToolError(\"Storage backend required to prune object backups.\");\n await storage.deleteObject(key, dest.bucket);\n };\n\n await pruneWith(toDelete, deleteObject);\n return toDelete;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-console-C_ISGwJo.js","names":[],"sources":["../src/cli-console.ts"],"sourcesContent":["/**\n * `rebase console` — the driver's half.\n *\n * Loads the project's collections the way `rebase db push` does, opens the\n * database with {@link openRebase}, and either evaluates one piece of code\n * (`--eval`) or starts a Node REPL with `rebase` and `collections` in scope.\n * The `rebase` CLI resolves the database first (the managed one included) and\n * hands this process `DATABASE_URL`, exactly as it does for `db` commands.\n *\n * This is the boot side of `openRebase`: it is where the environment is read\n * — `DATABASE_URL`, and the `REBASE_DB_POOL_MAX` ceiling the managed\n * development database needs — and turned into explicit options.\n */\nimport arg from \"arg\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport readline from \"node:readline\";\nimport repl from \"node:repl\";\nimport { PassThrough } from \"node:stream\";\nimport tty from \"node:tty\";\nimport { inspect } from \"node:util\";\nimport { setLogOutput } from \"@rebasepro/server\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { RELAYED_FLAGS } from \"./cli-flags\";\nimport { diagnoseDbError } from \"./cli-errors\";\nimport { out, outError, describeFailure } from \"./cli-output\";\nimport { poolMaxCeiling } from \"./connection\";\nimport { openRebase, type OpenedRebase } from \"./open-rebase\";\n\n/**\n * The flags `rebase console` takes. `--history` is the CLI's own: where it\n * keeps the REPL's history (`.rebase/console_history` in the project).\n */\nexport const CONSOLE_FLAGS = {\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--eval\": String,\n \"-e\": \"--eval\",\n \"--history\": String\n} satisfies arg.Spec;\n\n/** What the code the console runs can name. */\nexport interface ConsoleScope {\n rebase: OpenedRebase;\n collections: CollectionConfig[];\n}\n\ntype ScopedFunction = (rebase: OpenedRebase, collections: CollectionConfig[]) => Promise<unknown>;\ntype AsyncFunctionConstructor = new (...parameters: string[]) => ScopedFunction;\n\n/** The constructor of `async function` — what `new Function` is for a body that awaits. */\nconst AsyncFunction: AsyncFunctionConstructor = Object.getPrototypeOf(async () => undefined).constructor;\n\n/**\n * Run `code` with `rebase` and `collections` in scope and resolve with its\n * value.\n *\n * Read as an expression first, so `rebase.dataAsAdmin.posts.find()` is its own\n * result; code that is not one expression runs as a function body, where\n * `await` works everywhere and `return` gives the result.\n */\nexport async function evaluateInConsole(code: string, scope: ConsoleScope): Promise<unknown> {\n let run: ScopedFunction;\n try {\n run = new AsyncFunction(\"rebase\", \"collections\", `return (${code}\\n);`);\n } catch (error) {\n if (!(error instanceof SyntaxError)) throw error;\n run = new AsyncFunction(\"rebase\", \"collections\", code);\n }\n return run(scope.rebase, scope.collections);\n}\n\n/** A result as JSON, the way `--eval` prints it: dates as ISO strings, bigints as strings. */\nexport function consoleJson(value: unknown): string {\n return JSON.stringify(value, (_key, item: unknown) => typeof item === \"bigint\" ? item.toString() : item, 2) ?? \"undefined\";\n}\n\n/** The host and database a URL names, for the banner — never its credentials. */\nfunction describeDatabase(databaseUrl: string): string {\n try {\n const url = new URL(databaseUrl);\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}${url.pathname}`;\n } catch {\n return \"the configured database\";\n }\n}\n\n/** Where the REPL reads and writes; the terminal unless a caller hands it streams. */\nexport interface ConsoleStreams {\n input?: NodeJS.ReadableStream;\n output?: NodeJS.WritableStream;\n}\n\n/**\n * `rebase console [--eval <code>] [--collections <dir>]`, from the driver\n * line `[\"console\", …flags]`. Resolves with the exit code.\n */\nexport async function consoleCommand(args: string[], streams: ConsoleStreams = {}): Promise<number> {\n const flags = arg({ ...RELAYED_FLAGS, ...CONSOLE_FLAGS }, { argv: args.slice(1) });\n if (flags[\"--help\"]) {\n out(\"Run `rebase console --help` for the options — this is the driver's own entry point.\");\n return 0;\n }\n\n const databaseUrl = flags[\"--database-url\"] || process.env.DATABASE_URL;\n if (!databaseUrl) {\n outError(\"✗ No database to open: DATABASE_URL is not set. Set it in .env, or pass --database-url.\");\n return 1;\n }\n\n // The console's own lines are what the reader is here for; the driver's\n // info-level boot notes are not, unless they asked for them.\n if (process.env.LOG_LEVEL === undefined) process.env.LOG_LEVEL = \"warn\";\n\n // And whatever level they asked for, a log line is never part of the\n // answer: stdout holds the `--eval` result a pipe reads (`| jq .`) and the\n // REPL's values. The scaffold's `.env` sets LOG_LEVEL=info, and under\n // NODE_ENV=production warnings go to stdout too.\n setLogOutput(\"stderr\");\n try {\n return await openAndRun({\n databaseUrl,\n collections: flags[\"--collections\"],\n eval: flags[\"--eval\"],\n history: flags[\"--history\"]\n }, streams);\n } finally {\n setLogOutput();\n }\n}\n\n/** Whether a connection was reset or cut off before it answered, anywhere in the error's chain. */\nfunction connectionCutOff(error: unknown, depth = 0): boolean {\n if (!error || typeof error !== \"object\" || depth > 4) return false;\n if (\"code\" in error && error.code === \"ECONNRESET\") return true;\n if (error instanceof Error && /Connection terminated unexpectedly/.test(error.message)) return true;\n if (\"errors\" in error && Array.isArray(error.errors) && error.errors.some(inner => connectionCutOff(inner, depth + 1))) return true;\n return \"cause\" in error && connectionCutOff(error.cause, depth + 1);\n}\n\n/**\n * What the console says when the project's database will not open: the banner\n * every db command prints for a database it cannot reach, or one sentence —\n * never the driver's error serialised onto one line, stack and all.\n *\n * The managed development database refuses a connection over its limit by\n * resetting it, and `read ECONNRESET` says nothing about why. It accepts only a\n * few at a time, and a running `rebase dev` holds some of them.\n */\nexport function describeOpenFailure(error: unknown, databaseUrl: string, managed: boolean): string {\n if (managed && connectionCutOff(error)) {\n return \"✗ The managed development database has no free connection: it accepts only a few at a time, \" +\n \"and other clients hold them — a running `rebase dev` holds some.\\n\" +\n \" Close another `rebase console`, psql or database GUI and run this again, \" +\n \"or stop `rebase dev` while you use the console.\";\n }\n return diagnoseDbError(error, databaseUrl) ?? describeFailure(error);\n}\n\n/** Open the project, run `--eval` or the REPL, and close it again. Resolves with the exit code. */\nasync function openAndRun(\n options: { databaseUrl: string; collections?: string; eval?: string; history?: string },\n streams: ConsoleStreams\n): Promise<number> {\n const { databaseUrl } = options;\n const collectionsPath = path.resolve(options.collections || path.join(\"..\", \"config\", \"collections\"));\n const { loadCollections } = await import(\"./schema/doctor\");\n const collections = fs.existsSync(collectionsPath) ? await loadCollections(collectionsPath) : [];\n\n const ceiling = poolMaxCeiling();\n let rebase: OpenedRebase;\n try {\n rebase = await openRebase({\n databaseUrl,\n collections,\n ...(ceiling !== null ? { pool: { max: ceiling } } : {})\n });\n } catch (error) {\n outError(describeOpenFailure(error, databaseUrl, process.env.REBASE_DEV_DATABASE_KIND === \"managed\"));\n return 1;\n }\n\n try {\n const code = options.eval;\n if (code !== undefined) {\n try {\n const result = await evaluateInConsole(code, { rebase, collections: rebase.collections });\n if (result !== undefined) out(consoleJson(result));\n return 0;\n } catch (error) {\n outError(describeFailure(error));\n return 1;\n }\n }\n\n await startRepl({ rebase, collections: rebase.collections }, {\n history: options.history,\n database: describeDatabase(databaseUrl),\n ...streams\n });\n return 0;\n } finally {\n await rebase.close();\n }\n}\n\n/** The interactive half: a Node REPL until `.exit`, Ctrl-D or the end of piped input. */\nasync function startRepl(scope: ConsoleScope, options: { history?: string; database: string } & ConsoleStreams): Promise<void> {\n const input = options.input ?? process.stdin;\n const output = options.output ?? process.stdout;\n const typed = input instanceof tty.ReadStream && input.isTTY;\n\n // For a person at a terminal. Piped, stdout is the REPL's answers, which\n // whatever reads the pipe is after, so the banner goes to stderr with the\n // rest of what the console says about itself.\n const say = typed ? out : outError;\n say(\"\");\n say(` rebase console — ${options.database}, ${scope.collections.length} collection(s)`);\n say(\" In scope: rebase (dataAsAdmin, sql, withAuth, close) and collections. Top-level await works.\");\n say(\" Try: await rebase.dataAsAdmin.<collection>.find({ limit: 5 })\");\n say(\"\");\n\n // Typed at a terminal, the REPL reads it as one. Anything else — a pipe, a\n // file, a stream handed in — reaches it a line at a time through `feed`\n // (see `feedLineByLine`).\n const feed = typed ? undefined : new PassThrough();\n // Colours for a terminal only: piped to a file or another program, the\n // escape codes are noise in the text.\n const colors = output instanceof tty.WriteStream && output.isTTY;\n // Started without a prompt, and given one once the scope is in place: a\n // line that arrives with the first prompt must already see `rebase`.\n const server = repl.start({\n prompt: \"\",\n useGlobal: false,\n ...(feed ? { input: feed, output, terminal: false } : { input, output }),\n writer: (value: unknown) => inspect(value, { colors, depth: 6 })\n });\n server.context.rebase = scope.rebase;\n server.context.collections = scope.collections;\n server.setPrompt(\"rebase> \");\n\n const history = options.history;\n if (history) {\n fs.mkdirSync(path.dirname(history), { recursive: true });\n await new Promise<void>((resolve) => {\n server.setupHistory(history, (error) => {\n if (error) outError(` (history is off: ${error.message})`);\n resolve();\n });\n });\n }\n\n const exited = new Promise<void>((resolve) => server.once(\"exit\", () => resolve()));\n server.displayPrompt();\n if (feed) await feedLineByLine(input, server, feed, exited);\n await exited;\n}\n\n/**\n * Hand input that is not typed at a terminal to the REPL a line at a time,\n * each once the REPL has finished the one before.\n *\n * Typed, the REPL holds keystrokes back while a top-level `await` is pending.\n * Piped, every line arrives at once and each is evaluated as it arrives, so in\n * `const { data } = await …` followed by `data.map(…)` the second line read\n * `data` before the first had assigned it.\n *\n * The REPL is paused as each line reaches it, and it resumes its input when it\n * displays its prompt again: once the line's value has settled, its error has\n * been printed, or it needs the next line to complete it. That resumption is\n * when the next line goes in. Lines are written one per chunk, because a\n * chunk holding several is split and every line of it handed over at once.\n */\nasync function feedLineByLine(\n source: NodeJS.ReadableStream,\n server: repl.REPLServer,\n feed: PassThrough,\n exited: Promise<void>\n): Promise<void> {\n let evaluated: (() => void) | undefined;\n const ready = () => {\n const resolve = evaluated;\n evaluated = undefined;\n resolve?.();\n };\n // Before the REPL's own handler, so a line it evaluates at once resumes\n // after this pause rather than before it.\n server.prependListener(\"line\", () => {\n if (!server.editorMode) server.pause();\n });\n // `.editor` collects lines with no prompt until Ctrl-D: they go straight in.\n server.on(\"line\", () => {\n if (!server.editorMode) return;\n server.resume();\n ready();\n });\n server.on(\"resume\", ready);\n\n const closed = exited.then(() => \"closed\" as const);\n const lines = readline.createInterface({ input: source, crlfDelay: Infinity });\n for await (const line of lines) {\n const done = new Promise<\"done\">((resolve) => { evaluated = () => resolve(\"done\"); });\n feed.write(`${line}\\n`);\n // `.exit`: the rest of the input is not for this REPL.\n if (await Promise.race([done, closed]) === \"closed\") break;\n }\n feed.end();\n}\n"],"mappings":"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"}