fix(sequences): coerce NUMERIC frame_rate to float in all API responses

node-postgres returns NUMERIC columns as strings by default.  Add a
mapSeq() helper that parses frame_rate to a JS float before any response
is sent.  Affected routes: GET /, POST /, PUT /:id, GET /:id.
This commit is contained in:
Zac Gaetano 2026-05-19 23:24:16 -04:00
parent bfc2649909
commit 4d0e715982

View file

@ -7,6 +7,15 @@ import { requireAuth } from '../middleware/auth.js';
const router = express.Router();
router.use(requireAuth);
// ── Row mapper ────────────────────────────────────────────────────────────────
// node-postgres returns NUMERIC columns as strings. Coerce frame_rate to a
// JS float before sending any sequence object to clients.
function mapSeq(row) {
if (!row) return row;
return { ...row, frame_rate: parseFloat(row.frame_rate) || 59.94 };
}
// ── Timecode helpers ──────────────────────────────────────────────────────────
//
// generateEDL emits CMX3600 timecode using the sequence's frame_rate.
@ -19,7 +28,7 @@ router.use(requireAuth);
function pad2(n) { return String(Math.floor(n)).padStart(2, '0'); }
function framesToTC(totalFrames, fps) {
fps = fps || 59.94;
fps = parseFloat(fps) || 59.94;
const fc = Math.max(0, Math.round(totalFrames));
// 29.97 DF ─ drop 2 frames per minute except every 10th
@ -77,7 +86,7 @@ function framesToTC(totalFrames, fps) {
}
function generateEDL(seqName, clips, fps) {
fps = fps || 59.94;
fps = parseFloat(fps) || 59.94;
const lines = [`TITLE: ${seqName}`, ''];
clips.forEach((c, i) => {
const num = String(i + 1).padStart(3, '0');
@ -105,7 +114,7 @@ router.get('/', async (req, res, next) => {
`SELECT * FROM sequences WHERE project_id = $1 ORDER BY updated_at DESC`,
[project_id]
);
res.json(r.rows);
res.json(r.rows.map(mapSeq));
} catch (e) { next(e); }
});
@ -125,7 +134,7 @@ router.post('/', async (req, res, next) => {
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[project_id, name, frame_rate, width, height]
);
res.status(201).json(r.rows[0]);
res.status(201).json(mapSeq(r.rows[0]));
} catch (e) { next(e); }
});
@ -160,7 +169,7 @@ router.get('/:id', async (req, res, next) => {
})
);
res.json({ ...seqR.rows[0], clips });
res.json({ ...mapSeq(seqR.rows[0]), clips });
} catch (e) { next(e); }
});
@ -183,7 +192,7 @@ router.put('/:id', async (req, res, next) => {
params
);
if (!r.rows.length) return res.status(404).json({ error: 'Sequence not found' });
res.json(r.rows[0]);
res.json(mapSeq(r.rows[0]));
} catch (e) { next(e); }
});
@ -254,7 +263,7 @@ router.post('/:id/export/edl', async (req, res, next) => {
try {
const seqR = await pool.query(`SELECT * FROM sequences WHERE id = $1`, [req.params.id]);
if (!seqR.rows.length) return res.status(404).json({ error: 'Sequence not found' });
const seq = seqR.rows[0];
const seq = mapSeq(seqR.rows[0]);
// Export V1 clips only (primary video track) sorted by position
const clipsR = await pool.query(