Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
310 lines
9.1 KiB
JavaScript
310 lines
9.1 KiB
JavaScript
// API v3: Column Mapping Routes
|
|
// ADR-069: Provides mapping between table columns and standard ticket fields
|
|
// Allows CardDetailModal to display consistent labels across different table types
|
|
|
|
/**
|
|
* @swagger
|
|
* components:
|
|
* schemas:
|
|
* StandardField:
|
|
* type: object
|
|
* properties:
|
|
* key:
|
|
* type: string
|
|
* description: Standard field key (e.g., 'title', 'description')
|
|
* label:
|
|
* type: string
|
|
* description: Human-readable label
|
|
* required:
|
|
* type: boolean
|
|
* description: Whether this field is required
|
|
* ColumnMapping:
|
|
* type: object
|
|
* properties:
|
|
* tableId:
|
|
* type: integer
|
|
* tableName:
|
|
* type: string
|
|
* mappings:
|
|
* type: object
|
|
* description: Object mapping standard field keys to column names
|
|
*/
|
|
|
|
import express from 'express';
|
|
import { dbAll, dbGet, dbRun } from '../../database/connection.js';
|
|
import { apiLogger } from '../../utils/logger.js';
|
|
import { success, badRequest, notFound } from '../../utils/response.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// Standard fields that can be mapped to table columns
|
|
const STANDARD_FIELDS = [
|
|
{ key: 'title', label: 'Заголовок', required: true },
|
|
{ key: 'description', label: 'Описание', required: false },
|
|
{ key: 'priority', label: 'Приоритет', required: false },
|
|
{ key: 'status', label: 'Статус', required: false },
|
|
{ key: 'assignee', label: 'Исполнитель', required: false },
|
|
{ key: 'dueDate', label: 'Срок', required: false },
|
|
{ key: 'type', label: 'Тип', required: false },
|
|
{ key: 'createdBy', label: 'Автор', required: false },
|
|
];
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/v3/column-mapping/defaults:
|
|
* get:
|
|
* summary: Get standard fields that can be mapped
|
|
* tags: [Column Mapping]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: List of standard fields
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* standardFields:
|
|
* type: array
|
|
* items:
|
|
* $ref: '#/components/schemas/StandardField'
|
|
*/
|
|
router.get('/defaults', async (req, res) => {
|
|
try {
|
|
return success(res, { standardFields: STANDARD_FIELDS });
|
|
} catch (error) {
|
|
apiLogger.error({ err: error }, 'Failed to get column mapping defaults');
|
|
return badRequest(res, 'Failed to get defaults');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/v3/column-mapping/{tableId}:
|
|
* get:
|
|
* summary: Get column mapping for a table
|
|
* tags: [Column Mapping]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: tableId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The table ID
|
|
* responses:
|
|
* 200:
|
|
* description: Column mapping for the table
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* data:
|
|
* $ref: '#/components/schemas/ColumnMapping'
|
|
* 404:
|
|
* description: Table not found
|
|
*/
|
|
router.get('/:tableId', async (req, res) => {
|
|
try {
|
|
const { tableId } = req.params;
|
|
|
|
// Validate tableId is a number
|
|
if (isNaN(parseInt(tableId, 10))) {
|
|
return badRequest(res, 'Invalid table ID');
|
|
}
|
|
|
|
// Get table info
|
|
const table = await dbGet('SELECT id, name FROM tables WHERE id = $1', [tableId]);
|
|
if (!table) {
|
|
return notFound(res, 'Table');
|
|
}
|
|
|
|
// Get mappings from table_column_mappings table
|
|
const mappings = await dbAll(
|
|
'SELECT standard_field, column_name FROM table_column_mappings WHERE table_id = $1',
|
|
[tableId]
|
|
);
|
|
|
|
// Convert to object format
|
|
const mappingsObj = {};
|
|
for (const m of mappings) {
|
|
mappingsObj[m.standard_field] = m.column_name;
|
|
}
|
|
|
|
apiLogger.debug({ tableId, mappingsCount: mappings.length }, 'Retrieved column mapping');
|
|
|
|
return success(res, {
|
|
tableId: parseInt(tableId, 10),
|
|
tableName: table.name,
|
|
mappings: mappingsObj
|
|
});
|
|
} catch (error) {
|
|
apiLogger.error({ err: error, tableId: req.params.tableId }, 'Failed to get column mapping');
|
|
return badRequest(res, 'Failed to get column mapping');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/v3/column-mapping/{tableId}:
|
|
* post:
|
|
* summary: Save column mapping for a table
|
|
* tags: [Column Mapping]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: tableId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The table ID
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - mappings
|
|
* properties:
|
|
* mappings:
|
|
* type: object
|
|
* description: Object mapping standard field keys to column names
|
|
* responses:
|
|
* 200:
|
|
* description: Column mapping saved
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* data:
|
|
* type: object
|
|
* properties:
|
|
* tableId:
|
|
* type: integer
|
|
* mappings:
|
|
* type: object
|
|
* 400:
|
|
* description: Invalid request
|
|
* 404:
|
|
* description: Table not found
|
|
*/
|
|
router.post('/:tableId', async (req, res) => {
|
|
try {
|
|
const { tableId } = req.params;
|
|
const { mappings } = req.body;
|
|
|
|
// Validate tableId is a number
|
|
if (isNaN(parseInt(tableId, 10))) {
|
|
return badRequest(res, 'Invalid table ID');
|
|
}
|
|
|
|
// Validate mappings object
|
|
if (!mappings || typeof mappings !== 'object' || Array.isArray(mappings)) {
|
|
return badRequest(res, 'Invalid mappings object');
|
|
}
|
|
|
|
// Check table exists
|
|
const table = await dbGet('SELECT id FROM tables WHERE id = $1', [tableId]);
|
|
if (!table) {
|
|
return notFound(res, 'Table');
|
|
}
|
|
|
|
// Validate standard field keys
|
|
const validKeys = STANDARD_FIELDS.map(f => f.key);
|
|
for (const key of Object.keys(mappings)) {
|
|
if (!validKeys.includes(key)) {
|
|
return badRequest(res, `Invalid standard field: ${key}`);
|
|
}
|
|
}
|
|
|
|
// Delete existing mappings
|
|
await dbRun('DELETE FROM table_column_mappings WHERE table_id = $1', [tableId]);
|
|
|
|
// Insert new mappings
|
|
const savedMappings = {};
|
|
for (const [standardField, columnName] of Object.entries(mappings)) {
|
|
if (columnName && typeof columnName === 'string' && columnName.trim()) {
|
|
await dbRun(
|
|
'INSERT INTO table_column_mappings (table_id, standard_field, column_name) VALUES ($1, $2, $3)',
|
|
[tableId, standardField, columnName.trim()]
|
|
);
|
|
savedMappings[standardField] = columnName.trim();
|
|
}
|
|
}
|
|
|
|
apiLogger.info({ tableId, mappings: savedMappings }, 'Column mapping saved');
|
|
|
|
return success(res, {
|
|
tableId: parseInt(tableId, 10),
|
|
mappings: savedMappings
|
|
});
|
|
} catch (error) {
|
|
apiLogger.error({ err: error, tableId: req.params.tableId }, 'Failed to save column mapping');
|
|
return badRequest(res, 'Failed to save column mapping');
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /api/v3/column-mapping/{tableId}:
|
|
* delete:
|
|
* summary: Delete all column mappings for a table
|
|
* tags: [Column Mapping]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: tableId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: The table ID
|
|
* responses:
|
|
* 200:
|
|
* description: Mappings deleted
|
|
* 404:
|
|
* description: Table not found
|
|
*/
|
|
router.delete('/:tableId', async (req, res) => {
|
|
try {
|
|
const { tableId } = req.params;
|
|
|
|
// Validate tableId is a number
|
|
if (isNaN(parseInt(tableId, 10))) {
|
|
return badRequest(res, 'Invalid table ID');
|
|
}
|
|
|
|
// Check table exists
|
|
const table = await dbGet('SELECT id FROM tables WHERE id = $1', [tableId]);
|
|
if (!table) {
|
|
return notFound(res, 'Table');
|
|
}
|
|
|
|
// Delete all mappings for this table
|
|
await dbRun('DELETE FROM table_column_mappings WHERE table_id = $1', [tableId]);
|
|
|
|
apiLogger.info({ tableId }, 'Column mapping deleted');
|
|
|
|
return success(res, { tableId: parseInt(tableId, 10), deleted: true });
|
|
} catch (error) {
|
|
apiLogger.error({ err: error, tableId: req.params.tableId }, 'Failed to delete column mapping');
|
|
return badRequest(res, 'Failed to delete column mapping');
|
|
}
|
|
});
|
|
|
|
export default router;
|