const express = require('express'); const db = require('../db/models'); const wrapAsync = require('../helpers').wrapAsync; const { validateReadOnlySql } = require('../utils/sqlValidator'); const router = express.Router(); const MAX_SQL_LENGTH = 5000; const MAX_SQL_ROWS = 1000; const SQL_TIMEOUT_MS = 5000; /** * @swagger * /api/sql: * post: * security: * - bearerAuth: [] * summary: Execute a SELECT-only SQL query * description: Executes a read-only SQL query and returns rows. * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * sql: * type: string * required: * - sql * responses: * 200: * description: Query result * 400: * description: Invalid SQL * 401: * $ref: "#/components/responses/UnauthorizedError" * 500: * description: Internal server error */ router.post( '/', wrapAsync(async (req, res) => { const { currentUser } = req; const isAdminUser = Boolean( currentUser && currentUser.app_role && (currentUser.app_role.name === 'Administrator' || currentUser.app_role.globalAccess === true), ); if (!isAdminUser) { return res .status(403) .json({ error: 'Only administrators can execute SQL queries' }); } const { sql } = req.body; const validation = validateReadOnlySql(sql, { maxLength: MAX_SQL_LENGTH }); if (!validation.valid) { return res.status(400).json({ error: validation.error }); } const normalized = validation.normalized; const wrappedSql = `SELECT * FROM (${normalized}) AS query_result LIMIT ${MAX_SQL_ROWS}`; const rows = await db.sequelize.transaction(async (transaction) => { await db.sequelize.query( `SET LOCAL statement_timeout = ${SQL_TIMEOUT_MS}`, { transaction }, ); return db.sequelize.query(wrappedSql, { transaction, type: db.Sequelize.QueryTypes.SELECT, }); }); return res.status(200).json({ rows, meta: { maxRows: MAX_SQL_ROWS, statementTimeoutMs: SQL_TIMEOUT_MS, }, }); }), ); module.exports = router;