Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
/**
|
|
* Table single-row get controller
|
|
* Handles: GET /tables/:tableId/rows/base/:baseId,
|
|
* GET /tables/:tableId/rows/:rowId
|
|
*/
|
|
import express from 'express';
|
|
import { dbAll, dbGet, safeJsonParse } from '../../../database/connection.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, notFound, forbidden, error } from '../../../utils/response.js';
|
|
import { checkTableAccess } from './helpers.js';
|
|
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /api/v3/tables/:tableId/rows/base/:baseId
|
|
* Get a single row by base_id
|
|
* IMPORTANT: Must be defined BEFORE /:rowId route to avoid Express param conflict
|
|
*/
|
|
router.get('/tables/:tableId/rows/base/:baseId', async (req, res) => {
|
|
try {
|
|
const { tableId, baseId } = req.params;
|
|
|
|
if (req.user?.projectId) {
|
|
const access = await checkTableAccess(tableId, req.user);
|
|
if (!access.allowed) {
|
|
return forbidden(res, access.error);
|
|
}
|
|
}
|
|
|
|
apiLogger.debug({ tableId, baseId }, 'GET single row by base_id request');
|
|
|
|
const table = await dbGet(`
|
|
SELECT data_source_id, source_table_name, source_id_column, is_system, sync_target, project_id
|
|
FROM universal_tables
|
|
WHERE id = ?
|
|
`, [tableId]);
|
|
|
|
// System table
|
|
if (table && table.is_system && table.sync_target) {
|
|
const { getSystemTableData } = await import('../../../services/SystemTableService.js');
|
|
const systemRows = await getSystemTableData(table.sync_target, table.project_id);
|
|
const row = systemRows.find(r => String(r.base_id || r.id) === String(baseId));
|
|
if (!row) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
return success(res, { row });
|
|
}
|
|
|
|
// External data source
|
|
if (table && table.data_source_id && table.source_table_name) {
|
|
const DataSourceService = (await import('../../../services/DataSourceService.js')).default;
|
|
const dataSourceService = new DataSourceService();
|
|
const dataSource = await dataSourceService.get(table.data_source_id);
|
|
|
|
if (dataSource.type === 'internal') {
|
|
const idColumn = table.source_id_column || 'id';
|
|
const sourceRow = await dbGet(
|
|
`SELECT * FROM "${table.source_table_name}" WHERE "${idColumn}" = ?`,
|
|
[baseId]
|
|
);
|
|
|
|
if (!sourceRow) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
const columns = await dbAll('SELECT * FROM table_columns WHERE table_id = ?', [tableId]);
|
|
const data = {};
|
|
columns.forEach(col => {
|
|
if (sourceRow[col.column_name] !== undefined) {
|
|
data[col.id] = sourceRow[col.column_name];
|
|
}
|
|
});
|
|
|
|
return success(res, {
|
|
row: {
|
|
id: sourceRow[idColumn] || baseId,
|
|
table_id: Number(tableId),
|
|
base_id: baseId,
|
|
data,
|
|
created_at: sourceRow.created_at || new Date().toISOString(),
|
|
updated_at: sourceRow.updated_at || new Date().toISOString()
|
|
}
|
|
});
|
|
}
|
|
|
|
if (dataSource.type === 'local_mysql') {
|
|
const mysql = await import('mysql2/promise');
|
|
const connection = await mysql.createConnection({
|
|
host: dataSource.db_host,
|
|
port: dataSource.db_port,
|
|
database: dataSource.db_name,
|
|
user: dataSource.db_username,
|
|
password: ''
|
|
});
|
|
|
|
try {
|
|
const idColumn = table.source_id_column || 'id';
|
|
const [rows] = await connection.execute(
|
|
`SELECT * FROM \`${table.source_table_name}\` WHERE \`${idColumn}\` = ? LIMIT 1`,
|
|
[baseId]
|
|
);
|
|
|
|
if (!rows.length) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
return success(res, {
|
|
row: {
|
|
id: baseId,
|
|
table_id: Number(tableId),
|
|
data: rows[0],
|
|
created_at: rows[0].created_at || new Date().toISOString(),
|
|
updated_at: rows[0].updated_at || new Date().toISOString()
|
|
}
|
|
});
|
|
} finally {
|
|
await connection.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default: internal table_rows
|
|
const row = await dbGet('SELECT * FROM table_rows WHERE base_id = ? AND table_id = ?', [baseId, tableId]);
|
|
|
|
if (!row) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
let parsedData = safeJsonParse(row.data, {});
|
|
if (typeof parsedData === 'object' && !parsedData.id) {
|
|
parsedData.id = row.id;
|
|
}
|
|
|
|
const columns = await dbAll(
|
|
'SELECT id, column_name, display_name, type, config FROM table_columns WHERE table_id = ? ORDER BY order_index',
|
|
[tableId]
|
|
);
|
|
|
|
success(res, {
|
|
row: {
|
|
id: row.id,
|
|
base_id: row.base_id,
|
|
table_id: row.table_id,
|
|
data: parsedData,
|
|
created_by: row.created_by,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at
|
|
},
|
|
columns: columns.map(c => ({
|
|
id: c.id,
|
|
column_name: c.column_name,
|
|
display_name: c.display_name,
|
|
type: c.type,
|
|
config: safeJsonParse(c.config, {})
|
|
}))
|
|
});
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'GET /tables/:tableId/rows/base/:baseId error');
|
|
error(res, 'ROW_FETCH_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/v3/tables/:tableId/rows/:rowId
|
|
* Get a single row by ID (numeric id, falls back to base_id)
|
|
*/
|
|
router.get('/tables/:tableId/rows/:rowId', async (req, res) => {
|
|
try {
|
|
const { tableId, rowId } = req.params;
|
|
|
|
if (req.user?.projectId) {
|
|
const access = await checkTableAccess(tableId, req.user);
|
|
if (!access.allowed) {
|
|
return forbidden(res, access.error);
|
|
}
|
|
}
|
|
|
|
apiLogger.debug({ tableId, rowId }, 'GET single row request');
|
|
|
|
const table = await dbGet(`
|
|
SELECT data_source_id, source_table_name, source_id_column, is_system, sync_target, project_id
|
|
FROM universal_tables
|
|
WHERE id = ?
|
|
`, [tableId]);
|
|
|
|
// System table
|
|
if (table && table.is_system && table.sync_target) {
|
|
const { getSystemTableData } = await import('../../../services/SystemTableService.js');
|
|
const systemRows = await getSystemTableData(table.sync_target, table.project_id);
|
|
const row = systemRows.find(r => String(r.id) === String(rowId));
|
|
if (!row) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
return success(res, { row });
|
|
}
|
|
|
|
// External data source (internal type)
|
|
if (table && table.data_source_id && table.source_table_name) {
|
|
const DataSourceService = (await import('../../../services/DataSourceService.js')).default;
|
|
const dataSourceService = new DataSourceService();
|
|
const dataSource = await dataSourceService.get(table.data_source_id);
|
|
|
|
if (dataSource.type === 'internal') {
|
|
let realRowId = rowId;
|
|
if (rowId.startsWith('int_')) {
|
|
const parts = rowId.split('_');
|
|
realRowId = parts[parts.length - 1];
|
|
} else if (rowId.startsWith('user-')) {
|
|
realRowId = rowId.replace('user-', '');
|
|
}
|
|
|
|
const idColumn = table.source_id_column || 'id';
|
|
const sourceRow = await dbGet(
|
|
`SELECT * FROM "${table.source_table_name}" WHERE "${idColumn}" = ?`,
|
|
[realRowId]
|
|
);
|
|
|
|
if (!sourceRow) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
const columns = await dbAll('SELECT * FROM table_columns WHERE table_id = ?', [tableId]);
|
|
const data = {};
|
|
columns.forEach(col => {
|
|
if (sourceRow[col.column_name] !== undefined) {
|
|
data[col.id] = sourceRow[col.column_name];
|
|
}
|
|
});
|
|
|
|
return success(res, {
|
|
row: {
|
|
id: rowId,
|
|
table_id: Number(tableId),
|
|
base_id: sourceRow[idColumn] || rowId,
|
|
data,
|
|
created_at: sourceRow.created_at || new Date().toISOString(),
|
|
updated_at: sourceRow.updated_at || new Date().toISOString()
|
|
},
|
|
columns: columns.map(c => ({
|
|
id: c.id,
|
|
column_name: c.column_name,
|
|
display_name: c.display_name,
|
|
type: c.type,
|
|
config: safeJsonParse(c.config, {})
|
|
}))
|
|
});
|
|
}
|
|
|
|
if (dataSource.type === 'local_mysql') {
|
|
const mysql = await import('mysql2/promise');
|
|
const connection = await mysql.createConnection({
|
|
host: dataSource.db_host,
|
|
port: dataSource.db_port,
|
|
database: dataSource.db_name,
|
|
user: dataSource.db_username,
|
|
password: ''
|
|
});
|
|
|
|
try {
|
|
const idColumn = table.source_id_column || 'id';
|
|
const [rows] = await connection.execute(
|
|
`SELECT * FROM \`${table.source_table_name}\` WHERE \`${idColumn}\` = ? LIMIT 1`,
|
|
[rowId]
|
|
);
|
|
|
|
if (!rows.length) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
const columns = await dbAll('SELECT * FROM table_columns WHERE table_id = ? ORDER BY order_index', [tableId]);
|
|
|
|
return success(res, {
|
|
row: {
|
|
id: rowId,
|
|
table_id: Number(tableId),
|
|
data: rows[0],
|
|
created_at: rows[0].created_at || new Date().toISOString(),
|
|
updated_at: rows[0].updated_at || new Date().toISOString()
|
|
},
|
|
columns: columns.map(c => ({
|
|
id: c.id,
|
|
column_name: c.column_name,
|
|
display_name: c.display_name,
|
|
type: c.type,
|
|
config: safeJsonParse(c.config, {})
|
|
}))
|
|
});
|
|
} finally {
|
|
await connection.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default: internal table_rows
|
|
let row = await dbGet('SELECT * FROM table_rows WHERE id = ? AND table_id = ?', [rowId, tableId]);
|
|
|
|
if (!row) {
|
|
row = await dbGet('SELECT * FROM table_rows WHERE base_id = ? AND table_id = ?', [rowId, tableId]);
|
|
}
|
|
|
|
if (!row) {
|
|
return notFound(res, 'Row');
|
|
}
|
|
|
|
let parsedData = safeJsonParse(row.data, {});
|
|
if (typeof parsedData === 'object' && !parsedData.id) {
|
|
parsedData.id = row.id;
|
|
}
|
|
|
|
const columns = await dbAll(
|
|
'SELECT id, column_name, display_name, type, config FROM table_columns WHERE table_id = ? ORDER BY order_index',
|
|
[tableId]
|
|
);
|
|
|
|
success(res, {
|
|
row: {
|
|
id: row.id,
|
|
base_id: row.base_id,
|
|
table_id: row.table_id,
|
|
data: parsedData,
|
|
created_by: row.created_by,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at
|
|
},
|
|
columns: columns.map(c => ({
|
|
id: c.id,
|
|
column_name: c.column_name,
|
|
display_name: c.display_name,
|
|
type: c.type,
|
|
config: safeJsonParse(c.config, {})
|
|
}))
|
|
});
|
|
} catch (err) {
|
|
apiLogger.error({ err }, 'GET /tables/:tableId/rows/:rowId error');
|
|
error(res, 'ROW_FETCH_FAILED', err.message, 500);
|
|
}
|
|
});
|
|
|
|
export default router;
|