@rebasepro/cli 0.23.0 → 0.24.1-canary.g19596ef
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/bin/rebase.js +4 -1
- package/dist/agent-setup-B31F00DC.js +324 -0
- package/dist/agent-setup-B31F00DC.js.map +1 -0
- package/dist/api-keys-Bl1I9q7M.js +525 -0
- package/dist/api-keys-Bl1I9q7M.js.map +1 -0
- package/dist/apps-CNgQJjg5.js +340 -0
- package/dist/apps-CNgQJjg5.js.map +1 -0
- package/dist/args-BnmsEXvu.js +109 -0
- package/dist/args-BnmsEXvu.js.map +1 -0
- package/dist/atlas-dev-database-D_LP_Woq.js +62 -0
- package/dist/atlas-dev-database-D_LP_Woq.js.map +1 -0
- package/dist/auth-aySHEF45.js +414 -0
- package/dist/auth-aySHEF45.js.map +1 -0
- package/dist/branch-pointer-DvSHBjjz.js +2 -0
- package/dist/build-DIHWIqO4.js +336 -0
- package/dist/build-DIHWIqO4.js.map +1 -0
- package/dist/bundle-CqURVN-K.js +1621 -0
- package/dist/bundle-CqURVN-K.js.map +1 -0
- package/dist/bundle.d.ts +12 -3
- package/dist/cli.d.ts +23 -2
- package/dist/cloud-DjByQlkW.js +9667 -0
- package/dist/cloud-DjByQlkW.js.map +1 -0
- package/dist/collection-drift-BKj_BoMq.js +106 -0
- package/dist/collection-drift-BKj_BoMq.js.map +1 -0
- package/dist/commands/__test_support__/parser-specs.d.ts +13 -0
- package/dist/commands/agent-setup.d.ts +116 -0
- package/dist/commands/api-keys.d.ts +86 -23
- package/dist/commands/apps.d.ts +17 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/build.d.ts +5 -1
- package/dist/commands/cloud/auth.d.ts +4 -3
- package/dist/commands/cloud/bundle-deploy.d.ts +45 -0
- package/dist/commands/cloud/context.d.ts +44 -5
- package/dist/commands/cloud/cron.d.ts +53 -0
- package/dist/commands/cloud/debug.d.ts +134 -0
- package/dist/commands/cloud/index.d.ts +89 -12
- package/dist/commands/cloud/token-capabilities.d.ts +76 -0
- package/dist/commands/cloud/tokens.d.ts +23 -0
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/db.d.ts +79 -25
- package/dist/commands/dev.d.ts +181 -13
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/generate_sdk.d.ts +29 -2
- package/dist/commands/init.d.ts +115 -14
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/skills.d.ts +126 -7
- package/dist/commands/start.d.ts +44 -0
- package/dist/commands/storage.d.ts +46 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/consent-ClRHmcxf.js +76 -0
- package/dist/consent-ClRHmcxf.js.map +1 -0
- package/dist/console-D6SJf8k_.js +102 -0
- package/dist/console-D6SJf8k_.js.map +1 -0
- package/dist/context-BqhRydBg.js +1234 -0
- package/dist/context-BqhRydBg.js.map +1 -0
- package/dist/{daemon-entry-LTFKtpHy.js → daemon-entry-Da-D3rmH.js} +3 -4
- package/dist/daemon-entry-Da-D3rmH.js.map +1 -0
- package/dist/daemon-pzsQwDHa.js +2 -0
- package/dist/db-B7RuNuaQ.js +1312 -0
- package/dist/db-B7RuNuaQ.js.map +1 -0
- package/dist/derive-Dl0z2OWi.js +484 -0
- package/dist/derive-Dl0z2OWi.js.map +1 -0
- package/dist/dev-CYr_WXan.js +1599 -0
- package/dist/dev-CYr_WXan.js.map +1 -0
- package/dist/dev-db/atlas-dev-database.d.ts +32 -0
- package/dist/dev-db/daemon-entry.d.ts +13 -0
- package/dist/dev-db/daemon.d.ts +10 -0
- package/dist/dev-db/last-dev-database.d.ts +41 -0
- package/dist/dev-db/prepare.d.ts +29 -3
- package/dist/dev-db/pull.d.ts +90 -5
- package/dist/dev-db/resolve.d.ts +26 -2
- package/dist/dev-preflight-B-LYyBf1.js +2 -0
- package/dist/dev-server-DG8Tt-uf.js +96 -0
- package/dist/dev-server-DG8Tt-uf.js.map +1 -0
- package/dist/doctor-BP8cZ-7F.js +771 -0
- package/dist/doctor-BP8cZ-7F.js.map +1 -0
- package/dist/eject-DizdoecV.js +335 -0
- package/dist/eject-DizdoecV.js.map +1 -0
- package/dist/fold-static-BC9CYicw.js +201 -0
- package/dist/fold-static-BC9CYicw.js.map +1 -0
- package/dist/fold-static.d.ts +26 -3
- package/dist/function-portability-BIvlvR9q.js +265 -0
- package/dist/function-portability-BIvlvR9q.js.map +1 -0
- package/dist/generate_sdk-BPqdJc3Q.js +489 -0
- package/dist/generate_sdk-BPqdJc3Q.js.map +1 -0
- package/dist/index.es.js +199 -22095
- package/dist/index.es.js.map +1 -1
- package/dist/init-Utstvh2Q.js +1149 -0
- package/dist/init-Utstvh2Q.js.map +1 -0
- package/dist/last-dev-database-B58g89uA.js +2 -0
- package/dist/manifest-DRp0UVTj.js +880 -0
- package/dist/manifest-DRp0UVTj.js.map +1 -0
- package/dist/manifest.d.ts +56 -4
- package/dist/normalize-imports-X0Tgze9y.js +107 -0
- package/dist/normalize-imports-X0Tgze9y.js.map +1 -0
- package/dist/package-manager-DSEOwTqM.js +193 -0
- package/dist/package-manager-DSEOwTqM.js.map +1 -0
- package/dist/path-flags-BFgG9V6y.js +91 -0
- package/dist/path-flags-BFgG9V6y.js.map +1 -0
- package/dist/precompress.d.ts +17 -0
- package/dist/prepare-CDN6lSpz.js +1236 -0
- package/dist/prepare-CDN6lSpz.js.map +1 -0
- package/dist/{project-3WCbrrfW.js → project-Wp2zoyeN.js} +34 -184
- package/dist/project-Wp2zoyeN.js.map +1 -0
- package/dist/pull-CrGYMEsy.js +458 -0
- package/dist/pull-CrGYMEsy.js.map +1 -0
- package/dist/resolve-B2O-wKKk.js +2 -0
- package/dist/resources-FD7AfZx8.js +183 -0
- package/dist/resources-FD7AfZx8.js.map +1 -0
- package/dist/schema-DMdr431S.js +125 -0
- package/dist/schema-DMdr431S.js.map +1 -0
- package/dist/skills-DTFibT1m.js +642 -0
- package/dist/skills-DTFibT1m.js.map +1 -0
- package/dist/start-DEa-hOBh.js +267 -0
- package/dist/start-DEa-hOBh.js.map +1 -0
- package/dist/{state-C59Elrnt.js → state-Do7ZJYkF.js} +49 -4
- package/dist/state-Do7ZJYkF.js.map +1 -0
- package/dist/status-DxnJTekv.js +352 -0
- package/dist/status-DxnJTekv.js.map +1 -0
- package/dist/storage-DyVq-f_F.js +303 -0
- package/dist/storage-DyVq-f_F.js.map +1 -0
- package/dist/telemetry/identity.d.ts +10 -1
- package/dist/telemetry-C8Uq8phb.js +129 -0
- package/dist/telemetry-C8Uq8phb.js.map +1 -0
- package/dist/telemetry-EMFRVRQi.js +351 -0
- package/dist/telemetry-EMFRVRQi.js.map +1 -0
- package/dist/unknown-command-DNwDcX3w.js +98 -0
- package/dist/unknown-command-DNwDcX3w.js.map +1 -0
- package/dist/upgrade-CtZtRkTE.js +1332 -0
- package/dist/upgrade-CtZtRkTE.js.map +1 -0
- package/dist/upgrade.d.ts +38 -5
- package/dist/utils/dev-preflight.d.ts +47 -5
- package/dist/utils/dev-server.d.ts +30 -0
- package/dist/utils/path-flags.d.ts +31 -0
- package/dist/utils/project.d.ts +16 -0
- package/dist/utils/temp-archive.d.ts +9 -0
- package/dist/version-Buyyo1ut.js +58 -0
- package/dist/version-Buyyo1ut.js.map +1 -0
- package/package.json +7 -7
- package/templates/eject/Dockerfile +4 -2
- package/templates/eject/backend/src/env.ts +1 -1
- package/templates/eject/backend/src/index.ts +42 -4
- package/templates/eject/docker-compose.custom.yml +8 -4
- package/templates/generated-schemas/blank.ts +40 -0
- package/templates/generated-schemas/blog.ts +127 -0
- package/templates/generated-schemas/ecommerce.ts +93 -0
- package/templates/overlays/baas/ai-instructions.md +1 -1
- package/templates/overlays/baas/config/package.json +1 -0
- package/templates/overlays/baas/config/storage.ts +5 -3
- package/templates/overlays/baas/package.json +1 -0
- package/templates/template/.cursorrules +1 -1
- package/templates/template/.env.example +20 -10
- package/templates/template/.github/copilot-instructions.md +1 -1
- package/templates/template/.mcp.json +2 -2
- package/templates/template/.windsurfrules +1 -1
- package/templates/template/AGENTS.md +1 -1
- package/templates/template/CLAUDE.md +1 -1
- package/templates/template/README.md +21 -15
- package/templates/template/ai-instructions.md +5 -4
- package/templates/template/backend/functions/hello.ts +8 -0
- package/templates/template/backend/package.json +1 -1
- package/templates/template/config/resources.ts +7 -6
- package/templates/template/config/storage.ts +5 -3
- package/templates/template/docker-compose.yml +15 -9
- package/templates/template/frontend/vite.config.ts +13 -3
- package/templates/template/gitignore +6 -1
- package/templates/template/package.json +5 -3
- package/templates/template/scripts/example.ts +6 -4
- package/templates/template/tsconfig.json +25 -0
- package/dist/branch-pointer-dBiClJ0p.js +0 -111
- package/dist/branch-pointer-dBiClJ0p.js.map +0 -1
- package/dist/constraints-DKGbNfUW.js +0 -48
- package/dist/constraints-DKGbNfUW.js.map +0 -1
- package/dist/daemon-DR9Tk0La.js +0 -355
- package/dist/daemon-DR9Tk0La.js.map +0 -1
- package/dist/daemon-entry-LTFKtpHy.js.map +0 -1
- package/dist/dev-preflight-CNLl4rdo.js +0 -367
- package/dist/dev-preflight-CNLl4rdo.js.map +0 -1
- package/dist/project-3WCbrrfW.js.map +0 -1
- package/dist/pull-Dywm5FWP.js +0 -232
- package/dist/pull-Dywm5FWP.js.map +0 -1
- package/dist/resolve-BAeRUBhg.js +0 -74
- package/dist/resolve-BAeRUBhg.js.map +0 -1
- package/dist/state-C59Elrnt.js.map +0 -1
- package/templates/template/backend/src/schema.generated.ts +0 -13
package/dist/pull-Dywm5FWP.js
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { execa } from "execa";
|
|
2
|
-
//#region src/dev-db/pull.ts
|
|
3
|
-
/**
|
|
4
|
-
* `rebase db pull` — copy a database's contents into local development.
|
|
5
|
-
*
|
|
6
|
-
* The common case is production into local, and the reason it exists is that
|
|
7
|
-
* the alternative is worse: without it people hand-roll a `pg_dump | psql` and
|
|
8
|
-
* get the flags wrong in ways that either fail loudly at 2am or, more often,
|
|
9
|
-
* quietly restore half a schema.
|
|
10
|
-
*
|
|
11
|
-
* Three things this command insists on, because copying a production database
|
|
12
|
-
* onto a laptop is a data-protection event whether or not anyone calls it one:
|
|
13
|
-
*
|
|
14
|
-
* 1. **It says what it is about to do, in full, before doing it** — which
|
|
15
|
-
* database it will read, which one it will overwrite, and where the data will
|
|
16
|
-
* come to rest on disk. The target path matters: people forget that
|
|
17
|
-
* `.rebase/pgdata` is a directory their backup software may be indexing.
|
|
18
|
-
*
|
|
19
|
-
* 2. **It refuses to run unattended without being told to.** The target is
|
|
20
|
-
* destroyed, so a mistyped `--from` with no confirmation would take the
|
|
21
|
-
* developer's working database with it.
|
|
22
|
-
*
|
|
23
|
-
* 3. **It will not write to a remote database.** The target is always the local
|
|
24
|
-
* development database; there is no flag that makes this push. A tool that
|
|
25
|
-
* can copy in both directions eventually copies in the wrong one.
|
|
26
|
-
*
|
|
27
|
-
* Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not
|
|
28
|
-
* an obviously safe one — the flag nobody types is the flag nobody gets. It is
|
|
29
|
-
* a best-effort pass over columns whose *names* look like personal data, and
|
|
30
|
-
* {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in
|
|
31
|
-
* a column called `notes`, and this file says so rather than implying a
|
|
32
|
-
* guarantee it cannot keep.
|
|
33
|
-
*/
|
|
34
|
-
/**
|
|
35
|
-
* Column-name patterns the anonymizer overwrites.
|
|
36
|
-
*
|
|
37
|
-
* Names, not contents: inspecting values would be slower, and would still miss
|
|
38
|
-
* the same things. This is a reasonable-effort measure for making a local copy
|
|
39
|
-
* less dangerous, and it is not a compliance control.
|
|
40
|
-
*/
|
|
41
|
-
var ANONYMIZE_PATTERNS = [
|
|
42
|
-
{
|
|
43
|
-
pattern: /^(.*_)?e?mail(_.*)?$/i,
|
|
44
|
-
replacement: "concat('user', id::text, '@example.invalid')"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i,
|
|
48
|
-
replacement: "'+10000000000'"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
pattern: /^(.*_)?(first_name|last_name|full_name|surname|given_name)(_.*)?$/i,
|
|
52
|
-
replacement: "'Redacted'"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i,
|
|
56
|
-
replacement: "'Redacted'"
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i,
|
|
60
|
-
replacement: "'REDACTED'"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i,
|
|
64
|
-
replacement: "'REDACTED'"
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i,
|
|
68
|
-
replacement: "'REDACTED'"
|
|
69
|
-
}
|
|
70
|
-
];
|
|
71
|
-
function shouldAnonymize(columnName) {
|
|
72
|
-
return ANONYMIZE_PATTERNS.some((rule) => rule.pattern.test(columnName));
|
|
73
|
-
}
|
|
74
|
-
function replacementFor(columnName) {
|
|
75
|
-
return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName))?.replacement ?? null;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Anonymizable columns: name looks personal, and the type can hold the
|
|
79
|
-
* replacement.
|
|
80
|
-
*
|
|
81
|
-
* The type check is what stops this generating `UPDATE … SET user_id =
|
|
82
|
-
* 'Redacted'` for an integer column called `user_id_email_seq` and failing the
|
|
83
|
-
* whole pass on a technicality.
|
|
84
|
-
*/
|
|
85
|
-
function anonymizableColumns(columns) {
|
|
86
|
-
const textual = /* @__PURE__ */ new Set([
|
|
87
|
-
"text",
|
|
88
|
-
"character varying",
|
|
89
|
-
"varchar",
|
|
90
|
-
"character",
|
|
91
|
-
"char",
|
|
92
|
-
"citext"
|
|
93
|
-
]);
|
|
94
|
-
return columns.filter((column) => shouldAnonymize(column.column) && textual.has(column.dataType.toLowerCase()));
|
|
95
|
-
}
|
|
96
|
-
/** `UPDATE` statements for one anonymization pass, in a stable order. */
|
|
97
|
-
function anonymizeStatements(columns) {
|
|
98
|
-
const byTable = /* @__PURE__ */ new Map();
|
|
99
|
-
for (const column of anonymizableColumns(columns)) {
|
|
100
|
-
const key = `${column.schema}.${column.table}`;
|
|
101
|
-
byTable.set(key, [...byTable.get(key) ?? [], column]);
|
|
102
|
-
}
|
|
103
|
-
return [...byTable.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([table, cols]) => {
|
|
104
|
-
const assignments = [...cols].sort((a, b) => a.column.localeCompare(b.column)).map((column) => `"${column.column}" = ${replacementFor(column.column)}`).join(", ");
|
|
105
|
-
return `UPDATE ${table.split(".").map((part) => `"${part}"`).join(".")} SET ${assignments};`;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Schemas the restored copy needs the app role provisioned on.
|
|
110
|
-
*
|
|
111
|
-
* `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives
|
|
112
|
-
* with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no
|
|
113
|
-
* privileges behind them. Measured on a 30-table project: 68 policies restored,
|
|
114
|
-
* 14 tables with RLS on, and **0** grants to `rebase_user` — where the source
|
|
115
|
-
* had 60. Reading one table as the role Rebase serves every request through:
|
|
116
|
-
*
|
|
117
|
-
* source: 6
|
|
118
|
-
* copy: ERROR: permission denied for table leads
|
|
119
|
-
*
|
|
120
|
-
* The dump flags are right and stay: without `--no-owner`/`--no-privileges`
|
|
121
|
-
* every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails
|
|
122
|
-
* against roles that do not exist on a laptop, and buries the real output. The
|
|
123
|
-
* repair belongs after the restore, and belongs to `ensureAppRole` — the same
|
|
124
|
-
* routine boot and `rebase db push` call — rather than to a second list of
|
|
125
|
-
* grants written here.
|
|
126
|
-
*
|
|
127
|
-
* Which schemas: every non-system schema the restored database actually has.
|
|
128
|
-
* Boot knows its `managedSchemas` from the collections; a restore knows only
|
|
129
|
-
* what arrived, and a source may carry schemas this project does not declare.
|
|
130
|
-
* `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to
|
|
131
|
-
* grant on and PostgreSQL would refuse anyway.
|
|
132
|
-
*/
|
|
133
|
-
function provisionableSchemas(rows) {
|
|
134
|
-
const skip = (name) => name === "information_schema" || name.startsWith("pg_");
|
|
135
|
-
return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();
|
|
136
|
-
}
|
|
137
|
-
/** Host and database of a connection string, with no credentials in it. */
|
|
138
|
-
function describeTarget(connectionString) {
|
|
139
|
-
try {
|
|
140
|
-
const url = new URL(connectionString);
|
|
141
|
-
const database = url.pathname.replace(/^\//, "") || "(default)";
|
|
142
|
-
return `${url.hostname}${url.port ? `:${url.port}` : ""}/${database}`;
|
|
143
|
-
} catch {
|
|
144
|
-
return "(unparseable connection string)";
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* `pg_dump` arguments for the source.
|
|
149
|
-
*
|
|
150
|
-
* `--no-owner` and `--no-acl` because the roles on a production server do not
|
|
151
|
-
* exist locally, and without them every `ALTER … OWNER TO` in the dump fails and
|
|
152
|
-
* buries the real output in noise. `--format=custom` so `pg_restore` can be told
|
|
153
|
-
* to continue past errors selectively rather than all-or-nothing.
|
|
154
|
-
*/
|
|
155
|
-
function dumpArgs(plan) {
|
|
156
|
-
const args = [
|
|
157
|
-
"--format=custom",
|
|
158
|
-
"--no-owner",
|
|
159
|
-
"--no-acl",
|
|
160
|
-
"--no-privileges"
|
|
161
|
-
];
|
|
162
|
-
for (const schema of plan.schemas) args.push("--schema", schema);
|
|
163
|
-
args.push("--dbname", plan.source);
|
|
164
|
-
return args;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* `pg_restore` arguments for the target.
|
|
168
|
-
*
|
|
169
|
-
* `--clean --if-exists` because a pull replaces what is there: restoring into a
|
|
170
|
-
* database that already has the tables would otherwise fail on every one of
|
|
171
|
-
* them. `--no-owner` for the same reason as the dump.
|
|
172
|
-
*/
|
|
173
|
-
function restoreArgs(plan, dumpFile) {
|
|
174
|
-
return [
|
|
175
|
-
"--clean",
|
|
176
|
-
"--if-exists",
|
|
177
|
-
"--no-owner",
|
|
178
|
-
"--no-privileges",
|
|
179
|
-
"--dbname",
|
|
180
|
-
plan.target,
|
|
181
|
-
dumpFile
|
|
182
|
-
];
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Is this PostgreSQL client tool on PATH, and what version? Both are checked
|
|
186
|
-
* before anything destructive: discovering `pg_restore` is missing after the
|
|
187
|
-
* dump has been taken — or after `--clean` has emptied the target — is the
|
|
188
|
-
* worst possible ordering.
|
|
189
|
-
*/
|
|
190
|
-
async function findPgTool(tool) {
|
|
191
|
-
try {
|
|
192
|
-
const { stdout } = await execa(tool, ["--version"]);
|
|
193
|
-
return stdout.trim();
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* What a finished `pg_restore` says about the copy.
|
|
200
|
-
*
|
|
201
|
-
* `--clean` empties the target before anything is written, so every way this
|
|
202
|
-
* can end short of exit 0 leaves a partial or empty local database, and none of
|
|
203
|
-
* them may be reported as a copy. A non-zero exit is pg_restore saying it
|
|
204
|
-
* skipped statements — "errors ignored on restore: N" — and whatever those
|
|
205
|
-
* statements created (a table, its rows, an RLS policy) is missing locally.
|
|
206
|
-
*
|
|
207
|
-
* `started` says whether anything can have arrived, which is what decides
|
|
208
|
-
* whether an anonymization pass still has data to redact.
|
|
209
|
-
*/
|
|
210
|
-
function restoreOutcome(run) {
|
|
211
|
-
if (run.signal) return {
|
|
212
|
-
started: true,
|
|
213
|
-
failure: `pg_restore was stopped by ${run.signal} before it finished.`
|
|
214
|
-
};
|
|
215
|
-
if (run.exitCode === void 0) return {
|
|
216
|
-
started: false,
|
|
217
|
-
failure: `pg_restore could not be started (${run.code ?? "unknown error"}).`
|
|
218
|
-
};
|
|
219
|
-
if (run.exitCode === 0) return {
|
|
220
|
-
started: true,
|
|
221
|
-
failure: null
|
|
222
|
-
};
|
|
223
|
-
const ignored = /errors ignored on restore: (\d+)/.exec(run.stderr ?? "");
|
|
224
|
-
return {
|
|
225
|
-
started: true,
|
|
226
|
-
failure: ignored ? `pg_restore could not restore ${ignored[1]} object(s) — the errors are above.` : `pg_restore exited with code ${run.exitCode}.`
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
//#endregion
|
|
230
|
-
export { anonymizeStatements, describeTarget, dumpArgs, findPgTool, provisionableSchemas, restoreArgs, restoreOutcome };
|
|
231
|
-
|
|
232
|
-
//# sourceMappingURL=pull-Dywm5FWP.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pull-Dywm5FWP.js","names":[],"sources":["../src/dev-db/pull.ts"],"sourcesContent":["/**\n * `rebase db pull` — copy a database's contents into local development.\n *\n * The common case is production into local, and the reason it exists is that\n * the alternative is worse: without it people hand-roll a `pg_dump | psql` and\n * get the flags wrong in ways that either fail loudly at 2am or, more often,\n * quietly restore half a schema.\n *\n * Three things this command insists on, because copying a production database\n * onto a laptop is a data-protection event whether or not anyone calls it one:\n *\n * 1. **It says what it is about to do, in full, before doing it** — which\n * database it will read, which one it will overwrite, and where the data will\n * come to rest on disk. The target path matters: people forget that\n * `.rebase/pgdata` is a directory their backup software may be indexing.\n *\n * 2. **It refuses to run unattended without being told to.** The target is\n * destroyed, so a mistyped `--from` with no confirmation would take the\n * developer's working database with it.\n *\n * 3. **It will not write to a remote database.** The target is always the local\n * development database; there is no flag that makes this push. A tool that\n * can copy in both directions eventually copies in the wrong one.\n *\n * Anonymization is opt-in (`--anonymize`), which is a deliberate choice and not\n * an obviously safe one — the flag nobody types is the flag nobody gets. It is\n * a best-effort pass over columns whose *names* look like personal data, and\n * {@link ANONYMIZE_PATTERNS} says exactly which. It cannot find personal data in\n * a column called `notes`, and this file says so rather than implying a\n * guarantee it cannot keep.\n */\n\nimport { execa } from \"execa\";\n\n/**\n * Column-name patterns the anonymizer overwrites.\n *\n * Names, not contents: inspecting values would be slower, and would still miss\n * the same things. This is a reasonable-effort measure for making a local copy\n * less dangerous, and it is not a compliance control.\n */\nexport const ANONYMIZE_PATTERNS: readonly { pattern: RegExp; replacement: string }[] = [\n { pattern: /^(.*_)?e?mail(_.*)?$/i, replacement: \"concat('user', id::text, '@example.invalid')\" },\n { pattern: /^(.*_)?(phone|mobile|tel|telephone)(_.*)?$/i, replacement: \"'+10000000000'\" },\n { pattern: /^(.*_)?(first_name|last_name|full_name|surname|given_name)(_.*)?$/i, replacement: \"'Redacted'\" },\n { pattern: /^(.*_)?(address|street|postcode|zip|zipcode)(_.*)?$/i, replacement: \"'Redacted'\" },\n { pattern: /^(.*_)?(ssn|tax_id|national_id|passport)(_.*)?$/i, replacement: \"'REDACTED'\" },\n { pattern: /^(.*_)?(password|password_hash|secret|token|api_key|access_token|refresh_token)(_.*)?$/i, replacement: \"'REDACTED'\" },\n { pattern: /^(.*_)?(ip|ip_address|user_agent)(_.*)?$/i, replacement: \"'REDACTED'\" }\n];\n\nexport function shouldAnonymize(columnName: string): boolean {\n return ANONYMIZE_PATTERNS.some((rule) => rule.pattern.test(columnName));\n}\n\nexport function replacementFor(columnName: string): string | null {\n return ANONYMIZE_PATTERNS.find((rule) => rule.pattern.test(columnName))?.replacement ?? null;\n}\n\n/** A text-ish column the anonymizer can overwrite without a type error. */\nexport interface ColumnRef {\n schema: string;\n table: string;\n column: string;\n dataType: string;\n}\n\n/**\n * Anonymizable columns: name looks personal, and the type can hold the\n * replacement.\n *\n * The type check is what stops this generating `UPDATE … SET user_id =\n * 'Redacted'` for an integer column called `user_id_email_seq` and failing the\n * whole pass on a technicality.\n */\nexport function anonymizableColumns(columns: readonly ColumnRef[]): ColumnRef[] {\n const textual = new Set([\"text\", \"character varying\", \"varchar\", \"character\", \"char\", \"citext\"]);\n\n return columns.filter((column) => shouldAnonymize(column.column) && textual.has(column.dataType.toLowerCase()));\n}\n\n/** `UPDATE` statements for one anonymization pass, in a stable order. */\nexport function anonymizeStatements(columns: readonly ColumnRef[]): string[] {\n const byTable = new Map<string, ColumnRef[]>();\n for (const column of anonymizableColumns(columns)) {\n const key = `${column.schema}.${column.table}`;\n byTable.set(key, [...(byTable.get(key) ?? []), column]);\n }\n\n return [...byTable.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([table, cols]) => {\n const assignments = [...cols]\n .sort((a, b) => a.column.localeCompare(b.column))\n .map((column) => `\"${column.column}\" = ${replacementFor(column.column)}`)\n .join(\", \");\n\n return `UPDATE ${table.split(\".\").map((part) => `\"${part}\"`).join(\".\")} SET ${assignments};`;\n });\n}\n\n/**\n * Schemas the restored copy needs the app role provisioned on.\n *\n * `pg_dump --no-privileges` strips every GRANT, so a pulled database arrives\n * with its RLS policies and its `FORCE ROW LEVEL SECURITY` intact and no\n * privileges behind them. Measured on a 30-table project: 68 policies restored,\n * 14 tables with RLS on, and **0** grants to `rebase_user` — where the source\n * had 60. Reading one table as the role Rebase serves every request through:\n *\n * source: 6\n * copy: ERROR: permission denied for table leads\n *\n * The dump flags are right and stay: without `--no-owner`/`--no-privileges`\n * every `ALTER … OWNER TO` and `GRANT … TO <prod role>` in the dump fails\n * against roles that do not exist on a laptop, and buries the real output. The\n * repair belongs after the restore, and belongs to `ensureAppRole` — the same\n * routine boot and `rebase db push` call — rather than to a second list of\n * grants written here.\n *\n * Which schemas: every non-system schema the restored database actually has.\n * Boot knows its `managedSchemas` from the collections; a restore knows only\n * what arrived, and a source may carry schemas this project does not declare.\n * `pg_catalog`, `information_schema` and the `pg_*` internals are never ours to\n * grant on and PostgreSQL would refuse anyway.\n */\nexport function provisionableSchemas(rows: readonly { schema: string }[]): string[] {\n const skip = (name: string) => name === \"information_schema\" || name.startsWith(\"pg_\");\n\n return [...new Set(rows.map((row) => row.schema))].filter((name) => !skip(name)).sort();\n}\n\n/** Host and database of a connection string, with no credentials in it. */\nexport function describeTarget(connectionString: string): string {\n try {\n const url = new URL(connectionString);\n const database = url.pathname.replace(/^\\//, \"\") || \"(default)\";\n\n return `${url.hostname}${url.port ? `:${url.port}` : \"\"}/${database}`;\n } catch {\n // Never echo the raw string: it carries a password, and this line is\n // printed to a terminal people paste into issues.\n return \"(unparseable connection string)\";\n }\n}\n\nexport interface PullPlan {\n /** Where the data comes from. */\n source: string;\n /** Where it lands. Always local. */\n target: string;\n anonymize: boolean;\n /** Schemas to copy. Empty means every non-system schema. */\n schemas: string[];\n}\n\n/**\n * `pg_dump` arguments for the source.\n *\n * `--no-owner` and `--no-acl` because the roles on a production server do not\n * exist locally, and without them every `ALTER … OWNER TO` in the dump fails and\n * buries the real output in noise. `--format=custom` so `pg_restore` can be told\n * to continue past errors selectively rather than all-or-nothing.\n */\nexport function dumpArgs(plan: PullPlan): string[] {\n const args = [\"--format=custom\", \"--no-owner\", \"--no-acl\", \"--no-privileges\"];\n for (const schema of plan.schemas) args.push(\"--schema\", schema);\n args.push(\"--dbname\", plan.source);\n\n return args;\n}\n\n/**\n * `pg_restore` arguments for the target.\n *\n * `--clean --if-exists` because a pull replaces what is there: restoring into a\n * database that already has the tables would otherwise fail on every one of\n * them. `--no-owner` for the same reason as the dump.\n */\nexport function restoreArgs(plan: PullPlan, dumpFile: string): string[] {\n return [\"--clean\", \"--if-exists\", \"--no-owner\", \"--no-privileges\", \"--dbname\", plan.target, dumpFile];\n}\n\n/**\n * Is this PostgreSQL client tool on PATH, and what version? Both are checked\n * before anything destructive: discovering `pg_restore` is missing after the\n * dump has been taken — or after `--clean` has emptied the target — is the\n * worst possible ordering.\n */\nexport async function findPgTool(tool: \"pg_dump\" | \"pg_restore\"): Promise<string | null> {\n try {\n const { stdout } = await execa(tool, [\"--version\"]);\n\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\n/** The fields of a finished `pg_restore` run that decide {@link restoreOutcome}. */\nexport interface RestoreRun {\n exitCode?: number;\n signal?: string;\n /** A spawn failure's code — `ENOENT` when the binary vanished. */\n code?: string;\n stderr?: string;\n}\n\n/**\n * What a finished `pg_restore` says about the copy.\n *\n * `--clean` empties the target before anything is written, so every way this\n * can end short of exit 0 leaves a partial or empty local database, and none of\n * them may be reported as a copy. A non-zero exit is pg_restore saying it\n * skipped statements — \"errors ignored on restore: N\" — and whatever those\n * statements created (a table, its rows, an RLS policy) is missing locally.\n *\n * `started` says whether anything can have arrived, which is what decides\n * whether an anonymization pass still has data to redact.\n */\nexport function restoreOutcome(run: RestoreRun): { started: boolean; failure: string | null } {\n if (run.signal) {\n return { started: true, failure: `pg_restore was stopped by ${run.signal} before it finished.` };\n }\n if (run.exitCode === undefined) {\n return { started: false, failure: `pg_restore could not be started (${run.code ?? \"unknown error\"}).` };\n }\n if (run.exitCode === 0) return { started: true, failure: null };\n\n const ignored = /errors ignored on restore: (\\d+)/.exec(run.stderr ?? \"\");\n\n return {\n started: true,\n failure: ignored\n ? `pg_restore could not restore ${ignored[1]} object(s) — the errors are above.`\n : `pg_restore exited with code ${run.exitCode}.`\n };\n}\n"],"mappings":"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"}
|
package/dist/resolve-BAeRUBhg.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
//#region src/dev-db/resolve.ts
|
|
3
|
-
var resolve_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
-
describeDevDatabase: () => describeDevDatabase,
|
|
5
|
-
resolveDevDatabase: () => resolveDevDatabase
|
|
6
|
-
});
|
|
7
|
-
/**
|
|
8
|
-
* A value that is present but empty is treated as absent.
|
|
9
|
-
*
|
|
10
|
-
* `DATABASE_URL=` in a `.env` is what a developer writes when they mean "not
|
|
11
|
-
* this one" — honouring it literally would hand an empty connection string to
|
|
12
|
-
* libpq and produce an error about a missing host, which explains nothing.
|
|
13
|
-
*/
|
|
14
|
-
function present(value) {
|
|
15
|
-
if (typeof value !== "string") return null;
|
|
16
|
-
const trimmed = value.trim();
|
|
17
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
18
|
-
}
|
|
19
|
-
function resolveDevDatabase(input = {}) {
|
|
20
|
-
const flagUrl = present(input.flagUrl);
|
|
21
|
-
if (flagUrl) return {
|
|
22
|
-
kind: "external",
|
|
23
|
-
url: flagUrl,
|
|
24
|
-
source: "flag"
|
|
25
|
-
};
|
|
26
|
-
const fromEnvironment = present(input.env?.DATABASE_URL);
|
|
27
|
-
if (fromEnvironment) return {
|
|
28
|
-
kind: "external",
|
|
29
|
-
url: fromEnvironment,
|
|
30
|
-
source: "environment"
|
|
31
|
-
};
|
|
32
|
-
const fromBranch = present(input.branch?.url);
|
|
33
|
-
if (fromBranch && input.branch) return {
|
|
34
|
-
kind: "external",
|
|
35
|
-
url: fromBranch,
|
|
36
|
-
source: "branch",
|
|
37
|
-
branch: input.branch.name
|
|
38
|
-
};
|
|
39
|
-
const fromEnvFile = present(input.envFile?.DATABASE_URL);
|
|
40
|
-
if (fromEnvFile) return {
|
|
41
|
-
kind: "external",
|
|
42
|
-
url: fromEnvFile,
|
|
43
|
-
source: "env-file"
|
|
44
|
-
};
|
|
45
|
-
if (input.flagDocker) return {
|
|
46
|
-
kind: "docker",
|
|
47
|
-
source: "docker",
|
|
48
|
-
url: present(input.composeUrl)
|
|
49
|
-
};
|
|
50
|
-
return {
|
|
51
|
-
kind: "managed",
|
|
52
|
-
source: "managed"
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/** One line for the startup banner, naming both the database and why. */
|
|
56
|
-
function describeDevDatabase(database) {
|
|
57
|
-
switch (database.kind) {
|
|
58
|
-
case "external":
|
|
59
|
-
switch (database.source) {
|
|
60
|
-
case "flag": return "your database (--database-url)";
|
|
61
|
-
case "environment": return "your database (DATABASE_URL in the environment)";
|
|
62
|
-
case "env-file": return "your database (DATABASE_URL in .env)";
|
|
63
|
-
case "branch": return `branch "${database.branch}" (rebase db branch switch)`;
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
case "docker": return "Postgres in Docker";
|
|
67
|
-
case "managed": return "the managed development database (PGlite)";
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
|
-
export { resolveDevDatabase as n, resolve_exports as r, describeDevDatabase as t };
|
|
73
|
-
|
|
74
|
-
//# sourceMappingURL=resolve-BAeRUBhg.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-BAeRUBhg.js","names":[],"sources":["../src/dev-db/resolve.ts"],"sourcesContent":["/**\n * Which database a command should talk to, decided in one place.\n *\n * Before this existed every command that needed Postgres read `DATABASE_URL`\n * for itself, which was fine while there was exactly one answer. Introducing a\n * managed database makes the question real: a project may have no\n * `DATABASE_URL` at all and still expect `rebase db push` to work, and a\n * project that *does* set one must never be quietly redirected somewhere else.\n *\n * So the rule is ordered and boring, and the order is the promise:\n *\n * 1. `--database-url <url>` — said on this command line, wins over everything\n * 2. `DATABASE_URL` in the shell environment\n * 3. the database branch this checkout is switched to\n * 4. `DATABASE_URL` in the project's `.env`\n * 5. `--docker`\n * 6. the managed PGlite database\n *\n * An explicit connection string always wins. That is the whole point of the\n * override: someone pointing Rebase at their own Postgres — a colleague's\n * staging box, a Neon branch, a container they manage — must get exactly that,\n * with no cleverness in between. The managed database is what fills the vacuum\n * when nobody has said anything, and it is the only case where the CLI picks.\n *\n * The branch at (3) is the one addition that is not an explicit connection\n * string, and it sits where it does deliberately. It has to outrank `.env` or\n * `rebase db branch switch` would silently do nothing on every project that\n * sets `DATABASE_URL` — which is every project not on the managed database.\n * It must not outrank (1) or (2), because a flag on this command line and a\n * variable in this shell are more immediate than a switch made yesterday.\n *\n * {@link resolveDevDatabase} is pure: inputs in, decision out, no filesystem\n * and no process. Reading `.env`, reading the branch pointer and starting a\n * daemon happen elsewhere, so the ordering above can be tested without any of\n * them.\n */\n\n/** Where the answer came from. Carried so diagnostics can name it. */\nexport type DevDatabaseSource =\n /** `--database-url` on the command line. */\n | \"flag\"\n /** `DATABASE_URL` in the shell environment. */\n | \"environment\"\n /** `DATABASE_URL` in the project's `.env`. */\n | \"env-file\"\n /** The branch this checkout is switched to, over the base connection. */\n | \"branch\"\n /** `--docker`. */\n | \"docker\"\n /** Nobody said anything, so the managed database fills in. */\n | \"managed\";\n\nexport type DevDatabase =\n | {\n kind: \"external\";\n /** The connection string, exactly as given. Never rewritten. */\n url: string;\n source: Extract<DevDatabaseSource, \"flag\" | \"environment\" | \"env-file\" | \"branch\">;\n /** The branch name, when this checkout is switched to one. */\n branch?: string;\n }\n | {\n kind: \"docker\";\n source: \"docker\";\n /**\n * The compose `db` service's connection string, derived by the caller.\n *\n * Null when the project has no compose file to derive one from, which\n * is a `--docker` that cannot be honoured rather than a fallback: the\n * caller says so instead of guessing at `localhost:5432`.\n */\n url: string | null;\n }\n | {\n kind: \"managed\";\n source: \"managed\";\n };\n\nexport interface ResolveDevDatabaseInput {\n /** `--database-url <url>`, if given. */\n flagUrl?: string | null;\n /** `--docker`, if given. */\n flagDocker?: boolean;\n /** The shell environment. Only `DATABASE_URL` is read. */\n env?: Record<string, string | undefined>;\n /** Parsed `.env` from the project root. Only `DATABASE_URL` is read. */\n envFile?: Record<string, string> | null;\n /**\n * The compose `db` service's connection string, when the project has one.\n *\n * Passed in rather than read here so this function stays pure. Only\n * consulted for the Docker case; an explicit `DATABASE_URL` still wins.\n */\n composeUrl?: string | null;\n /**\n * The branch this checkout is switched to, already resolved to a URL.\n *\n * Resolved by the caller rather than here so this stays pure: deriving it\n * needs the base connection string, which needs the `.env` this function is\n * handed rather than reads.\n */\n branch?: { name: string; url: string } | null;\n}\n\n/**\n * A value that is present but empty is treated as absent.\n *\n * `DATABASE_URL=` in a `.env` is what a developer writes when they mean \"not\n * this one\" — honouring it literally would hand an empty connection string to\n * libpq and produce an error about a missing host, which explains nothing.\n */\nfunction present(value: string | undefined | null): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n\n return trimmed.length > 0 ? trimmed : null;\n}\n\nexport function resolveDevDatabase(input: ResolveDevDatabaseInput = {}): DevDatabase {\n const flagUrl = present(input.flagUrl);\n if (flagUrl) return { kind: \"external\", url: flagUrl, source: \"flag\" };\n\n const fromEnvironment = present(input.env?.DATABASE_URL);\n if (fromEnvironment) return { kind: \"external\", url: fromEnvironment, source: \"environment\" };\n\n // Above `.env`, below anything said explicitly — see the ordering note at\n // the top of this file.\n const fromBranch = present(input.branch?.url);\n if (fromBranch && input.branch) {\n return { kind: \"external\", url: fromBranch, source: \"branch\", branch: input.branch.name };\n }\n\n const fromEnvFile = present(input.envFile?.DATABASE_URL);\n if (fromEnvFile) return { kind: \"external\", url: fromEnvFile, source: \"env-file\" };\n\n // Only consulted once every explicit connection string is exhausted: asking\n // for Docker is a choice about *how to get* a database, not which one, so a\n // DATABASE_URL that already names one outranks it.\n if (input.flagDocker) {\n return { kind: \"docker\", source: \"docker\", url: present(input.composeUrl) };\n }\n\n return { kind: \"managed\", source: \"managed\" };\n}\n\n/** One line for the startup banner, naming both the database and why. */\nexport function describeDevDatabase(database: DevDatabase): string {\n switch (database.kind) {\n case \"external\":\n switch (database.source) {\n case \"flag\":\n return \"your database (--database-url)\";\n case \"environment\":\n return \"your database (DATABASE_URL in the environment)\";\n case \"env-file\":\n return \"your database (DATABASE_URL in .env)\";\n case \"branch\":\n return `branch \"${database.branch}\" (rebase db branch switch)`;\n }\n break;\n case \"docker\":\n return \"Postgres in Docker\";\n case \"managed\":\n return \"the managed development database (PGlite)\";\n }\n\n // Unreachable while the union is exhaustive; kept so a future variant fails\n // loudly in review rather than printing \"undefined\" to a user.\n throw new Error(`Unhandled dev database: ${JSON.stringify(database)}`);\n}\n"],"mappings":";;;;;;;;;;;;;AA+GA,SAAS,QAAQ,OAAiD;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,MAAM,UAAU,MAAM,KAAK;CAE3B,OAAO,QAAQ,SAAS,IAAI,UAAU;AAC1C;AAEA,SAAgB,mBAAmB,QAAiC,CAAC,GAAgB;CACjF,MAAM,UAAU,QAAQ,MAAM,OAAO;CACrC,IAAI,SAAS,OAAO;EAAE,MAAM;EAAY,KAAK;EAAS,QAAQ;CAAO;CAErE,MAAM,kBAAkB,QAAQ,MAAM,KAAK,YAAY;CACvD,IAAI,iBAAiB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAiB,QAAQ;CAAc;CAI5F,MAAM,aAAa,QAAQ,MAAM,QAAQ,GAAG;CAC5C,IAAI,cAAc,MAAM,QACpB,OAAO;EAAE,MAAM;EAAY,KAAK;EAAY,QAAQ;EAAU,QAAQ,MAAM,OAAO;CAAK;CAG5F,MAAM,cAAc,QAAQ,MAAM,SAAS,YAAY;CACvD,IAAI,aAAa,OAAO;EAAE,MAAM;EAAY,KAAK;EAAa,QAAQ;CAAW;CAKjF,IAAI,MAAM,YACN,OAAO;EAAE,MAAM;EAAU,QAAQ;EAAU,KAAK,QAAQ,MAAM,UAAU;CAAE;CAG9E,OAAO;EAAE,MAAM;EAAW,QAAQ;CAAU;AAChD;;AAGA,SAAgB,oBAAoB,UAA+B;CAC/D,QAAQ,SAAS,MAAjB;EACI,KAAK;GACD,QAAQ,SAAS,QAAjB;IACI,KAAK,QACD,OAAO;IACX,KAAK,eACD,OAAO;IACX,KAAK,YACD,OAAO;IACX,KAAK,UACD,OAAO,WAAW,SAAS,OAAO;GAC1C;GACA;EACJ,KAAK,UACD,OAAO;EACX,KAAK,WACD,OAAO;CACf;CAIA,MAAM,IAAI,MAAM,2BAA2B,KAAK,UAAU,QAAQ,GAAG;AACzE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"state-C59Elrnt.js","names":[],"sources":["../src/dev-db/state.ts"],"sourcesContent":["/**\n * The managed database's state file, and the rules for trusting it.\n *\n * The daemon outlives the command that started it — `rebase db push` in one\n * terminal and `rebase dev` in another have to reach the *same* PGlite, because\n * two processes opening one data directory would corrupt it. So the daemon\n * records where it is, and every command reads that record.\n *\n * A record on disk is a claim, not a fact. The process it names may have been\n * killed, the machine may have rebooted and handed the pid to something else,\n * and the port may now belong to a stranger. {@link readState} therefore only\n * parses; deciding whether a record is live is {@link isDaemonAlive}'s job, and\n * it asks the daemon rather than the operating system.\n */\n\nimport fs from \"fs\";\nimport { resourceEnvSuffix } from \"@rebasepro/types\";\nimport net from \"net\";\nimport path from \"path\";\n\n/** Everything under here is generated and gitignored. */\nexport const DEV_DB_DIR = \".rebase\";\n/** PGlite's own data directory. Deleting it is what `--reset` means. */\nexport const DATA_DIR_NAME = \"pgdata\";\n/** The record the daemon writes once it is accepting connections. */\nexport const STATE_FILE_NAME = \"pglite.json\";\n/**\n * Held by whoever is currently starting a daemon.\n *\n * Without it, `rebase dev` and `rebase db push` started in the same second both\n * see no state file, both spawn, and two processes open one PGlite data\n * directory — the exact corruption the single-daemon design exists to prevent.\n * Observed as `ENOTEMPTY` during cleanup, which is the harmless way for it to\n * show up; the harmful way is a damaged database.\n */\nexport const START_LOCK_NAME = \"starting.lock\";\n\nexport interface DaemonState {\n /** TCP port the socket server is listening on, chosen when it started. */\n port: number;\n /** The daemon process. Used only as a fast negative check. */\n pid: number;\n /** Absolute path of the PGlite data directory this daemon has open. */\n dataDir: string;\n /** ISO timestamp, for diagnostics. */\n startedAt: string;\n /**\n * A random token the daemon also answers with over the wire, on\n * {@link identityPort}.\n *\n * Without it, \"is the daemon alive?\" degrades to \"is something listening on\n * that port?\", which is a different question and answers yes for whatever\n * process happened to take the port after a reboot. Rebase would then send\n * migrations to a stranger.\n */\n token: string;\n /** Loopback port that answers the identity check. */\n identityPort: number;\n /**\n * The additional databases this daemon serves, by declared key.\n *\n * One PGlite instance each — PGlite is one database per instance, so\n * `database(\"analytics\")` is a second data directory and a second port,\n * not a `CREATE DATABASE`. Started on demand when a command asks for the\n * key over the identity socket, and recorded here so `rebase status` and\n * a reset can see them. The default database is the fields above.\n */\n databases?: Record<string, { port: number; dataDir: string }>;\n}\n\nexport function devDbDir(projectRoot: string): string {\n return path.join(projectRoot, DEV_DB_DIR);\n}\n\nexport function dataDir(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), DATA_DIR_NAME);\n}\n\n/**\n * The data directory for an additional declared database.\n *\n * `pgdata__analytics`: the key spelled the way its variable is, so the\n * directory beside `pgdata` and `DATABASE_URL__ANALYTICS` are visibly the\n * same thing.\n */\nexport function additionalDataDir(projectRoot: string, key: string): string {\n return path.join(devDbDir(projectRoot), `${DATA_DIR_NAME}${resourceEnvSuffix(key).toLowerCase()}`);\n}\n\nexport function stateFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), STATE_FILE_NAME);\n}\n\nexport function startLockFile(projectRoot: string): string {\n return path.join(devDbDir(projectRoot), START_LOCK_NAME);\n}\n\n/**\n * Take the start lock, or report that somebody else holds it.\n *\n * `wx` is the whole mechanism: create-if-absent is a single atomic syscall, so\n * exactly one of two racing processes can succeed no matter how close together\n * they arrive.\n *\n * A lock older than `staleAfterMs` is broken rather than waited on — the holder\n * may have been killed between creating it and starting anything, and a\n * developer should never have to know this file exists in order to unstick\n * their project.\n */\nexport function acquireStartLock(projectRoot: string, staleAfterMs: number): boolean {\n const target = startLockFile(projectRoot);\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n\n const attempt = (): boolean => {\n try {\n const handle = fs.openSync(target, \"wx\");\n fs.writeSync(handle, `${process.pid} ${new Date().toISOString()}\\n`);\n fs.closeSync(handle);\n\n return true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n\n return false;\n }\n };\n\n if (attempt()) return true;\n\n try {\n // Clamped at zero: a filesystem whose timestamp granularity rounds the\n // mtime *up* reports a negative age for a lock created moments ago,\n // and a negative age is below every threshold — so `staleAfterMs: 0`,\n // which means \"break any lock\", would refuse to break one.\n const age = Math.max(0, Date.now() - fs.statSync(target).mtimeMs);\n if (age < staleAfterMs) return false;\n fs.unlinkSync(target);\n } catch {\n // Vanished under us, which means the holder finished. Either way the\n // next attempt is the answer.\n }\n\n return attempt();\n}\n\nexport function releaseStartLock(projectRoot: string): void {\n try {\n fs.unlinkSync(startLockFile(projectRoot));\n } catch {\n // Already released is the desired end state.\n }\n}\n\n/**\n * Parse the record, or `null` for anything that is not one.\n *\n * Every failure is the same answer — absent — because every failure has the\n * same remedy: start a daemon. A corrupt state file is not worth an error\n * message to a user who never wrote it.\n */\nexport function readState(projectRoot: string): DaemonState | null {\n let raw: string;\n try {\n raw = fs.readFileSync(stateFile(projectRoot), \"utf8\");\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as Partial<DaemonState>;\n if (\n typeof parsed.port !== \"number\" ||\n !Number.isInteger(parsed.port) ||\n parsed.port <= 0 ||\n parsed.port > 65535 ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.dataDir !== \"string\" ||\n typeof parsed.token !== \"string\" ||\n parsed.token.length === 0 ||\n typeof parsed.identityPort !== \"number\" ||\n !Number.isInteger(parsed.identityPort) ||\n parsed.identityPort <= 0 ||\n parsed.identityPort > 65535\n ) {\n return null;\n }\n\n // Only well-formed entries survive: a malformed one is treated as\n // absent, and the next ensure asks the daemon for it again.\n const databases: Record<string, { port: number; dataDir: string }> = {};\n if (parsed.databases && typeof parsed.databases === \"object\") {\n for (const [key, entry] of Object.entries(parsed.databases)) {\n if (\n entry && typeof entry.port === \"number\" && Number.isInteger(entry.port)\n && entry.port > 0 && entry.port <= 65535 && typeof entry.dataDir === \"string\"\n ) {\n databases[key] = { port: entry.port, dataDir: entry.dataDir };\n }\n }\n }\n\n return {\n port: parsed.port,\n pid: parsed.pid,\n dataDir: parsed.dataDir,\n startedAt: typeof parsed.startedAt === \"string\" ? parsed.startedAt : \"\",\n token: parsed.token,\n identityPort: parsed.identityPort,\n ...(Object.keys(databases).length > 0 ? { databases } : {})\n };\n } catch {\n return null;\n }\n}\n\nexport function writeState(projectRoot: string, state: DaemonState): void {\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n // Written whole then moved, so a reader never sees half a record — commands\n // poll this file while the daemon is starting.\n const target = stateFile(projectRoot);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\\n`, \"utf8\");\n fs.renameSync(temporary, target);\n}\n\nexport function clearState(projectRoot: string): void {\n try {\n fs.unlinkSync(stateFile(projectRoot));\n } catch {\n // Already gone is the desired end state.\n }\n}\n\n/** Is *some* process with this pid running? A fast, cheap negative check. */\nexport function pidRunning(pid: number): boolean {\n try {\n // Signal 0 performs the permission and existence checks without\n // delivering anything.\n process.kill(pid, 0);\n\n return true;\n } catch (error) {\n // EPERM means it exists and belongs to someone else, which for our\n // purposes is still \"running\".\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** Can a TCP connection be opened to this port on loopback? */\nexport function portAccepting(port: number, timeoutMs = 1000): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = new net.Socket();\n const settle = (answer: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(answer);\n };\n socket.setTimeout(timeoutMs);\n socket.once(\"connect\", () => settle(true));\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.connect(port, \"127.0.0.1\");\n });\n}\n\n/**\n * Ask a port for a free one, then hand back the number.\n *\n * Deliberately not the probe `rebase init` uses: that one has a documented\n * failure where a port is free to probe and unusable to publish. This binds on\n * loopback only, which is also where the daemon listens, so a port that binds\n * here binds there.\n */\nexport function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n server.close(() => reject(new Error(\"Could not determine a free port.\")));\n\n return;\n }\n const { port } = address;\n server.close(() => resolve(port));\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// This file is generated from config/collections/. Do not edit manually.
|
|
2
|
-
//
|
|
3
|
-
// `rebase dev` regenerates it before it starts anything, so the stub below is
|
|
4
|
-
// replaced on your first run and you should not have to think about this file.
|
|
5
|
-
// To regenerate it by hand: `pnpm run schema:generate`.
|
|
6
|
-
//
|
|
7
|
-
// Not `db:generate` — that one plans an Atlas migration and needs a real
|
|
8
|
-
// PostgreSQL. `schema:generate` only reads your collections and writes this
|
|
9
|
-
// file, so it works on any database, including the managed development one.
|
|
10
|
-
|
|
11
|
-
export const tables = {};
|
|
12
|
-
export const enums = {};
|
|
13
|
-
export const relations = {};
|