hazo_env 0.1.1 → 0.3.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 +26 -0
- package/README.md +117 -13
- package/SETUP_CHECKLIST.md +73 -0
- package/config/hazo_env_config.ini.sample +16 -0
- package/config/hazo_env_masking.ini.sample +16 -16
- package/dist/cli.js +170 -9
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +16 -3
- package/dist/env.server.d.ts +7 -1
- package/dist/env.server.d.ts.map +1 -1
- package/dist/env.server.js +25 -2
- package/dist/index.client.d.ts +6 -2
- package/dist/index.client.d.ts.map +1 -1
- package/dist/index.client.js +15 -9
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/secret_columns.d.ts +3 -0
- package/dist/lib/secret_columns.d.ts.map +1 -0
- package/dist/lib/secret_columns.js +14 -0
- package/dist/mask/registry.d.ts +8 -0
- package/dist/mask/registry.d.ts.map +1 -0
- package/dist/mask/registry.js +29 -0
- package/dist/mask/ruleset.d.ts +15 -0
- package/dist/mask/ruleset.d.ts.map +1 -0
- package/dist/mask/ruleset.js +77 -0
- package/dist/migrate/audit.d.ts +21 -0
- package/dist/migrate/audit.d.ts.map +1 -0
- package/dist/migrate/audit.js +36 -0
- package/dist/migrate/db.d.ts +13 -0
- package/dist/migrate/db.d.ts.map +1 -0
- package/dist/migrate/db.js +122 -0
- package/dist/migrate/files.d.ts +14 -0
- package/dist/migrate/files.d.ts.map +1 -0
- package/dist/migrate/files.js +72 -0
- package/dist/migrate/progress.d.ts +5 -0
- package/dist/migrate/progress.d.ts.map +1 -0
- package/dist/migrate/progress.js +24 -0
- package/dist/migrate/run.d.ts +3 -0
- package/dist/migrate/run.d.ts.map +1 -0
- package/dist/migrate/run.js +195 -0
- package/dist/migrate/snapshot.d.ts +8 -0
- package/dist/migrate/snapshot.d.ts.map +1 -0
- package/dist/migrate/snapshot.js +48 -0
- package/dist/migrate/transport.d.ts +3 -0
- package/dist/migrate/transport.d.ts.map +1 -0
- package/dist/migrate/transport.js +17 -0
- package/dist/migrate/verify.d.ts +8 -0
- package/dist/migrate/verify.d.ts.map +1 -0
- package/dist/migrate/verify.js +51 -0
- package/dist/types/index.d.ts +47 -27
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MigrationProgress } from '../types/index.js';
|
|
2
|
+
export declare function writeMigrationProgress(progressDir: string, jobId: string, p: MigrationProgress): void;
|
|
3
|
+
export declare function readMigrationProgress(progressDir: string, jobId: string): MigrationProgress | null;
|
|
4
|
+
export declare function clearMigrationProgress(progressDir: string, jobId: string): void;
|
|
5
|
+
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/migrate/progress.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAGrG;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAQlG;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAG/E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// hazo_env/src/migrate/progress.ts — Filesystem-backed migration progress store
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export function writeMigrationProgress(progressDir, jobId, p) {
|
|
5
|
+
fs.mkdirSync(progressDir, { recursive: true });
|
|
6
|
+
fs.writeFileSync(path.join(progressDir, `${jobId}.json`), JSON.stringify(p), 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
export function readMigrationProgress(progressDir, jobId) {
|
|
9
|
+
const filePath = path.join(progressDir, `${jobId}.json`);
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function clearMigrationProgress(progressDir, jobId) {
|
|
19
|
+
const filePath = path.join(progressDir, `${jobId}.json`);
|
|
20
|
+
try {
|
|
21
|
+
fs.unlinkSync(filePath);
|
|
22
|
+
}
|
|
23
|
+
catch { /* ignore */ }
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/migrate/run.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAkC,MAAM,mBAAmB,CAAC;AA0D3G,wBAAsB,YAAY,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkJlF"}
|
|
@@ -0,0 +1,195 @@
|
|
|
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 { resolveFilesConfig } from '../resolve/files.js';
|
|
10
|
+
import { resolveTransport } from './transport.js';
|
|
11
|
+
import { takeSnapshot } from './snapshot.js';
|
|
12
|
+
import { copyDb } from './db.js';
|
|
13
|
+
import { copyFiles } from './files.js';
|
|
14
|
+
import { verifyFiles } from './verify.js';
|
|
15
|
+
import { auditMigration } from './audit.js';
|
|
16
|
+
import { writeMigrationProgress } from './progress.js';
|
|
17
|
+
import { getEnvRole } from '../env.server.js';
|
|
18
|
+
function emitProgress(req, p) {
|
|
19
|
+
req.onProgress?.(p);
|
|
20
|
+
if (req.jobId && req.progressDir) {
|
|
21
|
+
writeMigrationProgress(req.progressDir, req.jobId, p);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Resolve the SQLite driver — prefer better-sqlite3 in tests when env var is set
|
|
25
|
+
function getSqliteDriver() {
|
|
26
|
+
if (process.env['HAZO_ENV_TEST_SQLITE_DRIVER'] === 'better-sqlite3')
|
|
27
|
+
return 'better-sqlite3';
|
|
28
|
+
return 'sql.js';
|
|
29
|
+
}
|
|
30
|
+
async function buildScrubHook(toEnv, scrubMode, tgtAdapter) {
|
|
31
|
+
const maskKey = process.env['HAZO_ENV_MASK_KEY'];
|
|
32
|
+
const toRole = getEnvRole(toEnv);
|
|
33
|
+
const shouldScrub = scrubMode !== 'none' && !!maskKey && (toRole === 'test' || toRole === 'staging');
|
|
34
|
+
if (!shouldScrub)
|
|
35
|
+
return undefined;
|
|
36
|
+
const { loadRuleset } = await import('../mask/ruleset.js');
|
|
37
|
+
const { getTransform } = await import('../mask/registry.js');
|
|
38
|
+
let ruleset;
|
|
39
|
+
try {
|
|
40
|
+
ruleset = await loadRuleset(tgtAdapter);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (ruleset.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
const transformMap = new Map();
|
|
48
|
+
for (const rule of ruleset) {
|
|
49
|
+
const fn = await getTransform(rule.transform);
|
|
50
|
+
if (!fn)
|
|
51
|
+
continue;
|
|
52
|
+
if (!transformMap.has(rule.table))
|
|
53
|
+
transformMap.set(rule.table, new Map());
|
|
54
|
+
transformMap.get(rule.table).set(rule.column, fn);
|
|
55
|
+
}
|
|
56
|
+
return (table, row) => {
|
|
57
|
+
const colTransforms = transformMap.get(table);
|
|
58
|
+
if (!colTransforms || colTransforms.size === 0)
|
|
59
|
+
return row;
|
|
60
|
+
const result = { ...row };
|
|
61
|
+
for (const [col, fn] of colTransforms) {
|
|
62
|
+
if (col in result)
|
|
63
|
+
result[col] = fn(result[col], col, maskKey);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export async function runMigration(req) {
|
|
69
|
+
const startMs = Date.now();
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const connect = await optional_import('hazo_connect/server');
|
|
72
|
+
if (!connect) {
|
|
73
|
+
throw new HazoError({
|
|
74
|
+
code: 'HAZO_ENV_MISSING_DEPENDENCY',
|
|
75
|
+
pkg: 'hazo_env',
|
|
76
|
+
message: 'hazo_connect is required for migration. Install it as a peer dependency.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Step 1: Validate
|
|
80
|
+
emitProgress(req, { phase: 'validate', message: 'Validating migration request', percent: 0 });
|
|
81
|
+
if (!req.from || !req.to) {
|
|
82
|
+
throw new HazoError({ code: 'HAZO_ENV_INVALID_REQUEST', pkg: 'hazo_env', message: 'Migration requires both from and to environments.' });
|
|
83
|
+
}
|
|
84
|
+
if (req.from === req.to) {
|
|
85
|
+
throw new HazoError({ code: 'HAZO_ENV_INVALID_REQUEST', pkg: 'hazo_env', message: `Source and target environments must differ (got "${req.from}" for both).` });
|
|
86
|
+
}
|
|
87
|
+
const toRole = getEnvRole(req.to);
|
|
88
|
+
if (toRole === 'production' && !req.allowProdTarget) {
|
|
89
|
+
throw new HazoError({
|
|
90
|
+
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
91
|
+
pkg: 'hazo_env',
|
|
92
|
+
message: `Refusing to migrate to production environment "${req.to}". Pass allowProdTarget:true and a confirmToken to override.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (toRole === 'production' && req.allowProdTarget && !req.confirmToken) {
|
|
96
|
+
throw new HazoError({
|
|
97
|
+
code: 'HAZO_ENV_PROD_TARGET_REFUSED',
|
|
98
|
+
pkg: 'hazo_env',
|
|
99
|
+
message: 'Production target requires a confirmToken for safety.',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const fromDbConfig = resolveConnectConfig({ env: req.from, allowOtherEnv: true });
|
|
103
|
+
const toDbConfig = resolveConnectConfig({ env: req.to, allowOtherEnv: true });
|
|
104
|
+
resolveTransport(fromDbConfig, toDbConfig, req.transport);
|
|
105
|
+
const includeDb = req.include?.db ?? true;
|
|
106
|
+
const includeFiles = req.include?.files ?? true;
|
|
107
|
+
const scrubMode = req.scrub ?? 'auto';
|
|
108
|
+
// Step 2: Snapshot target
|
|
109
|
+
emitProgress(req, { phase: 'snapshot', message: 'Taking snapshot of target', percent: 5 });
|
|
110
|
+
const snapshot = takeSnapshot(toDbConfig);
|
|
111
|
+
// Step 3: Plan (dry-run stops here)
|
|
112
|
+
emitProgress(req, { phase: 'plan', message: 'Building migration plan', percent: 10 });
|
|
113
|
+
if (req.dryRun) {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
snapshotId: snapshot.snapshotId,
|
|
117
|
+
db: includeDb ? { tables: 0, rows: 0, scrubbed: 0 } : undefined,
|
|
118
|
+
files: includeFiles ? { copied: 0, bytes: 0, placeholdered: 0 } : undefined,
|
|
119
|
+
warnings: ['dry-run: no changes made'],
|
|
120
|
+
durationMs: Date.now() - startMs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Create adapters
|
|
124
|
+
function makeAdapter(dbConfig, readOnly = false) {
|
|
125
|
+
if (dbConfig.type !== 'sqlite' || !dbConfig.sqlite) {
|
|
126
|
+
throw new HazoError({
|
|
127
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
128
|
+
pkg: 'hazo_env',
|
|
129
|
+
message: `Migration is only implemented for SQLite databases. Got type "${dbConfig.type}".`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return connect.createHazoConnect({
|
|
133
|
+
type: 'sqlite',
|
|
134
|
+
sqlite: {
|
|
135
|
+
database_path: dbConfig.sqlite.database_path,
|
|
136
|
+
read_only: readOnly,
|
|
137
|
+
driver: getSqliteDriver(),
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const srcAdapter = makeAdapter(fromDbConfig, true);
|
|
142
|
+
const tgtAdapter = makeAdapter(toDbConfig, false);
|
|
143
|
+
// Step 4: Schema parity (checked inside copyDb)
|
|
144
|
+
emitProgress(req, { phase: 'schema-check', message: 'Checking schema parity', percent: 15 });
|
|
145
|
+
let dbResult;
|
|
146
|
+
let filesResult;
|
|
147
|
+
// Step 5: DB copy
|
|
148
|
+
if (includeDb) {
|
|
149
|
+
emitProgress(req, { phase: 'db', message: 'Copying database tables', percent: 20 });
|
|
150
|
+
const scrubHook = await buildScrubHook(req.to, scrubMode, tgtAdapter);
|
|
151
|
+
const result = await copyDb(srcAdapter, tgtAdapter, {
|
|
152
|
+
tables: req.tables,
|
|
153
|
+
scrubHook,
|
|
154
|
+
onProgress: (msg) => emitProgress(req, { phase: 'db', message: msg }),
|
|
155
|
+
});
|
|
156
|
+
dbResult = result;
|
|
157
|
+
emitProgress(req, { phase: 'db', message: `DB copy complete: ${result.tables} tables, ${result.rows} rows`, percent: 70 });
|
|
158
|
+
}
|
|
159
|
+
// Step 6: File copy
|
|
160
|
+
if (includeFiles) {
|
|
161
|
+
emitProgress(req, { phase: 'files', message: 'Copying files', percent: 72 });
|
|
162
|
+
const result = await copyFiles({
|
|
163
|
+
fromEnv: req.from,
|
|
164
|
+
toEnv: req.to,
|
|
165
|
+
onProgress: (msg) => emitProgress(req, { phase: 'files', message: msg }),
|
|
166
|
+
});
|
|
167
|
+
filesResult = result;
|
|
168
|
+
}
|
|
169
|
+
// Step 7: Verify
|
|
170
|
+
emitProgress(req, { phase: 'verify', message: 'Running post-migration verification', percent: 80 });
|
|
171
|
+
const dataRoot = resolveFilesConfig().local.basePath;
|
|
172
|
+
const verifyReport = await verifyFiles(req.to, dataRoot, { hash: 'sample', checkOrphans: true });
|
|
173
|
+
if (!verifyReport.ok) {
|
|
174
|
+
warnings.push(`Verification found issues: missing=${verifyReport.missing.length}, sizeMismatch=${verifyReport.sizeMismatch.length}`);
|
|
175
|
+
}
|
|
176
|
+
// Step 8: Finalize — audit
|
|
177
|
+
emitProgress(req, { phase: 'finalize', message: 'Recording audit event', percent: 95 });
|
|
178
|
+
await auditMigration(tgtAdapter, {
|
|
179
|
+
from: req.from,
|
|
180
|
+
to: req.to,
|
|
181
|
+
scrub: scrubMode,
|
|
182
|
+
db: dbResult,
|
|
183
|
+
files: filesResult ? { copied: filesResult.copied, bytes: filesResult.bytes } : undefined,
|
|
184
|
+
verify: { ok: verifyReport.ok, checked: verifyReport.checked },
|
|
185
|
+
});
|
|
186
|
+
emitProgress(req, { phase: 'done', message: 'Migration complete', percent: 100 });
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
snapshotId: snapshot.snapshotId,
|
|
190
|
+
db: dbResult,
|
|
191
|
+
files: filesResult,
|
|
192
|
+
warnings,
|
|
193
|
+
durationMs: Date.now() - startMs,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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,CAmBlE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuB/E"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 !== 'sqlite' || !toConfig.sqlite?.database_path) {
|
|
7
|
+
throw new HazoError({
|
|
8
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
9
|
+
pkg: 'hazo_env',
|
|
10
|
+
message: 'Snapshot is only supported for SQLite databases.',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const srcPath = toConfig.sqlite.database_path;
|
|
14
|
+
if (!fs.existsSync(srcPath)) {
|
|
15
|
+
// No DB yet — snapshot is a no-op, return a virtual ID
|
|
16
|
+
return { snapshotId: `no-db:${srcPath}`, createdAt: new Date().toISOString() };
|
|
17
|
+
}
|
|
18
|
+
const snapshotDir = path.join(path.dirname(srcPath), 'snapshots');
|
|
19
|
+
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
20
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
21
|
+
const snapshotPath = path.join(snapshotDir, `pre-migrate-${ts}.sqlite`);
|
|
22
|
+
fs.copyFileSync(srcPath, snapshotPath);
|
|
23
|
+
return { snapshotId: snapshotPath, createdAt: new Date().toISOString() };
|
|
24
|
+
}
|
|
25
|
+
export function restoreSnapshot(toConfig, snapshotId) {
|
|
26
|
+
if (toConfig.type !== 'sqlite' || !toConfig.sqlite?.database_path) {
|
|
27
|
+
throw new HazoError({
|
|
28
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
29
|
+
pkg: 'hazo_env',
|
|
30
|
+
message: 'Restore is only supported for SQLite databases.',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (snapshotId.startsWith('no-db:')) {
|
|
34
|
+
throw new HazoError({
|
|
35
|
+
code: 'HAZO_ENV_SNAPSHOT_NOT_FOUND',
|
|
36
|
+
pkg: 'hazo_env',
|
|
37
|
+
message: 'Cannot restore: snapshot was taken when no database existed.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!fs.existsSync(snapshotId)) {
|
|
41
|
+
throw new HazoError({
|
|
42
|
+
code: 'HAZO_ENV_SNAPSHOT_NOT_FOUND',
|
|
43
|
+
pkg: 'hazo_env',
|
|
44
|
+
message: `Snapshot file not found: ${snapshotId}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
fs.copyFileSync(snapshotId, toConfig.sqlite.database_path);
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/migrate/transport.ts"],"names":[],"mappings":"AAEA,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,CAYjB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// hazo_env/src/migrate/transport.ts — resolve transport mode for a migration
|
|
2
|
+
import { HazoError } from 'hazo_core';
|
|
3
|
+
export function resolveTransport(fromConfig, toConfig, requested = 'auto') {
|
|
4
|
+
if (requested === 'ssh') {
|
|
5
|
+
throw new HazoError({
|
|
6
|
+
code: 'HAZO_ENV_NOT_IMPLEMENTED',
|
|
7
|
+
pkg: 'hazo_env',
|
|
8
|
+
message: 'SSH transport is not yet implemented. Use local or api transport.',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (requested === 'api')
|
|
12
|
+
return 'api';
|
|
13
|
+
// auto: both sqlite → local
|
|
14
|
+
if (fromConfig.type === 'sqlite' && toConfig.type === 'sqlite')
|
|
15
|
+
return 'local';
|
|
16
|
+
return 'api';
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HazoEnv, VerifyReport, HashMode } from '../types/index.js';
|
|
2
|
+
export interface VerifyOptions {
|
|
3
|
+
hash?: HashMode;
|
|
4
|
+
samplePct?: number;
|
|
5
|
+
checkOrphans?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function verifyFiles(env: HazoEnv, dataRoot: string, opts?: VerifyOptions): Promise<VerifyReport>;
|
|
8
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/migrate/verify.ts"],"names":[],"mappings":"AAIA,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;CACxB;AAMD,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAgDvB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// hazo_env/src/migrate/verify.ts — post-migration file verification
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function resolveEnvFilesRoot(env, dataRoot) {
|
|
5
|
+
return path.join(dataRoot, env, 'files');
|
|
6
|
+
}
|
|
7
|
+
export async function verifyFiles(env, dataRoot, opts = {}) {
|
|
8
|
+
const { checkOrphans = true } = opts;
|
|
9
|
+
const filesRoot = resolveEnvFilesRoot(env, dataRoot);
|
|
10
|
+
const missing = [];
|
|
11
|
+
const sizeMismatch = [];
|
|
12
|
+
const hashMismatch = [];
|
|
13
|
+
const orphans = [];
|
|
14
|
+
let checked = 0;
|
|
15
|
+
let sentinelOk = true;
|
|
16
|
+
// Layer 1: check sentinel file (data root exists and is readable)
|
|
17
|
+
if (!fs.existsSync(filesRoot)) {
|
|
18
|
+
sentinelOk = false;
|
|
19
|
+
return { ok: false, checked: 0, missing: [], sizeMismatch: [], hashMismatch: [], orphans: [], sentinelOk: false };
|
|
20
|
+
}
|
|
21
|
+
// Layer 2 & 3: walk all files
|
|
22
|
+
function walkDir(dir, relBase) {
|
|
23
|
+
if (!fs.existsSync(dir))
|
|
24
|
+
return [];
|
|
25
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
26
|
+
const relPath = path.join(relBase, entry.name);
|
|
27
|
+
const absPath = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory())
|
|
29
|
+
return walkDir(absPath, relPath);
|
|
30
|
+
return [relPath];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const diskFiles = walkDir(filesRoot, '');
|
|
34
|
+
for (const relPath of diskFiles) {
|
|
35
|
+
const absPath = path.join(filesRoot, relPath);
|
|
36
|
+
try {
|
|
37
|
+
fs.statSync(absPath); // existence + size (stat succeeds = file accessible)
|
|
38
|
+
checked++;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
missing.push(relPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Layer 4: orphan check — files on disk with no corresponding DB tracking row
|
|
45
|
+
// In Phase A, hazo_files integration is not wired; just verify directory is readable
|
|
46
|
+
if (checkOrphans && !fs.existsSync(filesRoot)) {
|
|
47
|
+
orphans.push(filesRoot);
|
|
48
|
+
}
|
|
49
|
+
const ok = missing.length === 0 && sizeMismatch.length === 0 && hashMismatch.length === 0;
|
|
50
|
+
return { ok, checked, missing, sizeMismatch, hashMismatch, orphans, sentinelOk };
|
|
51
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type HazoEnv = 'dev' | 'test' | 'staging' | 'prod' | string;
|
|
|
4
4
|
export type HazoEnvRole = 'development' | 'test' | 'staging' | 'production';
|
|
5
5
|
/** The set of valid environment names for this app, from [env] pattern in INI */
|
|
6
6
|
export type EnvPattern = string[];
|
|
7
|
+
/** Mapping of env name → broad role, for config-driven role resolution */
|
|
8
|
+
export type EnvRoleMap = Record<string, HazoEnvRole>;
|
|
7
9
|
/** Hint about where an environment runs */
|
|
8
10
|
export type HostHint = 'local' | 'remote' | 'unknown';
|
|
9
11
|
/** DB configuration for a single environment (from [db.<env>] in INI) */
|
|
@@ -34,45 +36,63 @@ export interface EnvDescription {
|
|
|
34
36
|
app: string;
|
|
35
37
|
dataRoot: string;
|
|
36
38
|
hostHint: HostHint;
|
|
39
|
+
roles?: EnvRoleMap;
|
|
37
40
|
}
|
|
38
41
|
/** Options for resolveConnectConfig */
|
|
39
42
|
export interface ResolveConnectOptions {
|
|
40
43
|
env?: HazoEnv;
|
|
41
44
|
allowOtherEnv?: boolean;
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
export type TransportMode = 'auto' | 'local' | 'ssh' | 'api';
|
|
47
|
+
export type ScrubMode = 'auto' | 'none';
|
|
48
|
+
export type HashMode = 'none' | 'sample' | 'full';
|
|
49
|
+
export interface MigrationProgress {
|
|
50
|
+
phase: 'validate' | 'snapshot' | 'plan' | 'schema-check' | 'db' | 'files' | 'verify' | 'finalize' | 'done';
|
|
51
|
+
message: string;
|
|
52
|
+
percent?: number;
|
|
53
|
+
}
|
|
44
54
|
export interface MigrationRequest {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
from: HazoEnv;
|
|
56
|
+
to: HazoEnv;
|
|
57
|
+
include?: {
|
|
58
|
+
db?: boolean;
|
|
59
|
+
files?: boolean;
|
|
60
|
+
};
|
|
61
|
+
transport?: TransportMode;
|
|
62
|
+
scrub?: ScrubMode;
|
|
63
|
+
tables?: '*' | string[];
|
|
48
64
|
dryRun?: boolean;
|
|
65
|
+
allowProdTarget?: boolean;
|
|
66
|
+
confirmToken?: string;
|
|
67
|
+
onProgress?: (p: MigrationProgress) => void;
|
|
68
|
+
jobId?: string;
|
|
69
|
+
progressDir?: string;
|
|
49
70
|
}
|
|
50
|
-
/** Result of a completed migration */
|
|
51
71
|
export interface MigrationResult {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
ok: boolean;
|
|
73
|
+
snapshotId?: string;
|
|
74
|
+
db?: {
|
|
75
|
+
tables: number;
|
|
76
|
+
rows: number;
|
|
77
|
+
scrubbed: number;
|
|
78
|
+
};
|
|
79
|
+
files?: {
|
|
80
|
+
copied: number;
|
|
81
|
+
bytes: number;
|
|
82
|
+
placeholdered: number;
|
|
83
|
+
};
|
|
84
|
+
warnings: string[];
|
|
85
|
+
durationMs: number;
|
|
55
86
|
error?: string;
|
|
56
87
|
}
|
|
57
|
-
/** Progress event emitted during a migration run */
|
|
58
|
-
export interface MigrationProgress {
|
|
59
|
-
phase: 'db' | 'files' | 'verify' | 'done';
|
|
60
|
-
message: string;
|
|
61
|
-
percent?: number;
|
|
62
|
-
}
|
|
63
|
-
/** Result of a post-migration file verification */
|
|
64
88
|
export interface VerifyReport {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
table: string;
|
|
73
|
-
column: string;
|
|
74
|
-
/** Transform strategy: replace with fixed value, hash, or nullify */
|
|
75
|
-
strategy: 'replace' | 'hash' | 'nullify';
|
|
76
|
-
replacement?: string;
|
|
89
|
+
ok: boolean;
|
|
90
|
+
checked: number;
|
|
91
|
+
missing: string[];
|
|
92
|
+
sizeMismatch: string[];
|
|
93
|
+
hashMismatch: string[];
|
|
94
|
+
orphans: string[];
|
|
95
|
+
sentinelOk: boolean;
|
|
77
96
|
}
|
|
97
|
+
export type MaskTransform = (value: unknown, key: string, maskKey: string) => unknown;
|
|
78
98
|
//# 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,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;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAID
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hazo_env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Canonical environment resolver — typed env names, per-env DB/file/secret config, doctor and CLI for hazo apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,8 +40,11 @@
|
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"hazo_core": "^1.1.0",
|
|
42
42
|
"hazo_config": "^2.1.10",
|
|
43
|
-
"hazo_connect": "^3.4.
|
|
43
|
+
"hazo_connect": "^3.4.1",
|
|
44
44
|
"hazo_files": "^3.0.0",
|
|
45
|
+
"hazo_secure": "^1.2.0",
|
|
46
|
+
"hazo_audit": "^2.1.0",
|
|
47
|
+
"hazo_pdf": "^2.0.0",
|
|
45
48
|
"react": "^18.0.0 || ^19.0.0",
|
|
46
49
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
47
50
|
"next": "^14.0.0 || ^16.0.0"
|
|
@@ -53,6 +56,15 @@
|
|
|
53
56
|
"hazo_files": {
|
|
54
57
|
"optional": true
|
|
55
58
|
},
|
|
59
|
+
"hazo_secure": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"hazo_audit": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"hazo_pdf": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
56
68
|
"react": {
|
|
57
69
|
"optional": true
|
|
58
70
|
},
|
|
@@ -74,7 +86,7 @@
|
|
|
74
86
|
"@types/react-dom": "^19.0.0",
|
|
75
87
|
"hazo_core": "^1.1.0",
|
|
76
88
|
"hazo_config": "^2.1.10",
|
|
77
|
-
"hazo_connect": "^3.4.
|
|
89
|
+
"hazo_connect": "^3.4.1",
|
|
78
90
|
"hazo_files": "^3.0.0",
|
|
79
91
|
"next": "^16.0.10",
|
|
80
92
|
"react": "^19.0.0",
|