52d5fca5d1
Each saved file now records three additional fields:
matched_identifier — the literal animal_id_string or animal_name
substring that matched in the filename
animal_id_string_snap — snapshot of animal.animal_id_string at
registration time
animal_name_snap — snapshot of animal.animal_name at
registration time
This lets future consumers (scripts, exports, re-matching) identify
the subject from the file record alone, even if animal data changes.
The matched identifier is also shown in the drop preview UI (↳ 2852).
Tests: 25 total (+2 for new payload fields).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
3.2 KiB
JavaScript
82 lines
3.2 KiB
JavaScript
const router = require('express').Router();
|
|
const prisma = require('../lib/prisma');
|
|
|
|
// POST /api/daily-statuses/:id/files — register file metadata (batch)
|
|
// Body: [{ filename, file_size, file_type, last_modified,
|
|
// matched_identifier, animal_id_string_snap, animal_name_snap, notes }, ...]
|
|
router.post('/daily-statuses/:id/files', async (req, res, next) => {
|
|
try {
|
|
const status = await prisma.dailyStatus.findUnique({ where: { id: req.params.id } });
|
|
if (!status) return res.status(404).json({ error: 'Daily status not found' });
|
|
|
|
const files = req.body;
|
|
if (!Array.isArray(files) || files.length === 0)
|
|
return res.status(422).json({ error: 'Request body must be a non-empty array of file metadata' });
|
|
|
|
if (files.length > 200)
|
|
return res.status(422).json({ error: 'At most 200 files per request' });
|
|
|
|
for (const f of files) {
|
|
if (!f.filename || typeof f.filename !== 'string' || !f.filename.trim())
|
|
return res.status(422).json({ error: 'Each file must have a non-empty filename' });
|
|
if (f.file_size == null || typeof f.file_size !== 'number' || f.file_size < 0)
|
|
return res.status(422).json({ error: `file_size must be a non-negative number (got: ${f.file_size})` });
|
|
}
|
|
|
|
const created = await prisma.$transaction(
|
|
files.map((f) =>
|
|
prisma.sessionFile.create({
|
|
data: {
|
|
daily_status_id: req.params.id,
|
|
filename: f.filename.trim(),
|
|
file_size: BigInt(Math.round(f.file_size)),
|
|
file_type: f.file_type ?? null,
|
|
last_modified: f.last_modified != null ? BigInt(Math.round(f.last_modified)) : null,
|
|
matched_identifier: f.matched_identifier ?? null,
|
|
animal_id_string_snap: f.animal_id_string_snap ?? null,
|
|
animal_name_snap: f.animal_name_snap ?? null,
|
|
notes: f.notes ?? null,
|
|
},
|
|
})
|
|
)
|
|
);
|
|
|
|
res.status(201).json(created.map(serializeFile));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// GET /api/daily-statuses/:id/files
|
|
router.get('/daily-statuses/:id/files', async (req, res, next) => {
|
|
try {
|
|
const status = await prisma.dailyStatus.findUnique({ where: { id: req.params.id } });
|
|
if (!status) return res.status(404).json({ error: 'Daily status not found' });
|
|
|
|
const files = await prisma.sessionFile.findMany({
|
|
where: { daily_status_id: req.params.id },
|
|
orderBy: { created_at: 'asc' },
|
|
});
|
|
res.json(files.map(serializeFile));
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// DELETE /api/session-files/:id
|
|
router.delete('/session-files/:id', async (req, res, next) => {
|
|
try {
|
|
const file = await prisma.sessionFile.findUnique({ where: { id: req.params.id } });
|
|
if (!file) return res.status(404).json({ error: 'Session file not found' });
|
|
await prisma.sessionFile.delete({ where: { id: req.params.id } });
|
|
res.status(204).send();
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// BigInt fields must be serialized to strings to survive JSON.stringify
|
|
function serializeFile(f) {
|
|
return {
|
|
...f,
|
|
file_size: f.file_size.toString(),
|
|
last_modified: f.last_modified != null ? f.last_modified.toString() : null,
|
|
};
|
|
}
|
|
|
|
module.exports = router;
|