feat(backend): Prisma schema, Express routes, audit middleware, error handling
This commit is contained in:
@@ -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;
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user