feat(FN-3503): add project-node path mapping persistence APIs and schema

Adds project-node path mapping infrastructure in the core database layer (schema, migration backfill, and persistence APIs in `central-core.ts` and `central-db.ts`) with accompanying documentation. Also introduces a review-before-apply draft gate for agent onboarding and the agent detail view's mail

Fusion-Task-Id: FN-3503
This commit is contained in:
Fusion
2026-05-07 12:32:49 -07:00
committed by gsxdsm
parent ad5bed22f8
commit b434cc2d38
9 changed files with 568 additions and 31 deletions

View File

@@ -23,7 +23,7 @@ export { toJson, toJsonNullable, fromJson };
// ── Schema Definition ───────────────────────────────────────────────────
const CENTRAL_SCHEMA_VERSION = 7;
const CENTRAL_SCHEMA_VERSION = 8;
const CENTRAL_SCHEMA_SQL = `
-- Projects table (project registry)
@@ -42,6 +42,20 @@ CREATE TABLE IF NOT EXISTS projects (
CREATE INDEX IF NOT EXISTS idxProjectsPath ON projects(path);
CREATE INDEX IF NOT EXISTS idxProjectsStatus ON projects(status);
-- Per-project, per-node working directory mappings
CREATE TABLE IF NOT EXISTS projectNodePathMappings (
projectId TEXT NOT NULL,
nodeId TEXT NOT NULL,
path TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
PRIMARY KEY (projectId, nodeId),
FOREIGN KEY (projectId) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idxProjectNodePathMappingsProjectId ON projectNodePathMappings(projectId);
CREATE INDEX IF NOT EXISTS idxProjectNodePathMappingsNodeId ON projectNodePathMappings(nodeId);
-- Project health table (mutable state, updated frequently)
CREATE TABLE IF NOT EXISTS projectHealth (
projectId TEXT PRIMARY KEY,
@@ -256,6 +270,21 @@ CREATE INDEX IF NOT EXISTS idxManagedDockerNodesNodeId ON managedDockerNodes(nod
// V7 migration adds dockerConfig persistence to nodes for Docker-managed runtime config updates.
const CENTRAL_SCHEMA_V8_MIGRATION_SQL = `
CREATE TABLE IF NOT EXISTS projectNodePathMappings (
projectId TEXT NOT NULL,
nodeId TEXT NOT NULL,
path TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
PRIMARY KEY (projectId, nodeId),
FOREIGN KEY (projectId) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (nodeId) REFERENCES nodes(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idxProjectNodePathMappingsProjectId ON projectNodePathMappings(projectId);
CREATE INDEX IF NOT EXISTS idxProjectNodePathMappingsNodeId ON projectNodePathMappings(nodeId);
`;
// ── Central Database Class ────────────────────────────────────────────────
export class CentralDatabase {
@@ -345,6 +374,39 @@ export class CentralDatabase {
migrated = true;
}
if (currentVersion < 8) {
this.db.exec(CENTRAL_SCHEMA_V8_MIGRATION_SQL);
const localNodeRow = this.db
.prepare("SELECT id FROM nodes WHERE type = 'local' ORDER BY createdAt ASC LIMIT 1")
.get() as { id: string } | undefined;
if (localNodeRow) {
this.db.prepare(
`INSERT OR IGNORE INTO projectNodePathMappings (projectId, nodeId, path, createdAt, updatedAt)
SELECT id, ?, path, createdAt, updatedAt
FROM projects`
).run(localNodeRow.id);
this.db.prepare(
`UPDATE projectNodePathMappings
SET path = (
SELECT projects.path
FROM projects
WHERE projects.id = projectNodePathMappings.projectId
),
updatedAt = (
SELECT projects.updatedAt
FROM projects
WHERE projects.id = projectNodePathMappings.projectId
)
WHERE nodeId = ?`
).run(localNodeRow.id);
}
migrated = true;
}
if (migrated) {
this.db
.prepare("INSERT INTO __meta (key, value) VALUES ('schemaVersion', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")