openvibers/openvibe.chat Commits

Chat history: an ISO ?before= cursor pages correctly (#544)

OP OpenVibers committed 2 days ago · c87a23a358 · 3 files, +28 −5

Since #542 the history pages give times as ISO, but /api/chat/history/:streamId compared ?before= raw
against the stored YYYY-MM-DD HH:MM:SS text; ISO sorts after every row of its own day, so a client
echoing a returned time got that day again. storedTime() puts the cursor in the stored shape (UTC); a
before that is not a time is a 400.

Changes

ddiff --git a/server/chat/routes.js b/server/chat/routes.js
iindex c14ab05..24bc728 100644
---- a/server/chat/routes.js
++++ b/server/chat/routes.js
@@@ -591,7 +591,9 @@ router.get('/:streamId/history', optionalAuth, async (req, res) => {
const streamId = positiveInt(req.params.streamId);
if (streamId === null) return res.status(400).json({ error: 'Invalid stream ID' });
const limit = Math.min(parseInt(req.query.limit || '500'), 500);
-- const before = req.query.before; // ISO timestamp for pagination
++ // A time cursor: the pages give times as ISO (#542) and the column keeps 'YYYY-MM-DD HH:MM:SS' UTC text.
++ const before = req.query.before != null && req.query.before !== '' ? require('../iso-times').storedTime(String(req.query.before)) : null;
++ if (req.query.before != null && req.query.before !== '' && !before) return res.status(400).json({ error: 'before is a time, like 2026-11-12T10:00:00Z' });
// Resolve which broadcaster this stream belongs to.
const stream = await ctx.ensureStream(streamId);
ddiff --git a/server/iso-times.js b/server/iso-times.js
iindex 9c16da7..d6074e6 100644
---- a/server/iso-times.js
++++ b/server/iso-times.js
@@@ -9,8 +9,8 @@
* stored text, cursors built from it and every query that compares it stay exactly as they are.
*
* Already-ISO values (with Z or an offset), dates (`YYYY-MM-DD`) and non-strings pass through.
-- * `POST /api/v1/...` cursors echo their own stored text, not one of these fields, so pagination is
-- * unaffected.
++ * `POST /api/v1/...` cursors echo their own stored text, not one of these fields. A time cursor a page
++ * documents as ISO (`/api/chat/history/:streamId?before=`) goes back through storedTime() first.
*/
const SQL_TEXT_TIME = /^\d{4}-\d\d-\d\d[ T]\d\d:\d\d(?::\d\d(?:\.\d+)?)?$/;
// created_at, updated_at, started_at, ended_at, expires_at, last_message_at, timestamp, …
@@@ -43,4 +43,17 @@ function isoJsonTimes(req, res, next) {
next();
}
--module.exports = { TIME_KEY, SQL_TEXT_TIME, isoTime, normalizeTimes, isoJsonTimes };
++/**
++ * The other way: a time a client sends back (an ISO `?before=` echoed from a page) in the stored shape, so a text
++ * comparison against the column orders correctly. ISO puts a 'T' where the stored text has a space, and 'T' sorts after
++ * every digit and the space, so an ISO cursor compared raw let through every row of that same day. Null when it is not
++ * a time.
++ */
++function storedTime(value) {
++ if (typeof value !== 'string' || !value || value.length > 40) return null;
++ const v = value.trim();
++ const t = Date.parse(SQL_TEXT_TIME.test(v) ? `${v.replace(' ', 'T')}Z` : v);
++ return Number.isFinite(t) ? new Date(t).toISOString().replace('T', ' ').slice(0, 19) : null;
++}
++
++module.exports = { TIME_KEY, SQL_TEXT_TIME, isoTime, normalizeTimes, isoJsonTimes, storedTime };
ddiff --git a/test/iso-times.test.js b/test/iso-times.test.js
iindex 730421f..a5487b6 100644
---- a/test/iso-times.test.js
++++ b/test/iso-times.test.js
@@@ -7,7 +7,15 @@
*/
const assert = require('assert');
const { boot, suite } = require('./helpers');
--const { isoTime, normalizeTimes } = require('../server/iso-times');
++const { isoTime, normalizeTimes, storedTime } = require('../server/iso-times');
++
++// A cursor sent back as ISO is compared in the stored shape (an ISO string sorts after every row of its own day).
++assert.strictEqual(storedTime('2026-11-12T10:00:05.123Z'), '2026-11-12 10:00:05', 'ISO with Z → stored text');
++assert.strictEqual(storedTime('2026-11-12T12:00:05+02:00'), '2026-11-12 10:00:05', 'an offset is moved to UTC');
++assert.strictEqual(storedTime('2026-11-12 10:00:05'), '2026-11-12 10:00:05', 'the stored shape is read as UTC and kept');
++assert.strictEqual(storedTime('not a time'), null, 'not a time');
++assert.strictEqual(storedTime(''), null, 'empty');
++assert.ok(storedTime('2026-11-12T10:00:05Z') < '2026-11-12 10:00:06' && storedTime('2026-11-12T10:00:05Z') > '2026-11-12 10:00:04', 'and it orders against the column');
assert.strictEqual(isoTime('2026-11-12 05:01:02'), '2026-11-12T05:01:02Z');
assert.strictEqual(isoTime('2026-11-12T05:01:02.123Z'), '2026-11-12T05:01:02.123Z', 'ISO with Z is left alone');