godcrm/backend/routes/v3/tables/tableCreateController.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

348 lines
11 KiB
JavaScript

/**
* Table creation and single-table read controller
* Handles: GET /users, GET /tables/:tableId, POST /tables/create-calendar, POST /tables
*/
import express from 'express';
import { dbAll, dbGet, dbRun, toBool, sqlNow } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, notFound, badRequest, forbidden, unauthorized, error } from '../../../utils/response.js';
import { checkTableAccess } from './helpers.js';
const router = express.Router();
/**
* GET /api/v3/users
* Get all system users
*/
router.get('/users', async (req, res) => {
try {
const users = await dbAll(`
SELECT u.id, u.name, u.email,
CASE WHEN length(u.avatar) > 2048 THEN NULL ELSE u.avatar END as avatar,
u.user_type,
u.managed_by_agent_table_id, u.managed_by_agent_row_id,
(SELECT r.data->>'icon' FROM table_rows r
WHERE r.id = u.managed_by_agent_row_id
AND r.table_id = u.managed_by_agent_table_id) as agent_icon,
(SELECT r.data->>'color' FROM table_rows r
WHERE r.id = u.managed_by_agent_row_id
AND r.table_id = u.managed_by_agent_table_id) as agent_color
FROM users u
ORDER BY u.name
`);
success(res, users || []);
} catch (err) {
apiLogger.error({ err }, 'GET /users error');
error(res, 'USERS_FETCH_FAILED', err.message, 500);
}
});
/**
* GET /api/v3/tables/:tableId
* Get table by ID
*/
router.get('/tables/:tableId', async (req, res) => {
try {
const { tableId } = req.params;
// Check access if API key has project restriction
if (req.user?.projectId) {
const access = await checkTableAccess(tableId, req.user);
if (!access.allowed) {
return forbidden(res, access.error);
}
}
const table = await dbGet(`
SELECT
id,
project_id,
name,
icon,
description,
is_system,
sync_target,
data_source_id,
source_table_name,
source_id_column,
sync_enabled,
sync_interval_minutes,
last_sync_at,
parent_table_id,
config,
created_at,
updated_at
FROM universal_tables
WHERE id = ?
`, [tableId]);
if (!table) {
return notFound(res, 'Table');
}
success(res, table);
} catch (err) {
apiLogger.error({ err }, 'GET /tables/:tableId error');
error(res, 'TABLE_FETCH_FAILED', err.message, 500);
}
});
/**
* POST /api/v3/tables/create-calendar
* Create a calendar table with weekend/holiday data
* IMPORTANT: This route must be defined BEFORE /tables/:tableId routes
*/
router.post('/tables/create-calendar', async (req, res) => {
try {
const { projectId, tableName = 'Calendar' } = req.body;
const userId = req.user?.id || req.user?.userId;
apiLogger.debug({ projectId, tableName, userId }, 'CREATE CALENDAR request received');
if (!projectId) {
return badRequest(res, 'projectId is required');
}
if (!userId) {
return unauthorized(res, 'User authentication required');
}
// Check if project exists
const project = await dbGet('SELECT id, space_id FROM projects WHERE id = ?', [projectId]);
if (!project) {
return notFound(res, 'Project');
}
apiLogger.debug({ tableName, projectId }, 'Creating calendar table');
// Import the service dynamically
const { createCalendarTable } = await import('../../../services/CalendarTableService.js');
// Create the calendar table
const tableId = await createCalendarTable(projectId, userId, tableName);
// Get the created table info
const table = await dbGet(`
SELECT t.*, p.name as project_name, p.icon as project_icon
FROM universal_tables t
LEFT JOIN projects p ON t.project_id = p.id
WHERE t.id = ?
`, [tableId]);
apiLogger.info({ tableId }, 'CREATE CALENDAR success');
success(res, {
tableId,
table: {
id: table.id,
name: table.name,
icon: table.icon,
description: table.description,
projectId: table.project_id,
projectName: table.project_name
}
});
} catch (err) {
apiLogger.error({ err }, 'POST /tables/create-calendar error');
error(res, 'CREATE_CALENDAR_FAILED', err.message, 500);
}
});
/**
* POST /api/v3/tables
* Create a new table
*/
router.post('/tables', async (req, res) => {
try {
const { name, description, icon, projectId, project_id, columns, data_source_id, external_table_name } = req.body;
// Support both projectId and project_id for compatibility
const effectiveProjectId = projectId || project_id;
// Stable module marker (ADR-164 style). Accept snake_case and the camelCase
// `tableType` the widget presets declare; persist so a module's tables are
// discoverable by type per-project (e.g. SC-SIM sim_runs) instead of by a
// hard-coded global id. Absent → null, preserving legacy behaviour.
const tableType = req.body.table_type || req.body.tableType || null;
if (!name) {
return badRequest(res, 'Table name is required');
}
if (!effectiveProjectId) {
return badRequest(res, 'Project ID is required');
}
// Create the table with optional data source connection
const result = await dbRun(`
INSERT INTO universal_tables (
project_id,
name,
description,
icon,
is_system,
table_type,
data_source_id,
source_table_name,
sync_enabled,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [
effectiveProjectId,
name,
description || null,
icon || '📊',
toBool(false),
tableType,
data_source_id || null,
external_table_name || null,
toBool(!!data_source_id)
]);
const tableId = result.lastID || result.lastInsertRowid;
if (!tableId) {
apiLogger.error({ result }, 'Failed to get table ID from insert result');
throw new Error('Failed to create table: no ID returned');
}
apiLogger.info({ tableId }, 'Table created');
// Create columns
const createdColumns = [];
// If external table, auto-import columns from data source
if (data_source_id && external_table_name) {
try {
apiLogger.debug({ external_table_name }, 'Importing columns from external table');
const dataSourceService = await import('../../../services/DataSourceService.js');
apiLogger.debug('DataSourceService loaded');
const externalColumns = await dataSourceService.default.listTableColumns(data_source_id, external_table_name);
apiLogger.debug({ count: externalColumns.length, columns: externalColumns }, 'External columns fetched');
for (let i = 0; i < externalColumns.length; i++) {
const col = externalColumns[i];
// Map external type to our internal type
let internalType = 'text';
if (col.type.includes('int') || col.type.includes('decimal') || col.type.includes('float')) {
internalType = 'number';
} else if (col.type.includes('date') || col.type.includes('time')) {
internalType = 'date';
} else if (col.type.includes('bool')) {
internalType = 'checkbox';
}
apiLogger.debug({ name: col.name, type: internalType }, 'Inserting column');
const columnResult = await dbRun(`
INSERT INTO table_columns (
table_id,
column_name,
display_name,
type,
is_required,
order_index,
is_visible,
config,
is_from_source,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [
tableId,
col.name,
col.name,
internalType,
toBool(false),
i,
toBool(true),
JSON.stringify({}),
toBool(true)
]);
apiLogger.debug({ columnId: columnResult.lastID || columnResult.lastInsertRowid }, 'Column inserted');
createdColumns.push({
id: columnResult.lastID || columnResult.lastInsertRowid,
table_id: tableId,
column_name: col.name,
display_name: col.name,
type: internalType
});
}
apiLogger.debug({ count: createdColumns.length }, "Imported columns from external table");
} catch (error) {
apiLogger.error({ err: error }, 'Failed to import external columns');
apiLogger.error({ stack: error.stack }, 'Error stack');
// Continue without columns - user can map them later
}
} else if (columns && Array.isArray(columns) && columns.length > 0) {
// Local table with provided columns
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
const columnResult = 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.name,
col.type,
toBool(col.isRequired),
col.orderIndex ?? i,
toBool(col.isVisible !== false),
JSON.stringify(col.config || {})
]);
createdColumns.push({
id: columnResult.lastID || columnResult.lastInsertRowid,
table_id: tableId,
column_name: col.name,
display_name: col.displayName || col.name,
type: col.type,
is_required: col.isRequired ? 1 : 0,
order_index: col.orderIndex ?? i,
is_visible: col.isVisible !== false ? 1 : 0,
config: col.config || {}
});
}
}
// Fetch the created table
const newTable = await dbGet(`
SELECT
id,
project_id,
name,
icon,
description,
is_system,
sync_target,
data_source_id,
created_at,
updated_at
FROM universal_tables
WHERE id = ?
`, [tableId]);
success(res, {
table: newTable,
columns: createdColumns
});
} catch (err) {
apiLogger.error({ err }, 'POST /tables error');
error(res, 'TABLE_CREATE_FAILED', err.message, 500);
}
});
export default router;