godcrm/backend/services/atoms-archive.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

147 lines
4.6 KiB
JavaScript

/**
* Atoms Archive — versioning hook for atoms_v2 (table 3574) per ADR-0001.
*
* On every UPDATE of a row in table 3574, snapshot the OLD row into
* `atoms_archive` (table 7260) before applying the UPDATE, then bump
* `version` on the new row.
*
* App-level hook (not a DB trigger) so it follows the JS code path and
* can be skipped/extended per caller.
*
* If the archive table doesn't exist yet (parallel rollout), we log a
* warning and DO NOT block the UPDATE — versioning is best-effort.
*/
import { dbGet, dbRun, sqlNow } from '../database/connection.js';
import { generateBaseId } from '../utils/baseId.js';
export const ATOMS_V2_TABLE_ID = 3574;
export const ATOMS_ARCHIVE_TABLE_ID = 7260;
/**
* Safe parse — handles both string (SQLite) and object (PG JSONB).
*/
function parseRowData(data) {
if (data === null || data === undefined) return {};
if (typeof data === 'object') return data;
try { return JSON.parse(data); } catch { return {}; }
}
/**
* Returns true if the given table_id is the atoms_v2 table that needs
* versioning. Centralised so the constant only lives here.
*/
export function isAtomsV2Table(table_id) {
return Number(table_id) === ATOMS_V2_TABLE_ID;
}
/**
* Snapshot the OLD row into atoms_archive and return the new version
* number (`old.version + 1`, defaulting to 1 -> 2 if absent).
*
* @param {object} opts
* @param {number} opts.atom_id — id in atoms_v2 (table 3574)
* @param {object} opts.oldRow — full row { id, data, ... }
* @param {string} [opts.changeReason] — optional comment
* @param {number|null} [opts.changedByUser] — Users(1782) row id
* @param {number|null} [opts.changedByAgent]— Agents(1784) row id
* @returns {Promise<number>} the new version to write on UPDATE
*/
export async function archiveAndBumpVersion({
atom_id,
oldRow,
changeReason = null,
changedByUser = null,
changedByAgent = null,
} = {}) {
const oldData = parseRowData(oldRow?.data);
const oldVersion = Number(oldData.version) || 1;
const nextVersion = oldVersion + 1;
// Best-effort archive write. If the archive table or its columns are not
// yet provisioned (parallel rollout), do not block the UPDATE.
try {
// Verify the archive table is materialised before writing.
const archiveTable = await dbGet(
'SELECT id FROM universal_tables WHERE id = ?',
[ATOMS_ARCHIVE_TABLE_ID]
);
if (!archiveTable) {
console.warn(
`[atoms-archive] table ${ATOMS_ARCHIVE_TABLE_ID} not found — ` +
`skipping snapshot for atom_id=${atom_id}. ` +
`Parallel migration may not have created atoms_archive yet.`
);
return nextVersion;
}
const snapshotData = {
atom_id: Number(atom_id),
document_id: oldData.document_id != null ? Number(oldData.document_id) : null,
version: oldVersion,
changed_at: new Date().toISOString(),
change_reason: changeReason,
snapshot: oldData,
...(changedByAgent != null ? { changed_by_agent: String(changedByAgent) } : {}),
...(changedByUser != null ? { changed_by_user: String(changedByUser) } : {}),
};
await dbRun(
`INSERT INTO table_rows (table_id, base_id, data, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})`,
[
ATOMS_ARCHIVE_TABLE_ID,
generateBaseId(),
JSON.stringify(snapshotData),
changedByUser || 1,
]
);
} catch (err) {
console.warn(
`[atoms-archive] snapshot failed for atom_id=${atom_id}: ${err.message}. ` +
`UPDATE will still proceed with version bump.`
);
}
return nextVersion;
}
/**
* Convenience: given table_id + atom_id + about-to-be-merged data, if this
* is atoms_v2, fetches the old row, archives it, and stamps the new
* `version` onto the data object. Returns the (possibly mutated) data
* object — caller should JSON.stringify and UPDATE as usual.
*
* Safe no-op if table_id !== ATOMS_V2_TABLE_ID.
*/
export async function applyAtomVersioning({
table_id,
row_id,
newData,
oldRow = null,
changedByUser = null,
changedByAgent = null,
changeReason = null,
} = {}) {
if (!isAtomsV2Table(table_id)) return newData;
if (!row_id) return newData;
let row = oldRow;
if (!row) {
row = await dbGet(
'SELECT id, data FROM table_rows WHERE id = ? AND table_id = ?',
[row_id, ATOMS_V2_TABLE_ID]
);
}
if (!row) return newData; // nothing to snapshot
const nextVersion = await archiveAndBumpVersion({
atom_id: row.id,
oldRow: row,
changeReason,
changedByUser,
changedByAgent,
});
return { ...newData, version: nextVersion };
}