-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathquery.js
More file actions
71 lines (63 loc) · 2.42 KB
/
query.js
File metadata and controls
71 lines (63 loc) · 2.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* ActualQL Query endpoint.
*
* Allows executing read-only ActualQL queries against Actual Budget data.
* Queries are validated and restricted for security:
* - Only whitelisted tables are allowed
* - Filter depth and complexity are limited
* - Results are truncated to prevent resource exhaustion
* - All queries are logged for audit purposes
*
* @see https://actualbudget.org/docs/api/actual-ql/ for ActualQL documentation
*/
import express from 'express';
import { authenticateJWT } from '../auth/jwt.js';
import { runActualQuery } from '../services/actualApi.js';
import { asyncHandler } from '../middleware/asyncHandler.js';
import { validateBody } from '../middleware/validation-schemas.js';
import { QuerySchema } from '../middleware/validation-schemas.js';
import { queryLimiter } from '../middleware/rateLimiters.js';
import { secureQueryMiddleware, limitQueryResults } from '../middleware/querySecurity.js';
import { sendSuccess } from '../middleware/responseHelpers.js';
import { queryBodyParser } from '../middleware/bodyParser.js';
import logger from '../logging/logger.js';
const router = express.Router();
router.use(authenticateJWT);
router.use(queryBodyParser); // Smaller limit for queries
router.post(
'/',
queryLimiter,
validateBody(QuerySchema),
secureQueryMiddleware,
asyncHandler(async (req, res) => {
const { query } = req.validatedBody;
try {
// Execute query through Actual API
const result = await runActualQuery(query);
// Limit results to prevent resource exhaustion
const limitedResult = limitQueryResults(result);
sendSuccess(res, {
result: limitedResult,
truncated: Array.isArray(result) && result.length > limitedResult.length,
});
} catch (error) {
// Sanitize query object for logging (don't log full filter data which may contain sensitive info)
const sanitizedQuery = {
table: query.table,
hasFilter: !!query.filter,
hasSelect: !!query.select,
selectType: Array.isArray(query.select) ? 'array' : typeof query.select,
selectCount: Array.isArray(query.select) ? query.select.length : null,
options: query.options,
};
logger.error('ActualQL query execution failed', {
requestId: req.id,
userId: req.user?.user_id,
query: sanitizedQuery,
error: error.message,
});
throw error;
}
})
);
export default router;