godcrm/backend/routes/v3/columns/columnOpsController.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

181 lines
6.2 KiB
JavaScript

// columns/columnOpsController.js — Reorder columns, convert-to-iso
import express from 'express';
import { dbAll, dbGet, dbRun } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, notFound, badRequest, error } from '../../../utils/response.js';
import { verifyTableAccess } from './helpers.js';
const router = express.Router();
// ============================================================
// POST /api/v3/tables/:tableId/columns/reorder
// Reorder columns
// ============================================================
router.post('/tables/:tableId/columns/reorder', verifyTableAccess, async (req, res) => {
try {
const { tableId } = req.params;
const { order } = req.body;
if (!order || !Array.isArray(order)) {
return badRequest(res, 'order must be an array of column IDs');
}
for (let i = 0; i < order.length; i++) {
await dbRun(
'UPDATE table_columns SET order_index = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND table_id = ?',
[i, order[i], tableId]
);
}
apiLogger.info({ tableId, order }, 'Columns reordered');
return success(res, { message: 'Columns reordered successfully' });
} catch (err) {
apiLogger.error({ err }, 'POST /tables/:tableId/columns/reorder error');
return error(res, 'COLUMN_REORDER_FAILED', 'Failed to reorder columns', 500, err.message);
}
});
// ============================================================
// POST /api/v3/tables/:tableId/columns/:columnId/convert-to-iso
// Batch-convert EU/US date values to ISO format
// ============================================================
router.post('/tables/:tableId/columns/:columnId/convert-to-iso', verifyTableAccess, async (req, res) => {
try {
const { tableId, columnId } = req.params;
const column = await dbGet(
'SELECT id, column_name AS name, type AS column_type, config FROM table_columns WHERE id = ? AND table_id = ?',
[columnId, tableId]
);
if (!column) {
return notFound(res, 'Column');
}
const config = column.config ? JSON.parse(column.config) : {};
const mode = config?.date?.mode || (column.column_type === 'datetime' ? 'datetime' : 'date');
const storageFormat = config?.date?.storageFormat || config?.date?.dateFormat || 'iso';
if (storageFormat === 'iso') {
return badRequest(res, 'Column is already in ISO format');
}
if (storageFormat !== 'eu' && storageFormat !== 'us') {
return badRequest(res, 'Convert to ISO only supports EU and US formats');
}
if (mode !== 'date' && mode !== 'datetime') {
return badRequest(res, 'Convert to ISO only applies to date and datetime modes');
}
const rows = await dbAll(
'SELECT id, data FROM table_rows WHERE table_id = ?',
[tableId]
);
let converted = 0;
let failed = 0;
let skipped = 0;
const columnName = column.name;
for (const row of rows) {
const rowData = row.data ? JSON.parse(row.data) : {};
const val = rowData[columnName];
if (val === null || val === undefined || val === '') {
skipped++;
continue;
}
const str = String(val);
let isoValue = null;
try {
if (storageFormat === 'eu') {
if (mode === 'date') {
// DD.MM.YYYY -> YYYY-MM-DD
const match = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
if (match) {
const day = match[1].padStart(2, '0');
const month = match[2].padStart(2, '0');
isoValue = `${match[3]}-${month}-${day}`;
}
} else {
// DD.MM.YYYY HH:mm[:ss] -> YYYY-MM-DDTHH:mm:ssZ
const match = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})\s(\d{2}):(\d{2})(?::(\d{2}))?/);
if (match) {
const day = match[1].padStart(2, '0');
const month = match[2].padStart(2, '0');
const sec = match[6] || '00';
isoValue = `${match[3]}-${month}-${day}T${match[4]}:${match[5]}:${sec}Z`;
}
}
} else if (storageFormat === 'us') {
if (mode === 'date') {
// MM/DD/YYYY -> YYYY-MM-DD
const match = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (match) {
const month = match[1].padStart(2, '0');
const day = match[2].padStart(2, '0');
isoValue = `${match[3]}-${month}-${day}`;
}
} else {
// MM/DD/YYYY HH:mm[:ss] -> YYYY-MM-DDTHH:mm:ssZ
const match = str.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s(\d{2}):(\d{2})(?::(\d{2}))?/);
if (match) {
const month = match[1].padStart(2, '0');
const day = match[2].padStart(2, '0');
const sec = match[6] || '00';
isoValue = `${match[3]}-${month}-${day}T${match[4]}:${match[5]}:${sec}Z`;
}
}
}
// Check if value is already ISO -- skip
if (!isoValue && /^\d{4}-\d{2}-\d{2}/.test(str)) {
skipped++;
continue;
}
if (isoValue) {
rowData[columnName] = isoValue;
await dbRun(
'UPDATE table_rows SET data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[JSON.stringify(rowData), row.id]
);
converted++;
} else {
failed++;
}
} catch (e) {
failed++;
}
}
// Update column config: storageFormat -> 'iso'
const newConfig = {
...config,
date: {
...config.date,
storageFormat: 'iso',
},
};
if (newConfig.date?.dateFormat) {
delete newConfig.date.dateFormat;
}
await dbRun(
'UPDATE table_columns SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[JSON.stringify(newConfig), columnId]
);
apiLogger.info({ columnId, tableId, converted, failed, skipped }, 'Column values converted to ISO');
return success(res, { converted, failed, skipped });
} catch (err) {
apiLogger.error({ err }, 'POST /tables/:tableId/columns/:columnId/convert-to-iso error');
return error(res, 'CONVERT_TO_ISO_FAILED', 'Failed to convert values to ISO', 500, err.message);
}
});
export default router;