feat(backend): Prisma schema, Express routes, audit middleware, error handling

This commit is contained in:
Experiments DB Dev
2026-04-15 10:40:39 -04:00
parent 44dfe3fdc4
commit 04d1eee8f8
13 changed files with 9104 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
const prisma = require('../lib/prisma');
/**
* Factory that creates audit middleware for a given Prisma model.
* On UPDATE: captures before/after diff.
* On DELETE: captures the full record before deletion.
*
* @param {string} tableName - The human-readable table name for audit logs
* @param {object} model - The Prisma model delegate (e.g., prisma.experiment)
* @param {string} idParam - The route param name that holds the record id (default: 'id')
*/
function auditMiddleware(tableName, model, idParam = 'id') {
return async (req, res, next) => {
const recordId = req.params[idParam];
if (!recordId) return next();
try {
const before = await model.findUnique({ where: { id: recordId } });
if (!before) return next();
// Attach before-state so route handlers can reference it if needed
req._auditBefore = before;
// Wrap res.json to intercept the response
const originalJson = res.json.bind(res);
res.json = async function (data) {
// Only log on successful mutations
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const action = req.method === 'DELETE' ? 'DELETE' : 'UPDATE';
const after = action === 'DELETE' ? null : await model.findUnique({ where: { id: recordId } });
const changes = {
before,
after,
};
await prisma.auditLog.create({
data: {
table_name: tableName,
record_id: recordId,
action,
changes,
},
});
} catch (auditErr) {
// Audit failures should never block the primary response
console.error('Audit log write failed:', auditErr.message);
}
}
return originalJson(data);
};
next();
} catch (err) {
next(err);
}
};
}
module.exports = auditMiddleware;
+40
View File
@@ -0,0 +1,40 @@
const { validationResult } = require('express-validator');
/**
* Validation check middleware — call after express-validator chains.
* Returns 422 with field-level errors if validation fails.
*/
function validateRequest(req, res, next) {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: 'Validation failed',
details: errors.array().map((e) => ({ field: e.path, message: e.msg })),
});
}
next();
}
/**
* Global Express error handler — must be registered last.
*/
function globalErrorHandler(err, req, res, next) {
console.error(err);
// Prisma known errors
if (err.code === 'P2025') {
return res.status(404).json({ error: 'Record not found' });
}
if (err.code === 'P2003') {
return res.status(400).json({ error: 'Foreign key constraint failed — referenced record does not exist' });
}
if (err.code === 'P2002') {
return res.status(409).json({ error: 'A record with these unique fields already exists' });
}
const status = err.status || 500;
const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message;
res.status(status).json({ error: message });
}
module.exports = { validateRequest, globalErrorHandler };