hazo_env 0.10.0 → 0.10.2
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 +22 -0
- package/dist/cli.js +0 -0
- package/dist/lib/i18n.d.ts +2 -0
- package/dist/lib/i18n.d.ts.map +1 -0
- package/dist/lib/i18n.js +21 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/locales/en.json +1 -0
- package/package.json +17 -13
- package/dist/mask/registry.d.ts +0 -8
- package/dist/mask/registry.d.ts.map +0 -1
- package/dist/mask/registry.js +0 -29
- package/dist/mask/ruleset.d.ts +0 -15
- package/dist/mask/ruleset.d.ts.map +0 -1
- package/dist/mask/ruleset.js +0 -77
- package/dist/migrate/audit.d.ts +0 -21
- package/dist/migrate/audit.d.ts.map +0 -1
- package/dist/migrate/audit.js +0 -36
- package/dist/migrate/clear.d.ts +0 -3
- package/dist/migrate/clear.d.ts.map +0 -1
- package/dist/migrate/clear.js +0 -117
- package/dist/migrate/db-dump-restore.d.ts +0 -24
- package/dist/migrate/db-dump-restore.d.ts.map +0 -1
- package/dist/migrate/db-dump-restore.js +0 -72
- package/dist/migrate/db.d.ts +0 -17
- package/dist/migrate/db.d.ts.map +0 -1
- package/dist/migrate/db.js +0 -127
- package/dist/migrate/db.postgrest.d.ts +0 -4
- package/dist/migrate/db.postgrest.d.ts.map +0 -1
- package/dist/migrate/db.postgrest.js +0 -95
- package/dist/migrate/files.d.ts +0 -15
- package/dist/migrate/files.d.ts.map +0 -1
- package/dist/migrate/files.js +0 -131
- package/dist/migrate/progress.d.ts +0 -5
- package/dist/migrate/progress.d.ts.map +0 -1
- package/dist/migrate/progress.js +0 -24
- package/dist/migrate/run.d.ts +0 -16
- package/dist/migrate/run.d.ts.map +0 -1
- package/dist/migrate/run.js +0 -270
- package/dist/migrate/snapshot.d.ts +0 -8
- package/dist/migrate/snapshot.d.ts.map +0 -1
- package/dist/migrate/snapshot.js +0 -51
- package/dist/migrate/transport.d.ts +0 -3
- package/dist/migrate/transport.d.ts.map +0 -1
- package/dist/migrate/transport.js +0 -12
- package/dist/migrate/verify.d.ts +0 -9
- package/dist/migrate/verify.d.ts.map +0 -1
- package/dist/migrate/verify.js +0 -141
- package/dist/resolve/backup.d.ts +0 -23
- package/dist/resolve/backup.d.ts.map +0 -1
- package/dist/resolve/backup.js +0 -52
- package/dist/resolve/migrate.d.ts +0 -3
- package/dist/resolve/migrate.d.ts.map +0 -1
- package/dist/resolve/migrate.js +0 -29
package/dist/migrate/run.js
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/run.ts — runMigration pipeline
|
|
2
|
-
//
|
|
3
|
-
// PDF scrubbing (Phase C): after each file copy, files.ts invokes hazo_pdf/server mask_pdf
|
|
4
|
-
// for any .pdf file when HAZO_ENV_MASK_KEY is set. hazo_pdf is an optional peer dep of
|
|
5
|
-
// hazo_env — if not installed the copy still completes (scrub silently skipped).
|
|
6
|
-
// See: src/migrate/files.ts scrubPdfFile() and hazo_pdf/src/server/mask.ts
|
|
7
|
-
import { HazoError, optional_import } from 'hazo_core';
|
|
8
|
-
import { resolveConnectConfig } from '../resolve/connect.js';
|
|
9
|
-
import { resolveMigrateConfig } from '../resolve/migrate.js';
|
|
10
|
-
import { resolveFilesConfig } from '../resolve/files.js';
|
|
11
|
-
import { resolveTransport } from './transport.js';
|
|
12
|
-
import { takeSnapshot } from './snapshot.js';
|
|
13
|
-
import { copyDb } from './db.js';
|
|
14
|
-
import { copyFiles } from './files.js';
|
|
15
|
-
import { verifyFiles } from './verify.js';
|
|
16
|
-
import { auditMigration } from './audit.js';
|
|
17
|
-
import { writeMigrationProgress } from './progress.js';
|
|
18
|
-
import { getEnvRole } from '../env.server.js';
|
|
19
|
-
function emitProgress(req, p) {
|
|
20
|
-
req.onProgress?.(p);
|
|
21
|
-
if (req.jobId && req.progressDir) {
|
|
22
|
-
writeMigrationProgress(req.progressDir, req.jobId, p);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the effective table list handed to copyDb.
|
|
27
|
-
*
|
|
28
|
-
* `'*'` (and an unset request) both mean "all tables". The SQL path can
|
|
29
|
-
* auto-discover that set, but the PostgREST path cannot (no FK ordering without
|
|
30
|
-
* an explicit list) and rejects `'*'`. So when the request asks for "all", we
|
|
31
|
-
* fall back to the configured `[migrate] tables` list — the app's canonical
|
|
32
|
-
* FK-ordered definition of "all". Only when no config list exists do we pass
|
|
33
|
-
* `'*'` through (SQL auto-discovers; PostgREST then errors as designed).
|
|
34
|
-
*
|
|
35
|
-
* An explicit `string[]` from the request always wins.
|
|
36
|
-
*/
|
|
37
|
-
export function resolveTableList(reqTables, configTables) {
|
|
38
|
-
if (reqTables && reqTables !== '*')
|
|
39
|
-
return reqTables;
|
|
40
|
-
return configTables ?? '*';
|
|
41
|
-
}
|
|
42
|
-
// Resolve the SQLite driver — prefer better-sqlite3 in tests when env var is set
|
|
43
|
-
function getSqliteDriver() {
|
|
44
|
-
if (process.env['HAZO_ENV_TEST_SQLITE_DRIVER'] === 'better-sqlite3')
|
|
45
|
-
return 'better-sqlite3';
|
|
46
|
-
return 'sql.js';
|
|
47
|
-
}
|
|
48
|
-
async function buildScrubHook(toEnv, scrubMode, tgtAdapter) {
|
|
49
|
-
const maskKey = process.env['HAZO_ENV_MASK_KEY'];
|
|
50
|
-
const toRole = getEnvRole(toEnv);
|
|
51
|
-
const shouldScrub = scrubMode !== 'none' && !!maskKey && (toRole === 'test' || toRole === 'staging');
|
|
52
|
-
if (!shouldScrub)
|
|
53
|
-
return undefined;
|
|
54
|
-
const { loadRuleset } = await import('../mask/ruleset.js');
|
|
55
|
-
const { getTransform } = await import('../mask/registry.js');
|
|
56
|
-
let ruleset;
|
|
57
|
-
try {
|
|
58
|
-
ruleset = await loadRuleset(tgtAdapter);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
if (ruleset.length === 0)
|
|
64
|
-
return undefined;
|
|
65
|
-
const transformMap = new Map();
|
|
66
|
-
for (const rule of ruleset) {
|
|
67
|
-
const fn = await getTransform(rule.transform);
|
|
68
|
-
if (!fn)
|
|
69
|
-
continue;
|
|
70
|
-
if (!transformMap.has(rule.table))
|
|
71
|
-
transformMap.set(rule.table, new Map());
|
|
72
|
-
transformMap.get(rule.table).set(rule.column, fn);
|
|
73
|
-
}
|
|
74
|
-
return (table, row) => {
|
|
75
|
-
const colTransforms = transformMap.get(table);
|
|
76
|
-
if (!colTransforms || colTransforms.size === 0)
|
|
77
|
-
return row;
|
|
78
|
-
const result = { ...row };
|
|
79
|
-
for (const [col, fn] of colTransforms) {
|
|
80
|
-
if (col in result)
|
|
81
|
-
result[col] = fn(result[col], col, maskKey);
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
export async function runMigration(req) {
|
|
87
|
-
const startMs = Date.now();
|
|
88
|
-
const warnings = [];
|
|
89
|
-
const connect = await optional_import('hazo_connect/server');
|
|
90
|
-
if (!connect) {
|
|
91
|
-
throw new HazoError({
|
|
92
|
-
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
93
|
-
pkg: 'hazo_env',
|
|
94
|
-
message: 'hazo_connect is required for migration. Install it as a peer dependency.',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
// Step 1: Validate
|
|
98
|
-
emitProgress(req, { phase: 'validate', message: 'Validating migration request', percent: 0 });
|
|
99
|
-
if (!req.from || !req.to) {
|
|
100
|
-
throw new HazoError({ code: 'HAZO_ENV_INVALID_REQUEST', pkg: 'hazo_env', message: 'Migration requires both from and to environments.' });
|
|
101
|
-
}
|
|
102
|
-
if (req.from === req.to) {
|
|
103
|
-
throw new HazoError({ code: 'HAZO_ENV_INVALID_REQUEST', pkg: 'hazo_env', message: `Source and target environments must differ (got "${req.from}" for both).` });
|
|
104
|
-
}
|
|
105
|
-
const toRole = getEnvRole(req.to);
|
|
106
|
-
if (toRole === 'production' && !req.allowProdTarget) {
|
|
107
|
-
throw new HazoError({
|
|
108
|
-
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
109
|
-
pkg: 'hazo_env',
|
|
110
|
-
message: `Refusing to migrate to production environment "${req.to}". Pass allowProdTarget:true and a confirmToken to override.`,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
if (toRole === 'production' && req.allowProdTarget && !req.confirmToken) {
|
|
114
|
-
throw new HazoError({
|
|
115
|
-
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
116
|
-
pkg: 'hazo_env',
|
|
117
|
-
message: 'Production target requires a confirmToken for safety.',
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const fromDbConfig = resolveConnectConfig({ env: req.from, allowOtherEnv: true });
|
|
121
|
-
const toDbConfig = resolveConnectConfig({ env: req.to, allowOtherEnv: true });
|
|
122
|
-
resolveTransport(fromDbConfig, toDbConfig, req.transport);
|
|
123
|
-
const includeDb = req.include?.db ?? true;
|
|
124
|
-
const includeFiles = req.include?.files ?? true;
|
|
125
|
-
const scrubMode = req.scrub ?? 'auto';
|
|
126
|
-
// Step 2: Snapshot target
|
|
127
|
-
emitProgress(req, { phase: 'snapshot', message: 'Taking snapshot of target', percent: 5 });
|
|
128
|
-
const snapshot = takeSnapshot(toDbConfig);
|
|
129
|
-
// Step 3: Plan (dry-run stops here)
|
|
130
|
-
emitProgress(req, { phase: 'plan', message: 'Building migration plan', percent: 10 });
|
|
131
|
-
if (req.dryRun) {
|
|
132
|
-
return {
|
|
133
|
-
ok: true,
|
|
134
|
-
snapshotId: snapshot.snapshotId,
|
|
135
|
-
db: includeDb ? { tables: 0, rows: 0, scrubbed: 0 } : undefined,
|
|
136
|
-
files: includeFiles ? { copied: 0, bytes: 0, placeholdered: 0 } : undefined,
|
|
137
|
-
warnings: ['dry-run: no changes made'],
|
|
138
|
-
durationMs: Date.now() - startMs,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
// Create adapters
|
|
142
|
-
function makeAdapter(dbConfig, readOnly = false) {
|
|
143
|
-
if (dbConfig.type === 'sqlite') {
|
|
144
|
-
if (!dbConfig.sqlite) {
|
|
145
|
-
throw new HazoError({
|
|
146
|
-
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
147
|
-
pkg: 'hazo_env',
|
|
148
|
-
message: 'SQLite config missing database_path.',
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return connect.createHazoConnect({
|
|
152
|
-
type: 'sqlite',
|
|
153
|
-
sqlite: {
|
|
154
|
-
database_path: dbConfig.sqlite.database_path,
|
|
155
|
-
read_only: readOnly,
|
|
156
|
-
driver: getSqliteDriver(),
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (dbConfig.type === 'postgrest') {
|
|
161
|
-
if (!dbConfig.postgrest) {
|
|
162
|
-
throw new HazoError({
|
|
163
|
-
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
164
|
-
pkg: 'hazo_env',
|
|
165
|
-
message: 'PostgREST config missing base_url/api_key.',
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return connect.createHazoConnect({
|
|
169
|
-
type: 'postgrest',
|
|
170
|
-
postgrest: {
|
|
171
|
-
base_url: dbConfig.postgrest.base_url,
|
|
172
|
-
api_key: dbConfig.postgrest.api_key,
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
throw new HazoError({
|
|
177
|
-
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
178
|
-
pkg: 'hazo_env',
|
|
179
|
-
message: `Migration is not implemented for DB type "${dbConfig.type}".`,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
const srcAdapter = makeAdapter(fromDbConfig, true);
|
|
183
|
-
const tgtAdapter = makeAdapter(toDbConfig, false);
|
|
184
|
-
// Step 4: Schema parity (checked inside copyDb)
|
|
185
|
-
emitProgress(req, { phase: 'schema-check', message: 'Checking schema parity', percent: 15 });
|
|
186
|
-
let dbResult;
|
|
187
|
-
let filesResult;
|
|
188
|
-
const migrateConfig = resolveMigrateConfig();
|
|
189
|
-
// Step 5: DB copy
|
|
190
|
-
if (includeDb) {
|
|
191
|
-
emitProgress(req, { phase: 'db', message: 'Copying database tables', percent: 20 });
|
|
192
|
-
if (req.transport === 'dump-restore') {
|
|
193
|
-
const { resolveBackupConfig } = await import('../resolve/backup.js');
|
|
194
|
-
const { restoreDbViaDump } = await import('./db-dump-restore.js');
|
|
195
|
-
const backup = resolveBackupConfig(req.to);
|
|
196
|
-
if (!backup) {
|
|
197
|
-
throw new HazoError({
|
|
198
|
-
code: 'HAZO_ENV_INVALID_REQUEST',
|
|
199
|
-
pkg: 'hazo_env',
|
|
200
|
-
message: `[backup.${req.to}] config section is required for dump-restore transport`,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
dbResult = await restoreDbViaDump(backup, {
|
|
204
|
-
onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
|
|
205
|
-
});
|
|
206
|
-
emitProgress(req, { phase: 'db', message: 'DB restore via dump complete', percent: 70 });
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
const scrubHook = await buildScrubHook(req.to, scrubMode, tgtAdapter);
|
|
210
|
-
const result = await copyDb(srcAdapter, tgtAdapter, {
|
|
211
|
-
type: fromDbConfig.type,
|
|
212
|
-
tables: resolveTableList(req.tables, migrateConfig.tables),
|
|
213
|
-
preserve: migrateConfig.preserve,
|
|
214
|
-
pkOverrides: migrateConfig.pkOverrides,
|
|
215
|
-
scrubHook,
|
|
216
|
-
onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
|
|
217
|
-
onTableProgress: (idx, total, name) => emitProgress(req, {
|
|
218
|
-
phase: 'db',
|
|
219
|
-
message: `table ${idx}/${total}: ${name}`,
|
|
220
|
-
percent: 20 + Math.round((idx / total) * 50),
|
|
221
|
-
}),
|
|
222
|
-
});
|
|
223
|
-
dbResult = result;
|
|
224
|
-
emitProgress(req, { phase: 'db', message: `DB copy complete: ${result.tables} tables, ${result.rows} rows`, percent: 70 });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// Step 6: File copy
|
|
228
|
-
if (includeFiles) {
|
|
229
|
-
emitProgress(req, { phase: 'files', message: 'Copying files', percent: 72 });
|
|
230
|
-
const result = await copyFiles({
|
|
231
|
-
fromEnv: req.from,
|
|
232
|
-
toEnv: req.to,
|
|
233
|
-
onProgress: (msg) => emitProgress(req, { phase: 'files', message: msg }),
|
|
234
|
-
});
|
|
235
|
-
filesResult = result;
|
|
236
|
-
}
|
|
237
|
-
// Step 7: Verify
|
|
238
|
-
emitProgress(req, { phase: 'verify', message: 'Running post-migration verification', percent: 80 });
|
|
239
|
-
const dataRoot = resolveFilesConfig().local.basePath;
|
|
240
|
-
const verifyReport = await verifyFiles(req.to, dataRoot, { hash: 'sample', checkOrphans: true, adapter: tgtAdapter });
|
|
241
|
-
if (!verifyReport.ok) {
|
|
242
|
-
const parts = [];
|
|
243
|
-
if (verifyReport.missing.length)
|
|
244
|
-
parts.push(`missing=${verifyReport.missing.length}`);
|
|
245
|
-
if (verifyReport.sizeMismatch.length)
|
|
246
|
-
parts.push(`sizeMismatch=${verifyReport.sizeMismatch.length}`);
|
|
247
|
-
if (verifyReport.hashMismatch.length)
|
|
248
|
-
parts.push(`hashMismatch=${verifyReport.hashMismatch.length}`);
|
|
249
|
-
warnings.push(`Verify found issues (${parts.join(', ')}) — migration continues; snapshotId=${snapshot.snapshotId}`);
|
|
250
|
-
}
|
|
251
|
-
// Step 8: Finalize — audit
|
|
252
|
-
emitProgress(req, { phase: 'finalize', message: 'Recording audit event', percent: 95 });
|
|
253
|
-
await auditMigration(tgtAdapter, {
|
|
254
|
-
from: req.from,
|
|
255
|
-
to: req.to,
|
|
256
|
-
scrub: scrubMode,
|
|
257
|
-
db: dbResult,
|
|
258
|
-
files: filesResult ? { copied: filesResult.copied, bytes: filesResult.bytes } : undefined,
|
|
259
|
-
verify: { ok: verifyReport.ok, checked: verifyReport.checked },
|
|
260
|
-
});
|
|
261
|
-
emitProgress(req, { phase: 'done', message: 'Migration complete', percent: 100 });
|
|
262
|
-
return {
|
|
263
|
-
ok: true,
|
|
264
|
-
snapshotId: snapshot.snapshotId,
|
|
265
|
-
db: dbResult,
|
|
266
|
-
files: filesResult,
|
|
267
|
-
warnings,
|
|
268
|
-
durationMs: Date.now() - startMs,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { DbEnvConfig } from '../types/index.js';
|
|
2
|
-
export interface SnapshotResult {
|
|
3
|
-
snapshotId: string;
|
|
4
|
-
createdAt: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function takeSnapshot(toConfig: DbEnvConfig): SnapshotResult;
|
|
7
|
-
export declare function restoreSnapshot(toConfig: DbEnvConfig, snapshotId: string): void;
|
|
8
|
-
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/migrate/snapshot.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,cAAc,CAsBlE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuB/E"}
|
package/dist/migrate/snapshot.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/snapshot.ts — SQLite file-copy snapshot
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { HazoError } from 'hazo_core';
|
|
5
|
-
export function takeSnapshot(toConfig) {
|
|
6
|
-
if (toConfig.type === 'postgrest') {
|
|
7
|
-
return { snapshotId: 'postgrest:no-snapshot', createdAt: new Date().toISOString() };
|
|
8
|
-
}
|
|
9
|
-
if (toConfig.type !== 'sqlite' || !toConfig.sqlite?.database_path) {
|
|
10
|
-
throw new HazoError({
|
|
11
|
-
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
12
|
-
pkg: 'hazo_env',
|
|
13
|
-
message: 'Snapshot is only supported for SQLite databases.',
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
const srcPath = toConfig.sqlite.database_path;
|
|
17
|
-
if (!fs.existsSync(srcPath)) {
|
|
18
|
-
// No DB yet — snapshot is a no-op, return a virtual ID
|
|
19
|
-
return { snapshotId: `no-db:${srcPath}`, createdAt: new Date().toISOString() };
|
|
20
|
-
}
|
|
21
|
-
const snapshotDir = path.join(path.dirname(srcPath), 'snapshots');
|
|
22
|
-
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
23
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
24
|
-
const snapshotPath = path.join(snapshotDir, `pre-migrate-${ts}.sqlite`);
|
|
25
|
-
fs.copyFileSync(srcPath, snapshotPath);
|
|
26
|
-
return { snapshotId: snapshotPath, createdAt: new Date().toISOString() };
|
|
27
|
-
}
|
|
28
|
-
export function restoreSnapshot(toConfig, snapshotId) {
|
|
29
|
-
if (toConfig.type !== 'sqlite' || !toConfig.sqlite?.database_path) {
|
|
30
|
-
throw new HazoError({
|
|
31
|
-
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
32
|
-
pkg: 'hazo_env',
|
|
33
|
-
message: 'Restore is only supported for SQLite databases.',
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
if (snapshotId.startsWith('no-db:')) {
|
|
37
|
-
throw new HazoError({
|
|
38
|
-
code: 'HAZO_ENV_SNAPSHOT_NOT_FOUND',
|
|
39
|
-
pkg: 'hazo_env',
|
|
40
|
-
message: 'Cannot restore: snapshot was taken when no database existed.',
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
if (!fs.existsSync(snapshotId)) {
|
|
44
|
-
throw new HazoError({
|
|
45
|
-
code: 'HAZO_ENV_SNAPSHOT_NOT_FOUND',
|
|
46
|
-
pkg: 'hazo_env',
|
|
47
|
-
message: `Snapshot file not found: ${snapshotId}`,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
fs.copyFileSync(snapshotId, toConfig.sqlite.database_path);
|
|
51
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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,12 +0,0 @@
|
|
|
1
|
-
export function resolveTransport(fromConfig, toConfig, requested = 'auto') {
|
|
2
|
-
if (requested === 'dump-restore')
|
|
3
|
-
return 'dump-restore';
|
|
4
|
-
if (requested === 'ssh')
|
|
5
|
-
return 'rsync';
|
|
6
|
-
if (requested === 'api')
|
|
7
|
-
return 'api';
|
|
8
|
-
// auto: both sqlite → local
|
|
9
|
-
if (fromConfig.type === 'sqlite' && toConfig.type === 'sqlite')
|
|
10
|
-
return 'local';
|
|
11
|
-
return 'api';
|
|
12
|
-
}
|
package/dist/migrate/verify.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { HazoEnv, VerifyReport, HashMode } from '../types/index.js';
|
|
2
|
-
export interface VerifyOptions {
|
|
3
|
-
hash?: HashMode;
|
|
4
|
-
samplePct?: number;
|
|
5
|
-
checkOrphans?: boolean;
|
|
6
|
-
adapter?: import('hazo_connect').HazoConnectAdapter;
|
|
7
|
-
}
|
|
8
|
-
export declare function verifyFiles(env: HazoEnv, dataRoot: string, opts?: VerifyOptions): Promise<VerifyReport>;
|
|
9
|
-
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/migrate/verify.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,cAAc,EAAE,kBAAkB,CAAC;CACrD;AAqBD,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CA8IvB"}
|
package/dist/migrate/verify.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/migrate/verify.ts — post-migration file verification
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { optional_import } from 'hazo_core';
|
|
5
|
-
function resolveEnvFilesRoot(env, dataRoot) {
|
|
6
|
-
return path.join(dataRoot, env, 'files');
|
|
7
|
-
}
|
|
8
|
-
function walkDir(dir) {
|
|
9
|
-
if (!fs.existsSync(dir))
|
|
10
|
-
return [];
|
|
11
|
-
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
12
|
-
const absPath = path.join(dir, entry.name);
|
|
13
|
-
if (entry.isDirectory())
|
|
14
|
-
return walkDir(absPath);
|
|
15
|
-
// Return relative to dir's parent so caller can strip filesRoot prefix
|
|
16
|
-
return [absPath];
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
function walkRelative(filesRoot) {
|
|
20
|
-
const abs = walkDir(filesRoot);
|
|
21
|
-
return abs.map((p) => p.slice(filesRoot.length + 1)); // strip filesRoot + sep
|
|
22
|
-
}
|
|
23
|
-
export async function verifyFiles(env, dataRoot, opts = {}) {
|
|
24
|
-
const { hash = 'sample', checkOrphans = true, adapter } = opts;
|
|
25
|
-
const filesRoot = resolveEnvFilesRoot(env, dataRoot);
|
|
26
|
-
const missing = [];
|
|
27
|
-
const sizeMismatch = [];
|
|
28
|
-
const hashMismatch = [];
|
|
29
|
-
const orphans = [];
|
|
30
|
-
let checked = 0;
|
|
31
|
-
let hashed;
|
|
32
|
-
// Layer 1: sentinel — filesRoot must exist and be readable
|
|
33
|
-
const sentinelOk = fs.existsSync(filesRoot);
|
|
34
|
-
if (!sentinelOk) {
|
|
35
|
-
return { ok: false, checked: 0, missing: [], sizeMismatch: [], hashMismatch: [], orphans: [], sentinelOk: false };
|
|
36
|
-
}
|
|
37
|
-
// S3: Fallback detection — need both hazo_files module and an adapter
|
|
38
|
-
const hazoFiles = await optional_import('hazo_files');
|
|
39
|
-
if (!hazoFiles || !adapter) {
|
|
40
|
-
// Disk-only fallback
|
|
41
|
-
const diskFiles = walkRelative(filesRoot);
|
|
42
|
-
checked = diskFiles.length;
|
|
43
|
-
const skippedReason = 'no hazo_files tracking — size/hash/orphan skipped';
|
|
44
|
-
const ok = sentinelOk && missing.length === 0;
|
|
45
|
-
return { ok, checked, missing, sizeMismatch, hashMismatch, orphans, sentinelOk, skippedReason };
|
|
46
|
-
}
|
|
47
|
-
// Main path: adapter + hazo_files available
|
|
48
|
-
const connectMod = await optional_import('hazo_connect/server');
|
|
49
|
-
let rows = [];
|
|
50
|
-
let skippedReason;
|
|
51
|
-
try {
|
|
52
|
-
if (!connectMod)
|
|
53
|
-
throw new Error('hazo_connect not available');
|
|
54
|
-
const service = connectMod.createCrudService(adapter, 'hazo_files');
|
|
55
|
-
const all = await service.list();
|
|
56
|
-
rows = all.filter((r) => !r.storage_type || r.storage_type === 'local');
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// Table not found or connect unavailable — disk-only fallback
|
|
60
|
-
const diskFiles = walkRelative(filesRoot);
|
|
61
|
-
checked = diskFiles.length;
|
|
62
|
-
skippedReason = 'hazo_files table not found — disk-only fallback';
|
|
63
|
-
const ok = sentinelOk && missing.length === 0;
|
|
64
|
-
return { ok, checked, missing, sizeMismatch, hashMismatch, orphans, sentinelOk, skippedReason };
|
|
65
|
-
}
|
|
66
|
-
// Normalize DB paths: strip leading slash if present
|
|
67
|
-
const normalizeDbPath = (p) => p.startsWith('/') ? p.slice(1) : p;
|
|
68
|
-
const dbPathSet = new Set(rows.map((r) => normalizeDbPath(r.file_path)));
|
|
69
|
-
// Track present rows for hash/size checks
|
|
70
|
-
const presentRows = [];
|
|
71
|
-
// L2 Existence
|
|
72
|
-
for (const row of rows) {
|
|
73
|
-
const relPath = normalizeDbPath(row.file_path);
|
|
74
|
-
const absPath = path.join(filesRoot, relPath);
|
|
75
|
-
if (!fs.existsSync(absPath)) {
|
|
76
|
-
missing.push(row.file_path);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
presentRows.push({ ...row, file_path: relPath });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// L3 Size
|
|
83
|
-
for (const row of presentRows) {
|
|
84
|
-
const absPath = path.join(filesRoot, row.file_path);
|
|
85
|
-
const stat = fs.statSync(absPath);
|
|
86
|
-
if (row.file_size != null && stat.size !== row.file_size) {
|
|
87
|
-
sizeMismatch.push(row.file_path);
|
|
88
|
-
}
|
|
89
|
-
checked++;
|
|
90
|
-
}
|
|
91
|
-
// L4 Hash
|
|
92
|
-
if (hash !== 'none') {
|
|
93
|
-
let toHash;
|
|
94
|
-
if (hash === 'full') {
|
|
95
|
-
toHash = presentRows;
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
// sample: 1% random (min 1) + top 10 by file_size DESC + top 10 by file_changed_at DESC
|
|
99
|
-
const n = Math.max(1, Math.floor(presentRows.length * 0.01));
|
|
100
|
-
const shuffled = [...presentRows].sort(() => Math.random() - 0.5).slice(0, n);
|
|
101
|
-
const bySize = [...presentRows]
|
|
102
|
-
.filter((r) => r.file_size != null)
|
|
103
|
-
.sort((a, b) => (b.file_size ?? 0) - (a.file_size ?? 0))
|
|
104
|
-
.slice(0, 10);
|
|
105
|
-
const byChanged = [...presentRows]
|
|
106
|
-
.filter((r) => r.file_changed_at != null)
|
|
107
|
-
.sort((a, b) => (b.file_changed_at ?? '') < (a.file_changed_at ?? '') ? -1 : 1)
|
|
108
|
-
.slice(0, 10);
|
|
109
|
-
// Deduplicate by file_path
|
|
110
|
-
const seen = new Set();
|
|
111
|
-
toHash = [];
|
|
112
|
-
for (const r of [...shuffled, ...bySize, ...byChanged]) {
|
|
113
|
-
if (!seen.has(r.file_path)) {
|
|
114
|
-
seen.add(r.file_path);
|
|
115
|
-
toHash.push(r);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
hashed = 0;
|
|
120
|
-
for (const row of toHash) {
|
|
121
|
-
const absPath = path.join(filesRoot, row.file_path);
|
|
122
|
-
const buf = fs.readFileSync(absPath);
|
|
123
|
-
const computed = hazoFiles.computeFileHashSync(buf);
|
|
124
|
-
if (row.file_hash && computed !== row.file_hash) {
|
|
125
|
-
hashMismatch.push(row.file_path);
|
|
126
|
-
}
|
|
127
|
-
hashed++;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// L5 Orphans
|
|
131
|
-
if (checkOrphans) {
|
|
132
|
-
const diskFiles = walkRelative(filesRoot);
|
|
133
|
-
for (const diskRel of diskFiles) {
|
|
134
|
-
if (!dbPathSet.has(diskRel)) {
|
|
135
|
-
orphans.push(diskRel);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const ok = sentinelOk && missing.length === 0 && sizeMismatch.length === 0 && hashMismatch.length === 0;
|
|
140
|
-
return { ok, checked, missing, sizeMismatch, hashMismatch, orphans, sentinelOk, hashed, skippedReason };
|
|
141
|
-
}
|
package/dist/resolve/backup.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/resolve/backup.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/resolve/migrate.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wBAAgB,oBAAoB,IAAI,aAAa,CA2BpD"}
|
package/dist/resolve/migrate.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// hazo_env/src/resolve/migrate.ts — read [migrate] section from hazo_env_config.ini
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { HazoConfig } from 'hazo_config/server';
|
|
4
|
-
export function resolveMigrateConfig() {
|
|
5
|
-
try {
|
|
6
|
-
const filePath = path.resolve(process.cwd(), 'config', 'hazo_env_config.ini');
|
|
7
|
-
const config = new HazoConfig({ filePath });
|
|
8
|
-
const section = config.getSection('migrate');
|
|
9
|
-
if (!section)
|
|
10
|
-
return {};
|
|
11
|
-
const tables = section['tables']
|
|
12
|
-
? section['tables'].split(',').map((t) => t.trim()).filter(Boolean)
|
|
13
|
-
: undefined;
|
|
14
|
-
const preserve = section['preserve']
|
|
15
|
-
? section['preserve'].split(',').map((t) => t.trim()).filter(Boolean)
|
|
16
|
-
: undefined;
|
|
17
|
-
// Parse per-table pk overrides: keys like "pk.some_table = custom_col"
|
|
18
|
-
const pkOverrides = {};
|
|
19
|
-
for (const [k, v] of Object.entries(section)) {
|
|
20
|
-
if (k.startsWith('pk.') && typeof v === 'string') {
|
|
21
|
-
pkOverrides[k.slice(3)] = v.trim();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return { tables, preserve, ...(Object.keys(pkOverrides).length ? { pkOverrides } : {}) };
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
}
|