godcrm/scripts/migrate-doc-status-to-relation.mjs
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

132 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
// Migrate all documents_registry tables from legacy `status` (select text)
// to canonical `status_id` (relation → _doc_statuses). Idempotent.
//
// For each documents_registry table:
// 1. Ensure `status_id` relation column exists (→ _doc_statuses.id)
// 2. Ensure `verified` checkbox column exists (Gate A plan approval)
// 3. Backfill data->>'status_id' from data->>'status' slug on every row
// that is missing a status_id value.
//
// Safe to re-run: columns are only added if missing; backfill only
// populates rows whose status_id is NULL/missing.
//
// Usage: node scripts/migrate-doc-status-to-relation.mjs [--dry-run]
import { dbAll, dbGet, dbRun, sqlNow } from '../backend/database/connection.js';
const DRY = process.argv.includes('--dry-run');
const log = (...a) => console.log('[migrate-status]', ...a);
async function findStatusesTable() {
const row = await dbGet(`SELECT id FROM universal_tables WHERE name = '_doc_statuses' LIMIT 1`);
if (!row) throw new Error('_doc_statuses table not found — create it first');
return row.id;
}
async function loadStatusMap(statusesTableId) {
const rows = await dbAll(`SELECT id, data FROM table_rows WHERE table_id = ?`, [statusesTableId]);
const slugToId = new Map();
for (const r of rows) {
const d = typeof r.data === 'string' ? JSON.parse(r.data) : (r.data || {});
if (d.slug) slugToId.set(String(d.slug), r.id);
}
return slugToId;
}
async function ensureColumn(tableId, spec) {
const existing = await dbGet(
`SELECT id FROM table_columns WHERE table_id = ? AND column_name = ? LIMIT 1`,
[tableId, spec.column_name]
);
if (existing) {
log(` ✓ column ${spec.column_name} already on ${tableId}`);
return false;
}
if (DRY) {
log(` [dry] would add column ${spec.column_name} to ${tableId}`);
return true;
}
await dbRun(
`INSERT INTO table_columns (table_id, column_name, display_name, type, order_index, is_visible, config)
VALUES (?, ?, ?, ?, ?, 1, ?)`,
[tableId, spec.column_name, spec.display_name, spec.type, spec.order_index, spec.config ? JSON.stringify(spec.config) : null]
);
log(` + added column ${spec.column_name} (${spec.type}) to ${tableId}`);
return true;
}
async function backfillRows(tableId, slugToId) {
const rows = await dbAll(`SELECT id, data FROM table_rows WHERE table_id = ?`, [tableId]);
let updated = 0;
let skipped = 0;
for (const r of rows) {
const data = typeof r.data === 'string' ? JSON.parse(r.data) : (r.data || {});
const currentSlug = data.status || null;
const currentSid = data.status_id || null;
if (currentSid) { skipped++; continue; }
if (!currentSlug) { skipped++; continue; }
const sid = slugToId.get(String(currentSlug));
if (!sid) {
log(` ! row ${r.id}: slug "${currentSlug}" has no match in _doc_statuses`);
skipped++;
continue;
}
if (DRY) {
log(` [dry] row ${r.id}: status "${currentSlug}" → status_id ${sid}`);
} else {
const newData = { ...data, status_id: sid };
await dbRun(
`UPDATE table_rows SET data = ?::jsonb, updated_at = ${sqlNow()} WHERE id = ? AND table_id = ?`,
[JSON.stringify(newData), r.id, tableId]
);
}
updated++;
}
log(` backfill: ${updated} updated, ${skipped} skipped`);
return { updated, skipped };
}
async function main() {
log(DRY ? 'DRY RUN' : 'LIVE RUN');
const statusesTableId = await findStatusesTable();
log(`_doc_statuses table_id = ${statusesTableId}`);
const slugToId = await loadStatusMap(statusesTableId);
log(`status slug → id map:`, Object.fromEntries(slugToId));
const registries = await dbAll(
`SELECT id, name, project_id FROM universal_tables WHERE table_type = 'documents_registry' ORDER BY id`
);
log(`found ${registries.length} documents_registry tables`);
const summary = [];
for (const reg of registries) {
log(`\n→ registry ${reg.id} (project ${reg.project_id}, name=${reg.name})`);
await ensureColumn(reg.id, {
column_name: 'status_id',
display_name: 'Status',
type: 'relation',
order_index: 9,
config: { target_table_id: statusesTableId, display_column: 'label', icon: '🏷️' }
});
await ensureColumn(reg.id, {
column_name: 'verified',
display_name: 'Verified (Plan)',
type: 'checkbox',
order_index: 10,
config: {}
});
const { updated, skipped } = await backfillRows(reg.id, slugToId);
summary.push({ table_id: reg.id, updated, skipped });
}
log('\n=== summary ===');
for (const s of summary) log(` t${s.table_id}: updated=${s.updated} skipped=${s.skipped}`);
log(DRY ? '\nDRY RUN complete — no changes written.' : '\nmigration complete.');
process.exit(0);
}
main().catch((e) => {
console.error('[migrate-status] FAILED:', e);
process.exit(1);
});