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

273 lines
8.1 KiB
JavaScript

/**
* Shared helpers for Labs routes
* Used by all sub-modules in the labs/ directory
*/
import { dbGet, dbRun, dbAll, sqlNow, toBool } from '../../../database/connection.js';
import { generateBaseId } from '../../../utils/baseId.js';
export function generateId(prefix) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
export function generateLabId() {
return `lab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
export function slugify(text) {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
/**
* Transform a database node to MindWorkflow FlowNode format
* @param {Object} node - Node from labs_nodes table
* @param {Array} edges - All edges for the lab (optional, for connections)
* @returns {Object} FlowNode format
*/
export function toFlowNode(node, edges = []) {
// Parse JSONB fields
let meta = {};
let aiConfig = {};
let uiConfig = {};
if (node.meta) {
meta = typeof node.meta === 'string' ? JSON.parse(node.meta) : node.meta;
}
if (node.ai_config) {
aiConfig = typeof node.ai_config === 'string' ? JSON.parse(node.ai_config) : node.ai_config;
}
if (node.ui_config) {
uiConfig = typeof node.ui_config === 'string' ? JSON.parse(node.ui_config) : node.ui_config;
}
// Extract position and dimensions from meta
const posX = meta.position?.x || 0;
const posY = meta.position?.y || 0;
const width = meta.width || 300;
const height = meta.height || 200;
// Build connections from edges
const incoming = edges
.filter(e => e.target_node_id === node.node_id)
.map(e => ({
edge_id: e.edge_id || `${e.source_node_id}-${e.target_node_id}`,
from: e.source_node_id,
routing: e.source_handle || null
}));
const outgoing = edges
.filter(e => e.source_node_id === node.node_id)
.map(e => ({
edge_id: e.edge_id || `${e.source_node_id}-${e.target_node_id}`,
to: e.target_node_id,
routing: e.target_handle || null
}));
// Return MindWorkflow FlowNode format
return {
node_id: node.node_id,
type: node.type,
title: node.title,
content: node.content || '',
content_type: meta.content_type || 'text/plain',
meta: meta.config || {},
ai: aiConfig,
ui: {
color: uiConfig.color || '#6366f1',
bbox: {
x1: posX,
y1: posY,
x2: posX + width,
y2: posY + height
}
},
ai_visible: node.ai_visible !== false,
connections: { incoming, outgoing },
// Also include raw data for compatibility
position_x: posX,
position_y: posY,
width,
height
};
}
/**
* Helper: Create lab nodes table with standard columns
*/
export async function createLabNodesTable(tableName, projectId, userId) {
// Create the table
const baseId = generateBaseId();
const result = await dbRun(`
INSERT INTO universal_tables (
project_id,
name,
display_name,
table_type,
icon,
base_id,
created_by,
created_at,
updated_at
) VALUES (?, ?, ?, 'lab_nodes', '🔬', ?, ?, ${sqlNow()}, ${sqlNow()})
`, [projectId, tableName, tableName, baseId, userId]);
const tableId = result.lastInsertRowid || result.lastID;
// Create standard columns for lab nodes
const columns = [
{ name: 'node_id', displayName: 'Node ID', type: 'text', required: true, order: 1 },
{ name: 'type_key', displayName: 'Type', type: 'text', required: true, order: 2 },
{ name: 'title', displayName: 'Title', type: 'text', required: true, order: 3 },
{ name: 'content', displayName: 'Content', type: 'text', required: false, order: 4 },
{ name: 'position_x', displayName: 'X', type: 'number', required: false, order: 5 },
{ name: 'position_y', displayName: 'Y', type: 'number', required: false, order: 6 },
{ name: 'width', displayName: 'Width', type: 'number', required: false, order: 7 },
{ name: 'height', displayName: 'Height', type: 'number', required: false, order: 8 },
{ name: 'edges', displayName: 'Edges', type: 'text', required: false, order: 9 }, // JSON array of node_ids
{ name: 'ai_agent_id', displayName: 'AI Agent', type: 'number', required: false, order: 10 },
{ name: 'config', displayName: 'Config', type: 'text', required: false, order: 11 }, // JSON
{ name: 'order_index', displayName: 'Order', type: 'number', required: false, order: 12 }
];
for (const col of columns) {
await dbRun(`
INSERT INTO table_columns (
table_id,
column_name,
display_name,
type,
is_required,
order_index,
is_visible,
config,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [
tableId,
col.name,
col.displayName,
col.type,
toBool(col.required),
col.order,
toBool(true),
JSON.stringify({})
]);
}
return { id: tableId, name: tableName };
}
/**
* Helper: Insert row into table
*/
export async function insertRow(tableId, data, userId = null) {
const baseId = generateBaseId();
const result = await dbRun(`
INSERT INTO table_rows (table_id, base_id, data, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [tableId, baseId, JSON.stringify(data), userId]);
return { id: result.lastInsertRowid || result.lastID, ...data };
}
/**
* Helper: Get table rows
*/
export async function getTableRows(tableId) {
const rows = await dbAll(`
SELECT id, base_id, data, created_at, updated_at
FROM table_rows
WHERE table_id = ?
ORDER BY id
`, [tableId]);
return rows.map(row => {
// JSONB may be auto-parsed or string
const data = typeof row.data === 'string' ? JSON.parse(row.data || '{}') : (row.data || {});
return {
id: row.id,
base_id: row.base_id,
...data,
created_at: row.created_at,
updated_at: row.updated_at
};
});
}
/**
* Helper: Update row by field
*/
export async function updateRowByField(tableId, fieldName, fieldValue, updates) {
// Handle special case where fieldName is 'id' (row ID)
if (fieldName === 'id') {
const row = await dbGet(`
SELECT id, data FROM table_rows WHERE table_id = ? AND id = ?
`, [tableId, fieldValue]);
if (!row) {
throw new Error(`Row with id=${fieldValue} not found`);
}
const data = typeof row.data === 'string' ? JSON.parse(row.data || '{}') : (row.data || {});
const newData = { ...data, ...updates };
await dbRun(`
UPDATE table_rows SET data = ?, updated_at = ${sqlNow()}
WHERE id = ?
`, [JSON.stringify(newData), row.id]);
return { id: row.id, ...newData };
}
// Original logic for data field matching
const rows = await dbAll(`
SELECT id, data FROM table_rows WHERE table_id = ?
`, [tableId]);
for (const row of rows) {
// JSONB may be auto-parsed or string
const data = typeof row.data === 'string' ? JSON.parse(row.data || '{}') : (row.data || {});
if (data[fieldName] === fieldValue) {
const newData = { ...data, ...updates };
await dbRun(`
UPDATE table_rows SET data = ?, updated_at = ${sqlNow()}
WHERE id = ?
`, [JSON.stringify(newData), row.id]);
return { id: row.id, ...newData };
}
}
throw new Error(`Row with ${fieldName}=${fieldValue} not found`);
}
/**
* Helper: Delete row by field
*/
export async function deleteRowByField(tableId, fieldName, fieldValue) {
// Handle special case where fieldName is 'id' (row ID)
if (fieldName === 'id') {
const result = await dbRun('DELETE FROM table_rows WHERE table_id = ? AND id = ?', [tableId, fieldValue]);
return result.changes > 0;
}
// Original logic for data field matching
const rows = await dbAll(`
SELECT id, data FROM table_rows WHERE table_id = ?
`, [tableId]);
for (const row of rows) {
// JSONB may be auto-parsed or string
const data = typeof row.data === 'string' ? JSON.parse(row.data || '{}') : (row.data || {});
if (data[fieldName] === fieldValue) {
await dbRun('DELETE FROM table_rows WHERE id = ?', [row.id]);
return true;
}
}
return false;
}