perf(db): reuse prepared statements on hot paths
- Prepare the user_accounts role lookup once in the auth middleware; it runs on nearly every authenticated request - Prepare session store get/set/destroy/purge statements once in the constructor instead of per request - Prepare the per-check SELECT once per PDF job instead of once per check
This commit is contained in:
@@ -7,16 +7,20 @@ const { Store } = require('express-session');
|
|||||||
class SessionStore extends Store {
|
class SessionStore extends Store {
|
||||||
constructor(db) {
|
constructor(db) {
|
||||||
super();
|
super();
|
||||||
this.db = db;
|
// Prepared once — get/set run on every request
|
||||||
|
this.getStmt = db.prepare('SELECT sess, expired FROM sessions WHERE sid = ?');
|
||||||
|
this.setStmt = db.prepare('INSERT OR REPLACE INTO sessions (sid, sess, expired) VALUES (?, ?, ?)');
|
||||||
|
this.delStmt = db.prepare('DELETE FROM sessions WHERE sid = ?');
|
||||||
|
this.purgeStmt = db.prepare('DELETE FROM sessions WHERE expired < ?');
|
||||||
// Purge expired sessions every 10 minutes
|
// Purge expired sessions every 10 minutes
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
try { db.prepare('DELETE FROM sessions WHERE expired < ?').run(Date.now()); } catch (_) {}
|
try { this.purgeStmt.run(Date.now()); } catch (_) {}
|
||||||
}, 10 * 60 * 1000).unref();
|
}, 10 * 60 * 1000).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
get(sid, cb) {
|
get(sid, cb) {
|
||||||
try {
|
try {
|
||||||
const row = this.db.prepare('SELECT sess, expired FROM sessions WHERE sid = ?').get(sid);
|
const row = this.getStmt.get(sid);
|
||||||
if (!row) return cb(null, null);
|
if (!row) return cb(null, null);
|
||||||
if (Date.now() > row.expired) {
|
if (Date.now() > row.expired) {
|
||||||
this.destroy(sid, () => {});
|
this.destroy(sid, () => {});
|
||||||
@@ -33,16 +37,14 @@ class SessionStore extends Store {
|
|||||||
? sess.cookie.maxAge
|
? sess.cookie.maxAge
|
||||||
: 7 * 24 * 60 * 60 * 1000;
|
: 7 * 24 * 60 * 60 * 1000;
|
||||||
const expired = Date.now() + maxAge;
|
const expired = Date.now() + maxAge;
|
||||||
this.db.prepare(
|
this.setStmt.run(sid, JSON.stringify(sess), expired);
|
||||||
'INSERT OR REPLACE INTO sessions (sid, sess, expired) VALUES (?, ?, ?)'
|
|
||||||
).run(sid, JSON.stringify(sess), expired);
|
|
||||||
cb(null);
|
cb(null);
|
||||||
} catch (e) { cb(e); }
|
} catch (e) { cb(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(sid, cb) {
|
destroy(sid, cb) {
|
||||||
try {
|
try {
|
||||||
this.db.prepare('DELETE FROM sessions WHERE sid = ?').run(sid);
|
this.delStmt.run(sid);
|
||||||
cb(null);
|
cb(null);
|
||||||
} catch (e) { cb(e); }
|
} catch (e) { cb(e); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
const db = require('../db/database');
|
const db = require('../db/database');
|
||||||
|
|
||||||
|
// Prepared once — this lookup runs on nearly every authenticated request
|
||||||
|
const accountRoleStmt = db.prepare(
|
||||||
|
'SELECT role FROM user_accounts WHERE user_id = ? AND account_id = ?'
|
||||||
|
);
|
||||||
|
|
||||||
function requireAuth(req, res, next) {
|
function requireAuth(req, res, next) {
|
||||||
if (!req.session || !req.session.userId) {
|
if (!req.session || !req.session.userId) {
|
||||||
return res.status(401).json({ error: 'Not authenticated.' });
|
return res.status(401).json({ error: 'Not authenticated.' });
|
||||||
@@ -28,10 +33,7 @@ function requireEditor(req, res, next) {
|
|||||||
function canAccessAccount(session, accountId) {
|
function canAccessAccount(session, accountId) {
|
||||||
if (!session || !session.userId) return false;
|
if (!session || !session.userId) return false;
|
||||||
if (session.role === 'admin') return true;
|
if (session.role === 'admin') return true;
|
||||||
const row = db.prepare(
|
return !!accountRoleStmt.get(session.userId, accountId);
|
||||||
'SELECT 1 FROM user_accounts WHERE user_id = ? AND account_id = ?'
|
|
||||||
).get(session.userId, accountId);
|
|
||||||
return !!row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the user has editor (write) access to the given account.
|
// Returns true if the user has editor (write) access to the given account.
|
||||||
@@ -39,9 +41,7 @@ function canAccessAccount(session, accountId) {
|
|||||||
function isEditorForAccount(session, accountId) {
|
function isEditorForAccount(session, accountId) {
|
||||||
if (!session || !session.userId) return false;
|
if (!session || !session.userId) return false;
|
||||||
if (session.role === 'admin') return true;
|
if (session.role === 'admin') return true;
|
||||||
const row = db.prepare(
|
const row = accountRoleStmt.get(session.userId, accountId);
|
||||||
"SELECT role FROM user_accounts WHERE user_id = ? AND account_id = ?"
|
|
||||||
).get(session.userId, accountId);
|
|
||||||
return !!(row && row.role === 'editor');
|
return !!(row && row.role === 'editor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -30,10 +30,11 @@ router.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch checks in the order provided; verify each belongs to the declared account
|
// Fetch checks in the order provided; verify each belongs to the declared account
|
||||||
|
const checkStmt = db.prepare('SELECT * FROM checks WHERE id = ?');
|
||||||
let checks;
|
let checks;
|
||||||
try {
|
try {
|
||||||
checks = checkIds.map(id => {
|
checks = checkIds.map(id => {
|
||||||
const check = db.prepare('SELECT * FROM checks WHERE id = ?').get(id);
|
const check = checkStmt.get(id);
|
||||||
if (!check) throw new Error(`Check ID ${id} not found`);
|
if (!check) throw new Error(`Check ID ${id} not found`);
|
||||||
if (check.account_id !== resolvedAccountId) throw new Error(`Check ID ${id} does not belong to this account`);
|
if (check.account_id !== resolvedAccountId) throw new Error(`Check ID ${id} does not belong to this account`);
|
||||||
return check;
|
return check;
|
||||||
|
|||||||
Reference in New Issue
Block a user