Initial setup: snakkimo API server with PostgreSQL connection
Node.js/Express API server that connects to PostgreSQL via environment variables. Includes health check, table listing, row queries, and raw SQL endpoint. Designed for deployment in Coolify alongside the snakkimo PostgreSQL container. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
67
src/routes/index.js
Normal file
67
src/routes/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const router = require('express').Router();
|
||||
const { query } = require('../db');
|
||||
|
||||
// List all tables in the database
|
||||
router.get('/tables', async (req, res, next) => {
|
||||
try {
|
||||
const result = await query(
|
||||
`SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name`
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Get rows from any table (with optional limit)
|
||||
router.get('/tables/:table', async (req, res, next) => {
|
||||
try {
|
||||
const { table } = req.params;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 100, 1000);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
// Validate table name to prevent SQL injection
|
||||
const tableCheck = await query(
|
||||
`SELECT table_name FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = $1`,
|
||||
[table]
|
||||
);
|
||||
if (tableCheck.rows.length === 0) {
|
||||
return res.status(404).json({ error: `Table "${table}" not found` });
|
||||
}
|
||||
|
||||
const result = await query(
|
||||
`SELECT * FROM "${table}" LIMIT $1 OFFSET $2`,
|
||||
[limit, offset]
|
||||
);
|
||||
res.json({ table, count: result.rows.length, rows: result.rows });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Execute raw SQL (POST, restricted — use carefully)
|
||||
router.post('/query', async (req, res, next) => {
|
||||
try {
|
||||
const { sql, params } = req.body;
|
||||
if (!sql) return res.status(400).json({ error: 'Missing "sql" field' });
|
||||
|
||||
// Block destructive statements without explicit confirmation header
|
||||
const dangerous = /^\s*(drop|truncate|delete\s+from\s+\w+\s*;)/i;
|
||||
if (dangerous.test(sql) && req.headers['x-confirm-destructive'] !== 'yes') {
|
||||
return res.status(400).json({
|
||||
error: 'Destructive statement detected. Set header X-Confirm-Destructive: yes to proceed.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await query(sql, params || []);
|
||||
res.json({ rowCount: result.rowCount, rows: result.rows });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user