Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
205 lines
6.7 KiB
JavaScript
205 lines
6.7 KiB
JavaScript
/**
|
|
* Table column operations controller
|
|
* Handles: GET /tables/:tableId/columns
|
|
*/
|
|
import express from 'express';
|
|
import { dbAll, dbGet } from '../../../database/connection.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, error } from '../../../utils/response.js';
|
|
import { mapPostgresTypeToInternal, filterColumnsByRole, resolveUserRoleForTable } from './helpers.js';
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/v3/tables/:tableId/columns
|
|
* Get columns for a table
|
|
* Supports external data sources - reads schema from MySQL if connected
|
|
* mode=raw returns original source column names without transformation
|
|
* Filters columns based on user role and column access_control config
|
|
*/
|
|
router.get('/tables/:tableId/columns', async (req, res) => {
|
|
try {
|
|
const { tableId } = req.params;
|
|
const { mode } = req.query;
|
|
const rawMode = mode === 'raw';
|
|
const userId = req.user?.id;
|
|
|
|
// Check if table has external data source and get project_id
|
|
const table = await dbGet(`
|
|
SELECT data_source_id, source_table_name, project_id
|
|
FROM universal_tables
|
|
WHERE id = ?
|
|
`, [tableId]);
|
|
|
|
// Get user's role and granular access data for column filtering
|
|
const { userRole, userAccessData } = await resolveUserRoleForTable(userId, table);
|
|
apiLogger.debug({ userId, tableId, userRole, hasAccessData: !!userAccessData }, "User role for table");
|
|
|
|
// If table is connected to external data source, use columns from table_columns
|
|
if (table && table.data_source_id && table.source_table_name) {
|
|
|
|
// RAW mode for internal data source - get columns directly from database schema
|
|
if (rawMode) {
|
|
const DataSourceService = (await import('../../../services/DataSourceService.js')).default;
|
|
const dataSourceService = new DataSourceService();
|
|
const dataSource = await dataSourceService.get(table.data_source_id);
|
|
|
|
if (dataSource && dataSource.type === 'internal') {
|
|
const schemaColumns = await dbAll(`
|
|
SELECT column_name, data_type, is_nullable, column_default, ordinal_position
|
|
FROM information_schema.columns
|
|
WHERE table_name = $1
|
|
ORDER BY ordinal_position
|
|
`, [table.source_table_name]);
|
|
|
|
// Get primary key info
|
|
const pkColumns = await dbAll(`
|
|
SELECT a.attname
|
|
FROM pg_index i
|
|
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
WHERE i.indrelid = $1::regclass AND i.indisprimary
|
|
`, [table.source_table_name]).catch(() => []);
|
|
const pkSet = new Set(pkColumns.map(c => c.attname));
|
|
|
|
const columns = schemaColumns.map((col, index) => ({
|
|
id: col.column_name,
|
|
table_id: tableId,
|
|
name: col.column_name,
|
|
display_name: col.column_name,
|
|
column_type: mapPostgresTypeToInternal(col.data_type),
|
|
config: null,
|
|
is_required: col.is_nullable === 'NO' ? 1 : 0,
|
|
is_readonly: 0,
|
|
order_index: index,
|
|
is_visible: 1,
|
|
width: null,
|
|
mapping: null,
|
|
default_value: col.column_default,
|
|
formula: null,
|
|
is_from_source: 1,
|
|
is_primary_key: pkSet.has(col.column_name) ? 1 : 0,
|
|
is_locked: 0,
|
|
created_at: null,
|
|
updated_at: null
|
|
}));
|
|
|
|
apiLogger.debug({ count: columns.length }, "Internal table columns from schema");
|
|
|
|
return success(res, columns);
|
|
}
|
|
}
|
|
|
|
// For external tables, just return columns from table_columns
|
|
// These were created during table creation and have proper IDs
|
|
const columns = await dbAll(`
|
|
SELECT
|
|
id,
|
|
table_id,
|
|
column_name as name,
|
|
display_name,
|
|
type as column_type,
|
|
config,
|
|
is_required,
|
|
is_readonly,
|
|
order_index,
|
|
is_visible,
|
|
width,
|
|
mapping,
|
|
default_value,
|
|
formula,
|
|
is_from_source,
|
|
is_primary_key,
|
|
is_locked,
|
|
created_at,
|
|
updated_at
|
|
FROM table_columns
|
|
WHERE table_id = ?
|
|
ORDER BY order_index ASC
|
|
`, [tableId]);
|
|
|
|
// Parse config and mapping JSON
|
|
columns.forEach(col => {
|
|
if (typeof col.config === 'string') {
|
|
try {
|
|
col.config = JSON.parse(col.config);
|
|
} catch (e) {
|
|
col.config = {};
|
|
}
|
|
}
|
|
if (typeof col.mapping === 'string' && col.mapping) {
|
|
try {
|
|
col.mapping = JSON.parse(col.mapping);
|
|
} catch (e) {
|
|
col.mapping = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Apply role-based and granular column filtering
|
|
const filteredColumns = filterColumnsByRole(columns, userRole, userAccessData);
|
|
|
|
apiLogger.debug({ total: columns.length, filtered: filteredColumns.length, userRole }, "External table columns filtered");
|
|
apiLogger.debug({ columns: filteredColumns.slice(0, 3).map(c => ({ name: c.name, display_name: c.display_name })) }, 'First 3 columns with displayName');
|
|
|
|
return success(res, filteredColumns, { userRole: userRole || 'owner' });
|
|
}
|
|
|
|
// Fallback to local table_columns
|
|
const columns = await dbAll(`
|
|
SELECT
|
|
id,
|
|
table_id,
|
|
column_name as name,
|
|
display_name,
|
|
type as column_type,
|
|
config,
|
|
is_required,
|
|
is_readonly,
|
|
order_index,
|
|
is_visible,
|
|
width,
|
|
mapping,
|
|
default_value,
|
|
formula,
|
|
is_from_source,
|
|
is_primary_key,
|
|
is_locked,
|
|
created_at,
|
|
updated_at
|
|
FROM table_columns
|
|
WHERE table_id = ?
|
|
ORDER BY order_index ASC
|
|
`, [tableId]);
|
|
|
|
// Parse config and mapping JSON for local tables too
|
|
columns.forEach(col => {
|
|
if (typeof col.config === 'string') {
|
|
try {
|
|
col.config = JSON.parse(col.config);
|
|
} catch (e) {
|
|
col.config = {};
|
|
}
|
|
}
|
|
if (typeof col.mapping === 'string' && col.mapping) {
|
|
try {
|
|
col.mapping = JSON.parse(col.mapping);
|
|
} catch (e) {
|
|
col.mapping = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Apply role-based and granular column filtering
|
|
const filteredColumns = filterColumnsByRole(columns, userRole, userAccessData);
|
|
|
|
// Debug log column types
|
|
apiLogger.debug({ tableId, total: columns.length, filtered: filteredColumns.length, role: userRole }, 'GET columns summary');
|
|
|
|
success(res, filteredColumns, { userRole: userRole || 'owner' });
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'GET /tables/:tableId/columns error');
|
|
error(res, 'COLUMNS_FETCH_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
export default router;
|