godcrm/backend/routes/v3/labs/projects.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

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;