hazo_env 0.4.0 → 0.6.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.
- package/CHANGE_LOG.md +15 -0
- package/README.md +41 -0
- package/config/hazo_env_config.ini.sample +18 -0
- package/dist/cli.js +41 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/migrate/clear.d.ts +3 -0
- package/dist/migrate/clear.d.ts.map +1 -0
- package/dist/migrate/clear.js +117 -0
- package/dist/migrate/db-dump-restore.d.ts +24 -0
- package/dist/migrate/db-dump-restore.d.ts.map +1 -0
- package/dist/migrate/db-dump-restore.js +72 -0
- package/dist/migrate/db.d.ts +1 -0
- package/dist/migrate/db.d.ts.map +1 -1
- package/dist/migrate/db.postgrest.d.ts.map +1 -1
- package/dist/migrate/db.postgrest.js +3 -1
- package/dist/migrate/files.d.ts +2 -1
- package/dist/migrate/files.d.ts.map +1 -1
- package/dist/migrate/files.js +61 -0
- package/dist/migrate/run.d.ts.map +1 -1
- package/dist/migrate/run.js +34 -11
- package/dist/migrate/ssh-exec.d.ts +30 -0
- package/dist/migrate/ssh-exec.d.ts.map +1 -0
- package/dist/migrate/ssh-exec.js +82 -0
- package/dist/migrate/transport.d.ts +1 -1
- package/dist/migrate/transport.d.ts.map +1 -1
- package/dist/migrate/transport.js +4 -9
- package/dist/resolve/backup.d.ts +23 -0
- package/dist/resolve/backup.d.ts.map +1 -0
- package/dist/resolve/backup.js +52 -0
- package/dist/resolve/ssh.d.ts +15 -0
- package/dist/resolve/ssh.d.ts.map +1 -0
- package/dist/resolve/ssh.js +36 -0
- package/dist/types/index.d.ts +22 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGE_LOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# hazo_env — Change Log
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `clearEnv()` — delete all non-preserved tables via PostgREST (reverse FK order) + optional file-dir wipe. Exported from `src/index.ts`. CLI: `hazo-env clear <env>` (prod-target guard: `--allow-prod-target --confirm=<token>`).
|
|
7
|
+
- `ClearRequest` / `ClearResult` types in `src/types/index.ts`; `'clear'` added to `MigrationProgress.phase` union.
|
|
8
|
+
- rsync-over-SSH file transport — `copyFiles()` auto-dispatches to rsync when `[transport.ssh.<fromEnv>]` is configured. `copyFilesRsync()` in `src/migrate/files.ts` spawns `rsync -az --partial --info=progress2`, parses `to-chk=N/TOTAL` for live percent. `onProgress` signature extended with optional `percent` arg.
|
|
9
|
+
- `resolveSshConfig(env)` in `src/resolve/ssh.ts` — reads `[transport.ssh.<env>]` section (host/user/key/path) with `${VAR}` expansion.
|
|
10
|
+
- `resolveTransport()` return type widened to `'local' | 'api' | 'rsync'`; `'ssh'` input now returns `'rsync'` instead of throwing.
|
|
11
|
+
- Per-table DB progress — `DbCopyOptions.onTableProgress` callback fires once per table during PostgREST copy pass; `run.ts` maps it to the 20→70% band with a `"table N/M: <name>"` message.
|
|
12
|
+
- `[transport.ssh.<env>]` section added to `config/hazo_env_config.ini.sample`.
|
|
13
|
+
|
|
14
|
+
### Test-app
|
|
15
|
+
- `POST /api/clear` route added (mirrors `/api/migrate`).
|
|
16
|
+
- `'clear-env'` autotest scenario added (dry-run guard: verifies prod-target refuses without confirm).
|
|
17
|
+
|
|
3
18
|
## 0.4.0 — 2026-06-25
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -235,6 +235,47 @@ Key sections:
|
|
|
235
235
|
|
|
236
236
|
Secret placeholders use `${ENV_VAR_NAME}` syntax — hazo_env substitutes them from `.env.local` at runtime.
|
|
237
237
|
|
|
238
|
+
## dump-restore transport
|
|
239
|
+
|
|
240
|
+
The `dump-restore` transport copies a database via SSH `pg_dump`/`pg_restore` (binary format). Use it when the source and target share the same Postgres server (e.g. prod→dev on the same VPS) and you want a fast binary copy without going through the PostgREST row-copy loop.
|
|
241
|
+
|
|
242
|
+
### Config: `[backup.<env>]`
|
|
243
|
+
|
|
244
|
+
Add a section to `config/hazo_env_config.ini`:
|
|
245
|
+
|
|
246
|
+
```ini
|
|
247
|
+
[backup.dev]
|
|
248
|
+
ssh_env = prod ; reuse [transport.ssh.prod] SSH connection
|
|
249
|
+
target_db = myapp_dev ; DB to drop/recreate
|
|
250
|
+
owner = adminuser ; createdb -O <owner>
|
|
251
|
+
dump_source = /var/backups/myapp-*.pgdump ; glob (newest used) or "fresh"
|
|
252
|
+
fresh_dump_db = myapp ; source DB for pg_dump when dump_source=fresh
|
|
253
|
+
pre_restore_cmd = sudo systemctl stop myapp-postgrest-dev
|
|
254
|
+
post_restore_cmd= sudo systemctl start myapp-postgrest-dev
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
> **No PII scrubbing on this path.** The binary `pg_restore` copies raw rows; hazo_env's `scrubHook` only runs inside the PostgREST row-copy loop. If you need scrubbed data in dev, use the default `api` transport instead.
|
|
258
|
+
|
|
259
|
+
### CLI
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npx hazo-env migrate --from prod --to dev --transport dump-restore
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Programmatic API
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { resolveBackupConfig, restoreDbViaDump } from 'hazo_env';
|
|
269
|
+
|
|
270
|
+
const backup = resolveBackupConfig('dev'); // reads [backup.dev] from INI
|
|
271
|
+
if (backup) {
|
|
272
|
+
const result = await restoreDbViaDump(backup, {
|
|
273
|
+
onProgress: (msg) => console.log(msg),
|
|
274
|
+
});
|
|
275
|
+
// result.mode === 'dump-restore'
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
238
279
|
## License
|
|
239
280
|
|
|
240
281
|
MIT
|
|
@@ -62,3 +62,21 @@ location = remote
|
|
|
62
62
|
|
|
63
63
|
[host.prod]
|
|
64
64
|
location = remote
|
|
65
|
+
|
|
66
|
+
; ─── SSH transport for cross-host file rsync ─────────────────────────────────
|
|
67
|
+
; When [transport.ssh.<env>] is present, copyFiles will use rsync-over-SSH
|
|
68
|
+
; to pull files from that environment's file server instead of local copy.
|
|
69
|
+
; This enables prod→dev migration when both envs run on separate hosts.
|
|
70
|
+
|
|
71
|
+
[transport.ssh.prod]
|
|
72
|
+
; SSH connection to the prod file server (used when pulling prod→dev).
|
|
73
|
+
; The worker runs rsync FROM prod TO dev — prod stays read-only.
|
|
74
|
+
host = ${SSH_HOST_PROD}
|
|
75
|
+
user = ${SSH_USER_PROD}
|
|
76
|
+
key = ${SSH_KEY_PATH_PROD}
|
|
77
|
+
path = /home/pubs/kinstripe/var/uploads
|
|
78
|
+
; Optional: pin the server's host key (output of `ssh-keyscan <host>` minus the
|
|
79
|
+
; leading hostname, e.g. "ssh-ed25519 AAAA..."). When set, host-key checking is
|
|
80
|
+
; strict and pinned — no trust-on-first-use. When omitted, the first connection
|
|
81
|
+
; is accepted (accept-new) and persisted to config/hazo_env_known_hosts.
|
|
82
|
+
; host_key = ${SSH_HOST_KEY_PROD}
|
package/dist/cli.js
CHANGED
|
@@ -81,6 +81,8 @@ async function runMigrate() {
|
|
|
81
81
|
const tablesIdx = args.findIndex((a) => a === '--tables');
|
|
82
82
|
const tablesArg = tablesIdx >= 0 ? args[tablesIdx + 1] : undefined;
|
|
83
83
|
const tables = tablesArg ? tablesArg.split(',').map((t) => t.trim()) : undefined;
|
|
84
|
+
const transportIdx = args.findIndex((a) => a === '--transport');
|
|
85
|
+
const transport = transportIdx >= 0 ? args[transportIdx + 1] : undefined;
|
|
84
86
|
if (!from || !to) {
|
|
85
87
|
console.error(pc.red('Error: migrate requires --from <env> and --to <env>'));
|
|
86
88
|
process.exit(1);
|
|
@@ -96,6 +98,7 @@ async function runMigrate() {
|
|
|
96
98
|
dryRun,
|
|
97
99
|
allowProdTarget,
|
|
98
100
|
confirmToken,
|
|
101
|
+
transport: transport,
|
|
99
102
|
onProgress: (p) => {
|
|
100
103
|
const pct = p.percent != null ? `${p.percent}%`.padStart(4) + ' ' : ' ';
|
|
101
104
|
console.log(` ${pct}${pc.dim(p.phase.padEnd(12))} ${p.message}`);
|
|
@@ -167,6 +170,39 @@ async function runRestore() {
|
|
|
167
170
|
restoreSnapshot(toConfig, snapshotId);
|
|
168
171
|
console.log(` ${pc.green('✓')} Restored\n`);
|
|
169
172
|
}
|
|
173
|
+
async function runClear() {
|
|
174
|
+
const targetEnv = args[0] ?? getEnv();
|
|
175
|
+
const allowProd = args.includes('--allow-prod-target');
|
|
176
|
+
const confirm = args.find((a) => a.startsWith('--confirm='))?.split('=')[1];
|
|
177
|
+
console.log(`\n${pc.bold('hazo-env clear')} ${pc.dim(targetEnv)}\n`);
|
|
178
|
+
const { clearEnv } = await import('./migrate/clear.js');
|
|
179
|
+
try {
|
|
180
|
+
let lastMsg = '';
|
|
181
|
+
const result = await clearEnv({
|
|
182
|
+
env: targetEnv,
|
|
183
|
+
allowProdTarget: allowProd,
|
|
184
|
+
confirmToken: confirm,
|
|
185
|
+
onProgress: (p) => {
|
|
186
|
+
if (p.message !== lastMsg) {
|
|
187
|
+
const pctStr = p.percent != null ? ` [${p.percent}%]` : '';
|
|
188
|
+
console.log(` ${pc.dim('·')} ${p.message}${pctStr}`);
|
|
189
|
+
lastMsg = p.message;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
if (result.ok) {
|
|
194
|
+
console.log(` ${pc.green('✓')} Cleared ${result.tablesCleared} tables${result.filesCleared ? ' + files' : ''} in ${result.durationMs}ms\n`);
|
|
195
|
+
if (result.warnings.length)
|
|
196
|
+
result.warnings.forEach((w) => console.log(` ${pc.yellow('⚠')} ${w}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
201
|
+
console.error(`\n ${pc.red('✗')} ${msg}\n`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
console.log('');
|
|
205
|
+
}
|
|
170
206
|
async function runMask() {
|
|
171
207
|
const subCmd = args[0];
|
|
172
208
|
if (subCmd === 'sync') {
|
|
@@ -239,6 +275,8 @@ Usage:
|
|
|
239
275
|
hazo-env snapshot <env> Take a snapshot of an environment
|
|
240
276
|
hazo-env mask sync Load masking ruleset from INI into DB
|
|
241
277
|
hazo-env mask list List registered mask transforms
|
|
278
|
+
hazo-env clear <env> Wipe DB tables + files for an environment
|
|
279
|
+
--allow-prod-target --confirm=<token> Override prod-target safety
|
|
242
280
|
`);
|
|
243
281
|
}
|
|
244
282
|
(async () => {
|
|
@@ -264,6 +302,9 @@ Usage:
|
|
|
264
302
|
else if (command === 'mask') {
|
|
265
303
|
await runMask();
|
|
266
304
|
}
|
|
305
|
+
else if (command === 'clear') {
|
|
306
|
+
await runClear();
|
|
307
|
+
}
|
|
267
308
|
else {
|
|
268
309
|
printHelp();
|
|
269
310
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './resolve/connect.js';
|
|
|
6
6
|
export * from './resolve/files.js';
|
|
7
7
|
export * from './doctor.js';
|
|
8
8
|
export { runMigration } from './migrate/run.js';
|
|
9
|
+
export { clearEnv } from './migrate/clear.js';
|
|
9
10
|
export { verifyFiles } from './migrate/verify.js';
|
|
10
11
|
export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
|
|
11
12
|
export { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
|
|
@@ -14,4 +15,8 @@ export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.j
|
|
|
14
15
|
export type { MaskRule } from './mask/ruleset.js';
|
|
15
16
|
export type { MaskTransform } from './types/index.js';
|
|
16
17
|
export * from './lib/index.js';
|
|
18
|
+
export { resolveBackupConfig } from './resolve/backup.js';
|
|
19
|
+
export type { BackupConfig } from './resolve/backup.js';
|
|
20
|
+
export { runSsh, assertSafeSshField } from './migrate/ssh-exec.js';
|
|
21
|
+
export { restoreDbViaDump } from './migrate/db-dump-restore.js';
|
|
17
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEvG,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE9G,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACnF,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEvG,cAAc,kBAAkB,CAAC;AAEjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,aAAa,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE9G,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACnF,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './resolve/files.js';
|
|
|
13
13
|
export * from './doctor.js';
|
|
14
14
|
// Migration engine (Phase 2)
|
|
15
15
|
export { runMigration } from './migrate/run.js';
|
|
16
|
+
export { clearEnv } from './migrate/clear.js';
|
|
16
17
|
export { verifyFiles } from './migrate/verify.js';
|
|
17
18
|
export { takeSnapshot, restoreSnapshot } from './migrate/snapshot.js';
|
|
18
19
|
export { writeMigrationProgress, readMigrationProgress, clearMigrationProgress } from './migrate/progress.js';
|
|
@@ -22,3 +23,7 @@ export { registerMask } from './mask/registry.js';
|
|
|
22
23
|
export { loadRuleset, syncRulesetFromIni, parseIniRules } from './mask/ruleset.js';
|
|
23
24
|
// Lib
|
|
24
25
|
export * from './lib/index.js';
|
|
26
|
+
// dump-restore transport
|
|
27
|
+
export { resolveBackupConfig } from './resolve/backup.js';
|
|
28
|
+
export { runSsh, assertSafeSshField } from './migrate/ssh-exec.js';
|
|
29
|
+
export { restoreDbViaDump } from './migrate/db-dump-restore.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clear.d.ts","sourceRoot":"","sources":["../../src/migrate/clear.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AAYtF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAoHtE"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// hazo_env/src/migrate/clear.ts — clearEnv: wipe DB tables + files for a target environment
|
|
2
|
+
import { HazoError, optional_import } from 'hazo_core';
|
|
3
|
+
import { getEnvRole } from '../env.server.js';
|
|
4
|
+
import { resolveMigrateConfig } from '../resolve/migrate.js';
|
|
5
|
+
import { resolveConnectConfig } from '../resolve/connect.js';
|
|
6
|
+
import { resolveFilesRoot } from '../resolve/files.js';
|
|
7
|
+
import { writeMigrationProgress } from './progress.js';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
function emitProgress(req, p) {
|
|
10
|
+
req.onProgress?.(p);
|
|
11
|
+
if (req.jobId && req.progressDir) {
|
|
12
|
+
writeMigrationProgress(req.progressDir, req.jobId, p);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function clearEnv(req) {
|
|
16
|
+
const startMs = Date.now();
|
|
17
|
+
const warnings = [];
|
|
18
|
+
// Guard: refuse production target unless explicitly unlocked with a confirm token
|
|
19
|
+
const role = getEnvRole(req.env);
|
|
20
|
+
if (role === 'production' && !req.allowProdTarget) {
|
|
21
|
+
throw new HazoError({
|
|
22
|
+
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
23
|
+
pkg: 'hazo_env',
|
|
24
|
+
message: `Refusing to clear production environment "${req.env}". Pass allowProdTarget:true and a confirmToken to override.`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (role === 'production' && req.allowProdTarget && !req.confirmToken) {
|
|
28
|
+
throw new HazoError({
|
|
29
|
+
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
30
|
+
pkg: 'hazo_env',
|
|
31
|
+
message: 'Production target requires a confirmToken for safety.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
emitProgress(req, { phase: 'validate', message: 'Validating clear request', percent: 0 });
|
|
35
|
+
const migrateConfig = resolveMigrateConfig();
|
|
36
|
+
const { tables, preserve = [], pkOverrides = {} } = migrateConfig;
|
|
37
|
+
if (!tables || tables.length === 0) {
|
|
38
|
+
throw new HazoError({
|
|
39
|
+
code: 'HAZO_ENV_MISSING_TABLE_LIST',
|
|
40
|
+
pkg: 'hazo_env',
|
|
41
|
+
message: 'clearEnv requires an explicit table list. ' +
|
|
42
|
+
'Set [migrate] tables = ... in hazo_env_config.ini.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const tablesToClear = [...tables.filter((t) => !preserve.includes(t))].reverse();
|
|
46
|
+
const dbConfig = resolveConnectConfig({ env: req.env, allowOtherEnv: true });
|
|
47
|
+
if (dbConfig.type !== 'postgrest') {
|
|
48
|
+
throw new HazoError({
|
|
49
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
50
|
+
pkg: 'hazo_env',
|
|
51
|
+
message: `clearEnv only supports PostgREST connections (got "${dbConfig.type}").`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!dbConfig.postgrest) {
|
|
55
|
+
throw new HazoError({
|
|
56
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
57
|
+
pkg: 'hazo_env',
|
|
58
|
+
message: 'PostgREST config missing base_url/api_key.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const connect = await optional_import('hazo_connect/server');
|
|
62
|
+
if (!connect) {
|
|
63
|
+
throw new HazoError({
|
|
64
|
+
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
65
|
+
pkg: 'hazo_env',
|
|
66
|
+
message: 'hazo_connect is required for clearEnv. Install hazo_connect to continue.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const tgtAdapter = connect.createHazoConnect({
|
|
70
|
+
type: 'postgrest',
|
|
71
|
+
postgrest: {
|
|
72
|
+
base_url: dbConfig.postgrest.base_url,
|
|
73
|
+
api_key: dbConfig.postgrest.api_key,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
// DB clear (only if not explicitly disabled)
|
|
77
|
+
let tablesCleared = 0;
|
|
78
|
+
if (req.include?.db !== false) {
|
|
79
|
+
for (let idx = 0; idx < tablesToClear.length; idx++) {
|
|
80
|
+
const table = tablesToClear[idx];
|
|
81
|
+
const percent = Math.round(((idx + 1) / tablesToClear.length) * 90);
|
|
82
|
+
emitProgress(req, {
|
|
83
|
+
phase: 'clear',
|
|
84
|
+
message: `Clearing table ${idx + 1}/${tablesToClear.length}: ${table}`,
|
|
85
|
+
percent,
|
|
86
|
+
});
|
|
87
|
+
const pk = pkOverrides[table] ?? 'id';
|
|
88
|
+
try {
|
|
89
|
+
await tgtAdapter.rawQuery(`/${table}?${pk}=not.is.null`, { method: 'DELETE' });
|
|
90
|
+
tablesCleared++;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
warnings.push(`Warning: could not clear ${table}: ${msg}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// File clear
|
|
99
|
+
let filesCleared = false;
|
|
100
|
+
if (req.include?.files !== false) {
|
|
101
|
+
emitProgress(req, { phase: 'clear', message: 'Clearing files...', percent: 92 });
|
|
102
|
+
const filesRoot = resolveFilesRoot(req.env);
|
|
103
|
+
if (fs.existsSync(filesRoot)) {
|
|
104
|
+
fs.rmSync(filesRoot, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
fs.mkdirSync(filesRoot, { recursive: true });
|
|
107
|
+
filesCleared = true;
|
|
108
|
+
}
|
|
109
|
+
emitProgress(req, { phase: 'done', message: 'Clear complete', percent: 100 });
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
tablesCleared,
|
|
113
|
+
filesCleared,
|
|
114
|
+
durationMs: Date.now() - startMs,
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BackupConfig } from '../resolve/backup.js';
|
|
2
|
+
export interface DbDumpRestoreResult {
|
|
3
|
+
tables: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
scrubbed: number;
|
|
6
|
+
mode: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Restore a Postgres database on a remote server via SSH pg_dump/pg_restore.
|
|
10
|
+
*
|
|
11
|
+
* Steps executed remotely:
|
|
12
|
+
* 1. Obtain a dump file (fresh pg_dump or locate an existing .pgdump)
|
|
13
|
+
* 2. Run pre_restore_cmd (if configured)
|
|
14
|
+
* 3. dropdb --if-exists + createdb -O <owner>
|
|
15
|
+
* 4. pg_restore -d <target_db>
|
|
16
|
+
* 5. Run post_restore_cmd (if configured)
|
|
17
|
+
*
|
|
18
|
+
* Returns a DbDumpRestoreResult with mode: 'dump-restore' and zero
|
|
19
|
+
* row/table counts (binary restore does not report these).
|
|
20
|
+
*/
|
|
21
|
+
export declare function restoreDbViaDump(backup: BackupConfig, opts?: {
|
|
22
|
+
onProgress?: (msg: string) => void;
|
|
23
|
+
}): Promise<DbDumpRestoreResult>;
|
|
24
|
+
//# sourceMappingURL=db-dump-restore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-dump-restore.d.ts","sourceRoot":"","sources":["../../src/migrate/db-dump-restore.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC5C,OAAO,CAAC,mBAAmB,CAAC,CA6D9B"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// hazo_env/src/migrate/db-dump-restore.ts — DB copy via SSH pg_dump/pg_restore
|
|
2
|
+
//
|
|
3
|
+
// Builds a single remote bash script and runs it via runSsh.
|
|
4
|
+
// ⚠️ No PII scrubbing on this path — pg_restore copies raw binary rows.
|
|
5
|
+
// Use the default api transport when scrubbed data is required in dev.
|
|
6
|
+
import { resolveSshConfig } from '../resolve/ssh.js';
|
|
7
|
+
import { runSsh } from './ssh-exec.js';
|
|
8
|
+
/**
|
|
9
|
+
* Restore a Postgres database on a remote server via SSH pg_dump/pg_restore.
|
|
10
|
+
*
|
|
11
|
+
* Steps executed remotely:
|
|
12
|
+
* 1. Obtain a dump file (fresh pg_dump or locate an existing .pgdump)
|
|
13
|
+
* 2. Run pre_restore_cmd (if configured)
|
|
14
|
+
* 3. dropdb --if-exists + createdb -O <owner>
|
|
15
|
+
* 4. pg_restore -d <target_db>
|
|
16
|
+
* 5. Run post_restore_cmd (if configured)
|
|
17
|
+
*
|
|
18
|
+
* Returns a DbDumpRestoreResult with mode: 'dump-restore' and zero
|
|
19
|
+
* row/table counts (binary restore does not report these).
|
|
20
|
+
*/
|
|
21
|
+
export async function restoreDbViaDump(backup, opts) {
|
|
22
|
+
const ssh = resolveSshConfig(backup.ssh_env);
|
|
23
|
+
if (!ssh) {
|
|
24
|
+
throw new Error(`dump-restore: no [transport.ssh.${backup.ssh_env}] config found — ` +
|
|
25
|
+
`set ssh_env to a configured SSH transport in [backup.*]`);
|
|
26
|
+
}
|
|
27
|
+
// Build remote bash script lines
|
|
28
|
+
const lines = ['set -euo pipefail', ''];
|
|
29
|
+
// Step 1: Get or create dump
|
|
30
|
+
lines.push('# Step 1: Get or create dump');
|
|
31
|
+
if (backup.dump_source === 'fresh') {
|
|
32
|
+
lines.push('DUMP_FILE="/tmp/he-$(date +%s).pgdump"');
|
|
33
|
+
lines.push(`echo "Creating fresh dump of ${backup.fresh_dump_db}..."`);
|
|
34
|
+
lines.push(`pg_dump -Fc ${backup.fresh_dump_db} > "$DUMP_FILE"`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push(`echo "Locating dump file matching ${backup.dump_source}..."`);
|
|
38
|
+
lines.push(`DUMP_FILE=$(ls -t ${backup.dump_source} 2>/dev/null | head -1)`);
|
|
39
|
+
lines.push(`if [ -z "$DUMP_FILE" ]; then echo "No dump file found matching ${backup.dump_source}"; exit 1; fi`);
|
|
40
|
+
lines.push('echo "Using dump: $DUMP_FILE"');
|
|
41
|
+
}
|
|
42
|
+
lines.push('');
|
|
43
|
+
// Step 2: Pre-restore command (optional)
|
|
44
|
+
if (backup.pre_restore_cmd) {
|
|
45
|
+
lines.push('# Step 2: Pre-restore command');
|
|
46
|
+
lines.push('echo "Running pre-restore..."');
|
|
47
|
+
lines.push(backup.pre_restore_cmd);
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
// Step 3: Drop and recreate target DB
|
|
51
|
+
lines.push('# Step 3: Drop and recreate target DB');
|
|
52
|
+
lines.push(`echo "Dropping ${backup.target_db}..."`);
|
|
53
|
+
lines.push(`dropdb --if-exists ${backup.target_db}`);
|
|
54
|
+
lines.push(`echo "Creating ${backup.target_db}..."`);
|
|
55
|
+
lines.push(`createdb -O ${backup.owner} ${backup.target_db}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
// Step 4: Restore
|
|
58
|
+
lines.push('# Step 4: Restore');
|
|
59
|
+
lines.push(`echo "Restoring to ${backup.target_db}..."`);
|
|
60
|
+
lines.push(`pg_restore -d ${backup.target_db} "$DUMP_FILE"`);
|
|
61
|
+
lines.push('echo "Restore complete."');
|
|
62
|
+
// Step 5: Post-restore command (optional)
|
|
63
|
+
if (backup.post_restore_cmd) {
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('# Step 5: Post-restore command');
|
|
66
|
+
lines.push('echo "Running post-restore..."');
|
|
67
|
+
lines.push(backup.post_restore_cmd);
|
|
68
|
+
}
|
|
69
|
+
const script = lines.join('\n');
|
|
70
|
+
await runSsh(ssh, script, { onProgress: opts?.onProgress });
|
|
71
|
+
return { tables: 0, rows: 0, scrubbed: 0, mode: 'dump-restore' };
|
|
72
|
+
}
|
package/dist/migrate/db.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface DbCopyOptions {
|
|
|
6
6
|
pkOverrides?: Record<string, string>;
|
|
7
7
|
scrubHook?: (tableName: string, row: Record<string, unknown>) => Record<string, unknown>;
|
|
8
8
|
onProgress?: (msg: string) => void;
|
|
9
|
+
onTableProgress?: (tableIdx: number, totalTables: number, tableName: string) => void;
|
|
9
10
|
}
|
|
10
11
|
export interface DbCopyResult {
|
|
11
12
|
tables: number;
|
package/dist/migrate/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/migrate/db.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/migrate/db.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC9B,MAAM,CAAC,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACtF;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6BD,wBAAsB,MAAM,CAC1B,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAqHvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.postgrest.d.ts","sourceRoot":"","sources":["../../src/migrate/db.postgrest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM3D,wBAAsB,eAAe,CACnC,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"db.postgrest.d.ts","sourceRoot":"","sources":["../../src/migrate/db.postgrest.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM3D,wBAAsB,eAAe,CACnC,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA+GvB"}
|
|
@@ -43,7 +43,9 @@ export async function copyDbPostgrest(srcAdapter, tgtAdapter, opts = {}) {
|
|
|
43
43
|
let totalScrubbed = 0;
|
|
44
44
|
// --- Copy pass: forward dependency order ---
|
|
45
45
|
for (const table of tablesToProcess) {
|
|
46
|
-
|
|
46
|
+
const tableIdx = tablesToProcess.indexOf(table);
|
|
47
|
+
opts.onProgress?.(`Copying table ${tableIdx + 1}/${tablesToProcess.length}: ${table}`);
|
|
48
|
+
opts.onTableProgress?.(tableIdx + 1, tablesToProcess.length, table);
|
|
47
49
|
const pk = pkOverrides[table] ?? 'id';
|
|
48
50
|
const srcService = connect.createCrudService(srcAdapter, table, { autoId: false });
|
|
49
51
|
const tgtService = connect.createCrudService(tgtAdapter, table, { autoId: false });
|
package/dist/migrate/files.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type { HazoEnv } from '../types/index.js';
|
|
|
2
2
|
export interface FilesCopyOptions {
|
|
3
3
|
fromEnv: HazoEnv;
|
|
4
4
|
toEnv: HazoEnv;
|
|
5
|
+
transport?: 'local' | 'rsync';
|
|
5
6
|
fileScrubHook?: (filePath: string) => Promise<void>;
|
|
6
|
-
onProgress?: (msg: string) => void;
|
|
7
|
+
onProgress?: (msg: string, percent?: number) => void;
|
|
7
8
|
}
|
|
8
9
|
export interface FilesCopyResult {
|
|
9
10
|
copied: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/migrate/files.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/migrate/files.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAoFD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkDhF"}
|
package/dist/migrate/files.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// Failures are non-fatal warnings — the file copy still completes.
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
9
10
|
import { resolveFilesRoot } from '../resolve/files.js';
|
|
11
|
+
import { resolveSshConfig } from '../resolve/ssh.js';
|
|
12
|
+
import { assertSafeSshField, setupKnownHosts } from './ssh-exec.js';
|
|
10
13
|
function resolveEnvFilesRoot(env) {
|
|
11
14
|
return resolveFilesRoot(env);
|
|
12
15
|
}
|
|
@@ -29,7 +32,65 @@ async function scrubPdfFile(filePath) {
|
|
|
29
32
|
// Non-fatal — PDF scrubbing failure is a warning, not an abort
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
async function copyFilesRsync(opts, ssh) {
|
|
36
|
+
const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
|
|
37
|
+
fs.mkdirSync(tgtRoot, { recursive: true });
|
|
38
|
+
assertSafeSshField('user', ssh.user);
|
|
39
|
+
assertSafeSshField('host', ssh.host);
|
|
40
|
+
assertSafeSshField('key', ssh.key);
|
|
41
|
+
assertSafeSshField('path', ssh.path);
|
|
42
|
+
const { knownHosts, strict } = setupKnownHosts(ssh);
|
|
43
|
+
const remote = `${ssh.user}@${ssh.host}:${ssh.path}/`;
|
|
44
|
+
const sshCmd = `ssh -i ${ssh.key} -o StrictHostKeyChecking=${strict} -o UserKnownHostsFile=${knownHosts}`;
|
|
45
|
+
const args = ['-az', '--partial', '--info=progress2', '-e', sshCmd, remote, tgtRoot + '/'];
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const proc = spawn('rsync', args);
|
|
48
|
+
let totalFiles = 0;
|
|
49
|
+
let doneFiles = 0;
|
|
50
|
+
let totalBytes = 0;
|
|
51
|
+
proc.stdout.on('data', (chunk) => {
|
|
52
|
+
const text = chunk.toString();
|
|
53
|
+
// Parse --info=progress2: lines like " 117,504,614 100% 108.64MB/s 0:00:01 (xfr#123, to-chk=0/456)"
|
|
54
|
+
for (const line of text.split('\n')) {
|
|
55
|
+
// Extract to-chk=N/TOTAL
|
|
56
|
+
const chkMatch = line.match(/to-chk=(\d+)\/(\d+)/);
|
|
57
|
+
if (chkMatch) {
|
|
58
|
+
const remaining = parseInt(chkMatch[1], 10);
|
|
59
|
+
const total = parseInt(chkMatch[2], 10);
|
|
60
|
+
if (totalFiles === 0)
|
|
61
|
+
totalFiles = total;
|
|
62
|
+
doneFiles = total - remaining;
|
|
63
|
+
const pct = total > 0 ? Math.round((doneFiles / total) * 100) : 0;
|
|
64
|
+
opts.onProgress?.(`rsync: ${doneFiles}/${total} files (${pct}%)`, pct);
|
|
65
|
+
}
|
|
66
|
+
// Extract bytes: first number on lines with a %
|
|
67
|
+
const bytesMatch = line.match(/^\s+([\d,]+)\s+\d+%/);
|
|
68
|
+
if (bytesMatch) {
|
|
69
|
+
totalBytes = parseInt(bytesMatch[1].replace(/,/g, ''), 10);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
proc.stderr.on('data', (chunk) => {
|
|
74
|
+
opts.onProgress?.(`rsync: ${chunk.toString().trim()}`, undefined);
|
|
75
|
+
});
|
|
76
|
+
proc.on('error', (err) => reject(err));
|
|
77
|
+
proc.on('close', (code) => {
|
|
78
|
+
if (code !== 0) {
|
|
79
|
+
reject(new Error(`rsync exited with code ${code}`));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
resolve({ copied: doneFiles || totalFiles, bytes: totalBytes, placeholdered: 0 });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
32
87
|
export async function copyFiles(opts) {
|
|
88
|
+
const ssh = resolveSshConfig(opts.fromEnv);
|
|
89
|
+
if (opts.transport === 'rsync' || (opts.transport !== 'local' && ssh !== null)) {
|
|
90
|
+
if (!ssh)
|
|
91
|
+
throw new Error(`rsync requested but no [transport.ssh.${opts.fromEnv}] config found`);
|
|
92
|
+
return copyFilesRsync(opts, ssh);
|
|
93
|
+
}
|
|
33
94
|
const srcRoot = resolveEnvFilesRoot(opts.fromEnv);
|
|
34
95
|
const tgtRoot = resolveEnvFilesRoot(opts.toEnv);
|
|
35
96
|
if (!fs.existsSync(srcRoot)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AA0D3G,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AA0D3G,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyMlF"}
|
package/dist/migrate/run.js
CHANGED
|
@@ -172,17 +172,40 @@ export async function runMigration(req) {
|
|
|
172
172
|
// Step 5: DB copy
|
|
173
173
|
if (includeDb) {
|
|
174
174
|
emitProgress(req, { phase: 'db', message: 'Copying database tables', percent: 20 });
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
175
|
+
if (req.transport === 'dump-restore') {
|
|
176
|
+
const { resolveBackupConfig } = await import('../resolve/backup.js');
|
|
177
|
+
const { restoreDbViaDump } = await import('./db-dump-restore.js');
|
|
178
|
+
const backup = resolveBackupConfig(req.to);
|
|
179
|
+
if (!backup) {
|
|
180
|
+
throw new HazoError({
|
|
181
|
+
code: 'HAZO_ENV_INVALID_REQUEST',
|
|
182
|
+
pkg: 'hazo_env',
|
|
183
|
+
message: `[backup.${req.to}] config section is required for dump-restore transport`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
dbResult = await restoreDbViaDump(backup, {
|
|
187
|
+
onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
|
|
188
|
+
});
|
|
189
|
+
emitProgress(req, { phase: 'db', message: 'DB restore via dump complete', percent: 70 });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const scrubHook = await buildScrubHook(req.to, scrubMode, tgtAdapter);
|
|
193
|
+
const result = await copyDb(srcAdapter, tgtAdapter, {
|
|
194
|
+
type: fromDbConfig.type,
|
|
195
|
+
tables: req.tables ?? migrateConfig.tables,
|
|
196
|
+
preserve: migrateConfig.preserve,
|
|
197
|
+
pkOverrides: migrateConfig.pkOverrides,
|
|
198
|
+
scrubHook,
|
|
199
|
+
onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
|
|
200
|
+
onTableProgress: (idx, total, name) => emitProgress(req, {
|
|
201
|
+
phase: 'db',
|
|
202
|
+
message: `table ${idx}/${total}: ${name}`,
|
|
203
|
+
percent: 20 + Math.round((idx / total) * 50),
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
dbResult = result;
|
|
207
|
+
emitProgress(req, { phase: 'db', message: `DB copy complete: ${result.tables} tables, ${result.rows} rows`, percent: 70 });
|
|
208
|
+
}
|
|
186
209
|
}
|
|
187
210
|
// Step 6: File copy
|
|
188
211
|
if (includeFiles) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SshConfig } from '../resolve/ssh.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reject SSH config values that could smuggle extra SSH options through
|
|
4
|
+
* shell argument expansion. Values starting with '-' (e.g. -oProxyCommand=...)
|
|
5
|
+
* or containing whitespace would be re-split by SSH and treated as flags.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertSafeSshField(name: string, value: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Set up a persistent known_hosts file for SSH host-key pinning.
|
|
10
|
+
* When ssh.hostKey is set, the key is pinned and strict checking is enabled
|
|
11
|
+
* (no TOFU). Otherwise, accept-new is used — trust on first use, then persist.
|
|
12
|
+
*
|
|
13
|
+
* Returns { knownHosts: absolute path, strict: 'yes' | 'accept-new' }.
|
|
14
|
+
*/
|
|
15
|
+
export declare function setupKnownHosts(ssh: SshConfig): {
|
|
16
|
+
knownHosts: string;
|
|
17
|
+
strict: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Run a bash script on a remote host via SSH, piping remoteScript to stdin.
|
|
21
|
+
*
|
|
22
|
+
* Spawns: ssh -i <key> -o StrictHostKeyChecking=<strict>
|
|
23
|
+
* -o UserKnownHostsFile=<knownHosts> <user>@<host> bash -s
|
|
24
|
+
*
|
|
25
|
+
* Streams stdout/stderr lines to opts.onProgress. Rejects on non-zero exit.
|
|
26
|
+
*/
|
|
27
|
+
export declare function runSsh(ssh: SshConfig, remoteScript: string, opts?: {
|
|
28
|
+
onProgress?: (msg: string) => void;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
//# sourceMappingURL=ssh-exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-exec.d.ts","sourceRoot":"","sources":["../../src/migrate/ssh-exec.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAYtF;AAED;;;;;;;GAOG;AACH,wBAAsB,MAAM,CAC1B,GAAG,EAAE,SAAS,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAsCf"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// hazo_env/src/migrate/ssh-exec.ts — Shared SSH execution helpers
|
|
2
|
+
//
|
|
3
|
+
// Extracted from files.ts so that db-dump-restore.ts can reuse the same
|
|
4
|
+
// known_hosts pinning and safe-field validation without duplication.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Reject SSH config values that could smuggle extra SSH options through
|
|
10
|
+
* shell argument expansion. Values starting with '-' (e.g. -oProxyCommand=...)
|
|
11
|
+
* or containing whitespace would be re-split by SSH and treated as flags.
|
|
12
|
+
*/
|
|
13
|
+
export function assertSafeSshField(name, value) {
|
|
14
|
+
if (/^-/.test(value) || /\s/.test(value)) {
|
|
15
|
+
throw new Error(`Unsafe transport.ssh.${name} value: must not start with '-' or contain whitespace`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Set up a persistent known_hosts file for SSH host-key pinning.
|
|
20
|
+
* When ssh.hostKey is set, the key is pinned and strict checking is enabled
|
|
21
|
+
* (no TOFU). Otherwise, accept-new is used — trust on first use, then persist.
|
|
22
|
+
*
|
|
23
|
+
* Returns { knownHosts: absolute path, strict: 'yes' | 'accept-new' }.
|
|
24
|
+
*/
|
|
25
|
+
export function setupKnownHosts(ssh) {
|
|
26
|
+
const knownHosts = path.resolve(process.cwd(), 'config', 'hazo_env_known_hosts');
|
|
27
|
+
fs.mkdirSync(path.dirname(knownHosts), { recursive: true });
|
|
28
|
+
if (ssh.hostKey) {
|
|
29
|
+
// Pin the operator-provided host key; strict checking, no first-use prompt.
|
|
30
|
+
const line = `${ssh.host} ${ssh.hostKey}\n`;
|
|
31
|
+
const existing = fs.existsSync(knownHosts) ? fs.readFileSync(knownHosts, 'utf8') : '';
|
|
32
|
+
if (!existing.includes(line.trim()))
|
|
33
|
+
fs.appendFileSync(knownHosts, line);
|
|
34
|
+
}
|
|
35
|
+
// Pinned key → strict verify; otherwise accept-new — never disable host-key checking.
|
|
36
|
+
const strict = ssh.hostKey ? 'yes' : 'accept-new';
|
|
37
|
+
return { knownHosts, strict };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run a bash script on a remote host via SSH, piping remoteScript to stdin.
|
|
41
|
+
*
|
|
42
|
+
* Spawns: ssh -i <key> -o StrictHostKeyChecking=<strict>
|
|
43
|
+
* -o UserKnownHostsFile=<knownHosts> <user>@<host> bash -s
|
|
44
|
+
*
|
|
45
|
+
* Streams stdout/stderr lines to opts.onProgress. Rejects on non-zero exit.
|
|
46
|
+
*/
|
|
47
|
+
export async function runSsh(ssh, remoteScript, opts) {
|
|
48
|
+
const { knownHosts, strict } = setupKnownHosts(ssh);
|
|
49
|
+
const sshArgs = [
|
|
50
|
+
'-i', ssh.key,
|
|
51
|
+
'-o', `StrictHostKeyChecking=${strict}`,
|
|
52
|
+
'-o', `UserKnownHostsFile=${knownHosts}`,
|
|
53
|
+
`${ssh.user}@${ssh.host}`,
|
|
54
|
+
'bash', '-s',
|
|
55
|
+
];
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const proc = spawn('ssh', sshArgs);
|
|
58
|
+
proc.stdin.write(remoteScript);
|
|
59
|
+
proc.stdin.end();
|
|
60
|
+
proc.stdout.on('data', (chunk) => {
|
|
61
|
+
for (const line of chunk.toString().split('\n')) {
|
|
62
|
+
if (line.trim())
|
|
63
|
+
opts?.onProgress?.(line);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
proc.stderr.on('data', (chunk) => {
|
|
67
|
+
for (const line of chunk.toString().split('\n')) {
|
|
68
|
+
if (line.trim())
|
|
69
|
+
opts?.onProgress?.(line);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
proc.on('error', (err) => reject(err));
|
|
73
|
+
proc.on('close', (code) => {
|
|
74
|
+
if (code !== 0) {
|
|
75
|
+
reject(new Error(`ssh exited with code ${code}`));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
resolve();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { TransportMode, DbEnvConfig } from '../types/index.js';
|
|
2
|
-
export declare function resolveTransport(fromConfig: DbEnvConfig, toConfig: DbEnvConfig, requested?: TransportMode): 'local' | 'api';
|
|
2
|
+
export declare function resolveTransport(fromConfig: DbEnvConfig, toConfig: DbEnvConfig, requested?: TransportMode): 'local' | 'api' | 'rsync' | 'dump-restore';
|
|
3
3
|
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/migrate/transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/migrate/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,EACvB,QAAQ,EAAE,WAAW,EACrB,SAAS,GAAE,aAAsB,GAChC,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,cAAc,CAO5C"}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
// hazo_env/src/migrate/transport.ts — resolve transport mode for a migration
|
|
2
|
-
import { HazoError } from 'hazo_core';
|
|
3
1
|
export function resolveTransport(fromConfig, toConfig, requested = 'auto') {
|
|
4
|
-
if (requested === '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
message: 'SSH transport is not yet implemented. Use local or api transport.',
|
|
9
|
-
});
|
|
10
|
-
}
|
|
2
|
+
if (requested === 'dump-restore')
|
|
3
|
+
return 'dump-restore';
|
|
4
|
+
if (requested === 'ssh')
|
|
5
|
+
return 'rsync';
|
|
11
6
|
if (requested === 'api')
|
|
12
7
|
return 'api';
|
|
13
8
|
// auto: both sqlite → local
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BackupConfig {
|
|
2
|
+
/** Which [transport.ssh.*] connection to reuse for the SSH session */
|
|
3
|
+
ssh_env: string;
|
|
4
|
+
/** The Postgres database to drop/recreate on the remote */
|
|
5
|
+
target_db: string;
|
|
6
|
+
/** Owner passed to createdb -O */
|
|
7
|
+
owner: string;
|
|
8
|
+
/** Glob matching an existing dump file, or the literal string "fresh" */
|
|
9
|
+
dump_source: string;
|
|
10
|
+
/** Source DB for pg_dump — required when dump_source === 'fresh' */
|
|
11
|
+
fresh_dump_db?: string;
|
|
12
|
+
/** Shell command to run on the remote before dropping/restoring the DB */
|
|
13
|
+
pre_restore_cmd?: string;
|
|
14
|
+
/** Shell command to run on the remote after a successful restore */
|
|
15
|
+
post_restore_cmd?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the [backup.<env>] section from hazo_env_config.ini.
|
|
19
|
+
* Returns null when the section is missing (transport not configured).
|
|
20
|
+
* Throws when required fields are absent or ssh_env is unsafe.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveBackupConfig(env: string): BackupConfig | null;
|
|
23
|
+
//# sourceMappingURL=backup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/resolve/backup.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAcD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA8BpE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// hazo_env/src/resolve/backup.ts — dump-restore backup transport config resolver
|
|
2
|
+
//
|
|
3
|
+
// Reads [backup.<env>] from config/hazo_env_config.ini and returns a BackupConfig,
|
|
4
|
+
// or null when the section is absent (transport not configured for that env).
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { HazoConfig } from 'hazo_config/server';
|
|
7
|
+
import { assertSafeSshField } from '../migrate/ssh-exec.js';
|
|
8
|
+
function tryLoadEnvConfig() {
|
|
9
|
+
try {
|
|
10
|
+
return new HazoConfig({ filePath: path.resolve(process.cwd(), 'config', 'hazo_env_config.ini') });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function expandEnvVars(value) {
|
|
17
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the [backup.<env>] section from hazo_env_config.ini.
|
|
21
|
+
* Returns null when the section is missing (transport not configured).
|
|
22
|
+
* Throws when required fields are absent or ssh_env is unsafe.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveBackupConfig(env) {
|
|
25
|
+
const config = tryLoadEnvConfig();
|
|
26
|
+
if (!config)
|
|
27
|
+
return null;
|
|
28
|
+
const section = config.getSection(`backup.${env}`);
|
|
29
|
+
if (!section)
|
|
30
|
+
return null;
|
|
31
|
+
const ssh_env_raw = section['ssh_env'];
|
|
32
|
+
const target_db_raw = section['target_db'];
|
|
33
|
+
const owner_raw = section['owner'];
|
|
34
|
+
const dump_source_raw = section['dump_source'];
|
|
35
|
+
const fresh_dump_db_raw = section['fresh_dump_db'];
|
|
36
|
+
const pre_restore_cmd_raw = section['pre_restore_cmd'];
|
|
37
|
+
const post_restore_cmd_raw = section['post_restore_cmd'];
|
|
38
|
+
if (!ssh_env_raw || !target_db_raw || !owner_raw || !dump_source_raw)
|
|
39
|
+
return null;
|
|
40
|
+
const ssh_env = expandEnvVars(ssh_env_raw);
|
|
41
|
+
// ssh_env is used as an SSH argument — must not contain shell-hostile chars
|
|
42
|
+
assertSafeSshField('ssh_env', ssh_env);
|
|
43
|
+
return {
|
|
44
|
+
ssh_env,
|
|
45
|
+
target_db: expandEnvVars(target_db_raw),
|
|
46
|
+
owner: expandEnvVars(owner_raw),
|
|
47
|
+
dump_source: expandEnvVars(dump_source_raw),
|
|
48
|
+
fresh_dump_db: fresh_dump_db_raw ? expandEnvVars(fresh_dump_db_raw) : undefined,
|
|
49
|
+
pre_restore_cmd: pre_restore_cmd_raw ? expandEnvVars(pre_restore_cmd_raw) : undefined,
|
|
50
|
+
post_restore_cmd: post_restore_cmd_raw ? expandEnvVars(post_restore_cmd_raw) : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SshConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
user: string;
|
|
4
|
+
key: string;
|
|
5
|
+
path: string;
|
|
6
|
+
/**
|
|
7
|
+
* Optional pinned host public key (the right-hand side of a known_hosts line,
|
|
8
|
+
* e.g. "ssh-ed25519 AAAA...."). When set, host-key checking is strict and the
|
|
9
|
+
* key is pinned — no TOFU. When absent, the transport falls back to
|
|
10
|
+
* accept-new against a persistent known_hosts file.
|
|
11
|
+
*/
|
|
12
|
+
hostKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveSshConfig(env: string): SshConfig | null;
|
|
15
|
+
//# sourceMappingURL=ssh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/resolve/ssh.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsB9D"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// hazo_env/src/resolve/ssh.ts — SSH transport config resolver
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { HazoConfig } from 'hazo_config/server';
|
|
4
|
+
function tryLoadEnvConfig() {
|
|
5
|
+
try {
|
|
6
|
+
return new HazoConfig({ filePath: path.resolve(process.cwd(), 'config', 'hazo_env_config.ini') });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function expandEnvVars(value) {
|
|
13
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '');
|
|
14
|
+
}
|
|
15
|
+
export function resolveSshConfig(env) {
|
|
16
|
+
const config = tryLoadEnvConfig();
|
|
17
|
+
if (!config)
|
|
18
|
+
return null;
|
|
19
|
+
const section = config.getSection(`transport.ssh.${env}`);
|
|
20
|
+
if (!section)
|
|
21
|
+
return null;
|
|
22
|
+
const host = section['host'];
|
|
23
|
+
const user = section['user'];
|
|
24
|
+
const key = section['key'];
|
|
25
|
+
const remotePath = section['path'];
|
|
26
|
+
const hostKey = section['host_key'];
|
|
27
|
+
if (!host || !user || !key || !remotePath)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
host: expandEnvVars(host),
|
|
31
|
+
user: expandEnvVars(user),
|
|
32
|
+
key: expandEnvVars(key),
|
|
33
|
+
path: expandEnvVars(remotePath),
|
|
34
|
+
hostKey: hostKey ? expandEnvVars(hostKey) : undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -43,11 +43,11 @@ export interface ResolveConnectOptions {
|
|
|
43
43
|
env?: HazoEnv;
|
|
44
44
|
allowOtherEnv?: boolean;
|
|
45
45
|
}
|
|
46
|
-
export type TransportMode = 'auto' | 'local' | 'ssh' | 'api';
|
|
46
|
+
export type TransportMode = 'auto' | 'local' | 'ssh' | 'api' | 'dump-restore';
|
|
47
47
|
export type ScrubMode = 'auto' | 'none';
|
|
48
48
|
export type HashMode = 'none' | 'sample' | 'full';
|
|
49
49
|
export interface MigrationProgress {
|
|
50
|
-
phase: 'validate' | 'snapshot' | 'plan' | 'schema-check' | 'db' | 'files' | 'verify' | 'finalize' | 'done';
|
|
50
|
+
phase: 'validate' | 'snapshot' | 'plan' | 'schema-check' | 'db' | 'files' | 'verify' | 'finalize' | 'done' | 'clear';
|
|
51
51
|
message: string;
|
|
52
52
|
percent?: number;
|
|
53
53
|
}
|
|
@@ -75,6 +75,7 @@ export interface MigrationResult {
|
|
|
75
75
|
tables: number;
|
|
76
76
|
rows: number;
|
|
77
77
|
scrubbed: number;
|
|
78
|
+
mode?: string;
|
|
78
79
|
};
|
|
79
80
|
files?: {
|
|
80
81
|
copied: number;
|
|
@@ -102,5 +103,24 @@ export interface MigrateConfig {
|
|
|
102
103
|
preserve?: string[];
|
|
103
104
|
pkOverrides?: Record<string, string>;
|
|
104
105
|
}
|
|
106
|
+
export interface ClearRequest {
|
|
107
|
+
env: HazoEnv;
|
|
108
|
+
include?: {
|
|
109
|
+
db?: boolean;
|
|
110
|
+
files?: boolean;
|
|
111
|
+
};
|
|
112
|
+
allowProdTarget?: boolean;
|
|
113
|
+
confirmToken?: string;
|
|
114
|
+
jobId?: string;
|
|
115
|
+
progressDir?: string;
|
|
116
|
+
onProgress?: (p: MigrationProgress) => void;
|
|
117
|
+
}
|
|
118
|
+
export interface ClearResult {
|
|
119
|
+
ok: boolean;
|
|
120
|
+
tablesCleared: number;
|
|
121
|
+
filesCleared: boolean;
|
|
122
|
+
durationMs: number;
|
|
123
|
+
warnings: string[];
|
|
124
|
+
}
|
|
105
125
|
export type MaskTransform = (value: unknown, key: string, maskKey: string) => unknown;
|
|
106
126
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnE,mCAAmC;AACnC,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAE5E,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC;AAElC,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAErD,2CAA2C;AAC3C,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEtD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC7B,MAAM,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAED,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAID,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED