godcrm/scripts/migrate-doc-tables-widget-level.js
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

136 lines
5.3 KiB
JavaScript

#!/usr/bin/env node
/**
* ADR-0003 widget-embed Phase 1, frontend-backend meta-schema sync.
*
* Brings every existing per-document content table (`universal_tables` row
* with `table_type = 'document_content'`) in line with the new
* DOCUMENT_TABLE_COLUMNS template in
* `backend/routes/v3/documents/_helpers.js`:
*
* 1. `level` select column — config.options extended with
* ['atom', 'ticket', 'image', 'page_break', 'widget'] (was h1/h2/h3/text/divider
* only in the template; UI was already emitting the extras for a while, so
* existing rows may carry values the DB config didn't list — this realigns).
* 2. `widget_ref` relation column — added if missing, targets `widgets` table
* (target_table_id stored as the string 'widgets' — the existing relation
* UI resolves named targets for system tables; otherwise null).
* 3. `settings_override` text column — added if missing (stores JSON blob
* of preset-local overrides for an embedded widget).
*
* Idempotent: re-running finds existing columns / options and makes zero
* writes.
*
* Usage:
* node scripts/migrate-doc-tables-widget-level.js # dry-run
* node scripts/migrate-doc-tables-widget-level.js --apply
*/
import pg from 'pg';
const { Pool } = pg;
const APPLY = process.argv.includes('--apply');
const pool = new Pool({
host: process.env.POSTGRES_HOST || 'localhost',
port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
database: process.env.POSTGRES_DB || 'godcrm_prod',
user: process.env.POSTGRES_USER || 'godcrm',
password: process.env.POSTGRES_PASSWORD || 'godcrm_dev_2026',
});
const TARGET_LEVEL_OPTIONS = [
'h1', 'h2', 'h3', 'text', 'atom', 'ticket', 'image', 'divider', 'page_break', 'widget',
];
function log(...a) { console.log('[migrate-doc-tables-widget-level]', ...a); }
async function main() {
const docTables = (await pool.query(
`SELECT id, name, project_id FROM universal_tables
WHERE table_type = 'document_content' ORDER BY id`
)).rows;
log(`found ${docTables.length} document_content tables`);
let levelAligned = 0, widgetRefAdded = 0, settingsAdded = 0, untouched = 0;
for (const dt of docTables) {
const cols = (await pool.query(
`SELECT id, column_name, type, config, order_index
FROM table_columns WHERE table_id = $1 ORDER BY order_index, id`,
[dt.id]
)).rows;
let touchedThisTable = false;
// 1) level — extend select options
const levelCol = cols.find(c => c.column_name === 'level');
if (levelCol) {
const cfg = typeof levelCol.config === 'string'
? (levelCol.config ? JSON.parse(levelCol.config) : {})
: (levelCol.config || {});
const existingOpts = Array.isArray(cfg.options) ? cfg.options : [];
const merged = Array.from(new Set([...existingOpts, ...TARGET_LEVEL_OPTIONS]));
if (merged.length !== existingOpts.length) {
const newCfg = { ...cfg, options: merged };
log(` table ${dt.id} (${dt.name}): level options ${existingOpts.join(',')}${merged.join(',')}`);
if (APPLY) {
await pool.query(
`UPDATE table_columns SET config = $1 WHERE id = $2`,
[JSON.stringify(newCfg), levelCol.id]
);
}
levelAligned++;
touchedThisTable = true;
}
} else {
log(` table ${dt.id} (${dt.name}): WARN no 'level' column — skipping`);
}
// 2) widget_ref — relation
if (!cols.find(c => c.column_name === 'widget_ref')) {
const nextOrder = (cols.reduce((m, c) => Math.max(m, c.order_index || 0), 0)) + 1;
log(` table ${dt.id} (${dt.name}): add widget_ref (relation) order=${nextOrder}`);
if (APPLY) {
await pool.query(
`INSERT INTO table_columns (table_id, column_name, display_name, type, order_index, is_visible, config)
VALUES ($1, 'widget_ref', 'Widget Ref', 'relation', $2, 1, $3)`,
[dt.id, nextOrder, JSON.stringify({ relation_table: null, description: 'ADR-0003: embedded widget (widgets.id)' })]
);
}
widgetRefAdded++;
touchedThisTable = true;
}
// 3) settings_override — text (JSON blob)
if (!cols.find(c => c.column_name === 'settings_override')) {
const nextOrder = (cols.reduce((m, c) => Math.max(m, c.order_index || 0), 0)) + 2;
log(` table ${dt.id} (${dt.name}): add settings_override (text) order=${nextOrder}`);
if (APPLY) {
await pool.query(
`INSERT INTO table_columns (table_id, column_name, display_name, type, order_index, is_visible, config)
VALUES ($1, 'settings_override', 'Settings Override', 'text', $2, 0, $3)`,
[dt.id, nextOrder, JSON.stringify({ hidden_in_view: true, description: 'ADR-0003: JSON overrides for embedded widget' })]
);
}
settingsAdded++;
touchedThisTable = true;
}
if (!touchedThisTable) untouched++;
}
log('---');
log(`tables total : ${docTables.length}`);
log(`level extended : ${levelAligned}`);
log(`widget_ref added : ${widgetRefAdded}`);
log(`settings_override+ : ${settingsAdded}`);
log(`already up to date : ${untouched} ${APPLY ? '' : '(DRY RUN — use --apply to execute)'}`);
await pool.end();
}
main().catch(err => {
console.error('[migrate-doc-tables-widget-level] FATAL:', err);
process.exit(1);
});