2026-04-05 18:46:16 +04:00

88 lines
2.3 KiB
JavaScript

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;