Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
278 lines
8 KiB
JavaScript
278 lines
8 KiB
JavaScript
/**
|
|
* Labs Projects Routes (v4)
|
|
* CRUD for lab projects (both registry-based and MindWorkflow direct)
|
|
*/
|
|
import { Router } from 'express';
|
|
import { dbGet, dbRun, dbAll } from '../../../database/connection.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, created, notFound, badRequest, error } from '../../../utils/response.js';
|
|
import {
|
|
generateId,
|
|
slugify,
|
|
toFlowNode,
|
|
createLabNodesTable,
|
|
insertRow,
|
|
getTableRows,
|
|
updateRowByField,
|
|
deleteRowByField
|
|
} from './_helpers.js';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* GET /api/v3/labs/projects
|
|
* List labs - supports two modes:
|
|
* 1. With registry_table_id - uses table_rows (legacy)
|
|
* 2. Without registry_table_id - uses labs table directly (MindWorkflow)
|
|
*/
|
|
router.get('/projects', async (req, res) => {
|
|
try {
|
|
const { registry_table_id, space_id } = req.query;
|
|
|
|
// Mode 1: Registry-based (legacy)
|
|
if (registry_table_id) {
|
|
const labs = await getTableRows(registry_table_id);
|
|
return success(res, labs);
|
|
}
|
|
|
|
// Mode 2: Direct labs table (MindWorkflow)
|
|
let query = 'SELECT * FROM labs';
|
|
const params = [];
|
|
|
|
if (space_id) {
|
|
query += ' WHERE space_id = $1';
|
|
params.push(space_id);
|
|
}
|
|
|
|
query += ' ORDER BY created_at DESC';
|
|
|
|
const labs = await dbAll(query, params);
|
|
success(res, labs);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get labs');
|
|
badRequest(res, 'Failed to get labs');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/labs/projects
|
|
* Create lab - supports two modes:
|
|
* 1. With registry_table_id - uses table_rows (legacy)
|
|
* 2. Without registry_table_id - uses labs table directly (MindWorkflow)
|
|
*/
|
|
router.post('/projects', async (req, res) => {
|
|
try {
|
|
const { registry_table_id, name, title, description, icon, space_id } = req.body;
|
|
const userId = req.user?.id;
|
|
|
|
// Use title or name (MindWorkflow uses title, legacy uses name)
|
|
const labTitle = title || name;
|
|
|
|
if (!labTitle) {
|
|
return badRequest(res, 'Lab name/title is required');
|
|
}
|
|
|
|
// Mode 1: Registry-based (legacy)
|
|
if (registry_table_id) {
|
|
// Get registry table info to find project_id
|
|
const registryTable = await dbGet(`
|
|
SELECT project_id FROM universal_tables WHERE id = ?
|
|
`, [registry_table_id]);
|
|
|
|
if (!registryTable) {
|
|
return notFound(res, 'Registry table not found');
|
|
}
|
|
|
|
// Generate slug
|
|
const slug = slugify(labTitle);
|
|
|
|
// Create nodes table for this lab
|
|
const timestamp = Date.now();
|
|
const nodesTableName = `lab_${slug}_${timestamp}`;
|
|
const nodesTable = await createLabNodesTable(nodesTableName, registryTable.project_id, userId);
|
|
|
|
// Add entry to registry table
|
|
const labData = {
|
|
name: labTitle,
|
|
description: description || '',
|
|
slug,
|
|
table_id: nodesTable.id,
|
|
icon: icon || '🧪',
|
|
status: 'draft'
|
|
};
|
|
|
|
const labRow = await insertRow(registry_table_id, labData, userId);
|
|
|
|
apiLogger.info(`[Labs v4] Created lab "${labTitle}" with nodes table ${nodesTable.id}`);
|
|
|
|
return created(res, {
|
|
lab_id: labRow.id,
|
|
table_id: nodesTable.id,
|
|
slug,
|
|
name: labTitle
|
|
});
|
|
}
|
|
|
|
// Mode 2: Direct labs table (MindWorkflow)
|
|
const labId = generateId('lab');
|
|
|
|
await dbRun(`
|
|
INSERT INTO labs (lab_id, space_id, title, description, settings, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
|
`, [labId, space_id || null, labTitle, description || '', JSON.stringify({})]);
|
|
|
|
const lab = await dbGet('SELECT * FROM labs WHERE lab_id = $1', [labId]);
|
|
|
|
apiLogger.info({ labId, title: labTitle }, 'Created lab project');
|
|
|
|
created(res, lab);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to create lab');
|
|
error(res, 'CREATE_LAB_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/labs/projects/:id
|
|
* Get lab with nodes
|
|
*/
|
|
router.get('/projects/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { registry_table_id } = req.query;
|
|
|
|
// Mode 1: Registry-based (legacy)
|
|
if (registry_table_id) {
|
|
const labRows = await getTableRows(registry_table_id);
|
|
const lab = labRows.find(row => row.id == id);
|
|
|
|
if (!lab) {
|
|
return notFound(res, 'Lab not found');
|
|
}
|
|
|
|
// Get nodes from lab's table
|
|
let nodes = [];
|
|
if (lab.table_id) {
|
|
nodes = await getTableRows(lab.table_id);
|
|
}
|
|
|
|
return success(res, { ...lab, nodes });
|
|
}
|
|
|
|
// Mode 2: Direct labs table (MindWorkflow)
|
|
const lab = await dbGet('SELECT * FROM labs WHERE lab_id = $1', [id]);
|
|
if (!lab) {
|
|
return notFound(res, 'Lab not found');
|
|
}
|
|
|
|
// Get nodes from labs_nodes table
|
|
const nodes = await dbAll('SELECT * FROM labs_nodes WHERE lab_id = $1 ORDER BY created_at', [id]);
|
|
|
|
// Get edges from labs_edges table
|
|
const edges = await dbAll('SELECT * FROM labs_edges WHERE lab_id = $1', [id]);
|
|
|
|
// Transform nodes to MindWorkflow FlowNode format
|
|
const parsedNodes = nodes.map(node => toFlowNode(node, edges));
|
|
|
|
// Transform edges to MindWorkflow FlowEdge format
|
|
const parsedEdges = edges.map(edge => ({
|
|
from: edge.source_node_id,
|
|
to: edge.target_node_id,
|
|
label: null,
|
|
sourceHandle: edge.source_handle || null,
|
|
targetHandle: edge.target_handle || null
|
|
}));
|
|
|
|
// Build response in MindWorkflow ProjectFlow format
|
|
const projectFlow = {
|
|
project_id: lab.lab_id,
|
|
title: lab.title,
|
|
description: lab.description || '',
|
|
created_at: lab.created_at,
|
|
updated_at: lab.updated_at,
|
|
settings: typeof lab.settings === 'string' ? JSON.parse(lab.settings) : (lab.settings || {}),
|
|
nodes: parsedNodes,
|
|
edges: parsedEdges,
|
|
schemas: {},
|
|
is_public: false,
|
|
mode: 'editing',
|
|
role: 'owner'
|
|
};
|
|
|
|
success(res, projectFlow);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to get lab');
|
|
badRequest(res, 'Failed to get lab');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /api/v3/labs/projects/:id
|
|
* Update lab
|
|
*/
|
|
router.put('/projects/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { registry_table_id, name, description, icon, status } = req.body;
|
|
|
|
if (!registry_table_id) {
|
|
return badRequest(res, 'registry_table_id is required');
|
|
}
|
|
|
|
const updates = {};
|
|
if (name !== undefined) updates.name = name;
|
|
if (description !== undefined) updates.description = description;
|
|
if (icon !== undefined) updates.icon = icon;
|
|
if (status !== undefined) updates.status = status;
|
|
|
|
const updatedLab = await updateRowByField(registry_table_id, 'id', parseInt(id), updates);
|
|
success(res, updatedLab);
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to update lab');
|
|
badRequest(res, 'Failed to update lab');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/v3/labs/projects/:id
|
|
* Delete lab + nodes table
|
|
*/
|
|
router.delete('/projects/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { registry_table_id, delete_table = true } = req.body;
|
|
|
|
if (!registry_table_id) {
|
|
return badRequest(res, 'registry_table_id is required');
|
|
}
|
|
|
|
// Get lab info first
|
|
const labRows = await getTableRows(registry_table_id);
|
|
const lab = labRows.find(row => row.id == id);
|
|
|
|
if (!lab) {
|
|
return notFound(res, 'Lab not found');
|
|
}
|
|
|
|
// Delete nodes table if requested
|
|
if (delete_table && lab.table_id) {
|
|
await dbRun(`DELETE FROM table_rows WHERE table_id = ?`, [lab.table_id]);
|
|
await dbRun(`DELETE FROM table_columns WHERE table_id = ?`, [lab.table_id]);
|
|
await dbRun(`DELETE FROM universal_tables WHERE id = ?`, [lab.table_id]);
|
|
}
|
|
|
|
// Delete lab from registry
|
|
const deleted = await deleteRowByField(registry_table_id, 'id', parseInt(id));
|
|
|
|
if (!deleted) {
|
|
return notFound(res, 'Lab not found');
|
|
}
|
|
|
|
success(res, { deleted: true, table_deleted: delete_table && !!lab.table_id });
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'Failed to delete lab');
|
|
badRequest(res, 'Failed to delete lab');
|
|
}
|
|
});
|
|
|
|
export default router;
|