Add backend support for users to follow series

Introduces a UserSeriesFollow model with a dedicated join table so users
can follow/unfollow series and receive socket notifications when new
books are added to followed series. The user JSON response now includes a
seriesFollowing array, and three new API endpoints are available under
/api/me/follows/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul DeVito 2026-03-22 20:29:11 -04:00
parent 8b89b27654
commit 37b95582a2
9 changed files with 744 additions and 5 deletions

View file

@ -513,5 +513,87 @@ class MeController {
const data = await userStats.getStatsForYear(req.user.id, year)
res.json(data)
}
/**
* POST: /api/me/follows/series/:id
* Follow a series
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async followSeries(req, res) {
const series = await Database.seriesModel.findByPk(req.params.id)
if (!series) {
Logger.error(`[MeController] followSeries: Series ${req.params.id} not found`)
return res.sendStatus(404)
}
const existing = await Database.userSeriesFollowModel.findOne({
where: { userId: req.user.id, seriesId: series.id }
})
if (!existing) {
await Database.userSeriesFollowModel.create({
userId: req.user.id,
seriesId: series.id
})
Database.userModel.seriesFollowChanged(req.user.id)
}
const seriesFollowing = await Database.userSeriesFollowModel.getFollowedSeriesIdsForUser(req.user.id)
SocketAuthority.clientEmitter(req.user.id, 'user_series_follows_updated', { seriesFollowing })
res.sendStatus(200)
}
/**
* DELETE: /api/me/follows/series/:id
* Unfollow a series
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async unfollowSeries(req, res) {
const deleted = await Database.userSeriesFollowModel.destroy({
where: { userId: req.user.id, seriesId: req.params.id }
})
if (!deleted) {
return res.sendStatus(404)
}
Database.userModel.seriesFollowChanged(req.user.id)
const seriesFollowing = await Database.userSeriesFollowModel.getFollowedSeriesIdsForUser(req.user.id)
SocketAuthority.clientEmitter(req.user.id, 'user_series_follows_updated', { seriesFollowing })
res.sendStatus(200)
}
/**
* GET: /api/me/follows
* Get all follows for the current user
* Optional query param: ?type=series
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getFollows(req, res) {
const type = req.query.type
const result = {}
if (!type || type === 'series') {
const seriesFollows = await Database.userSeriesFollowModel.findAll({
where: { userId: req.user.id },
include: { model: Database.seriesModel, attributes: ['id', 'name', 'libraryId'] },
order: [['createdAt', 'DESC']]
})
result.series = seriesFollows.map((sf) => ({
seriesId: sf.seriesId,
seriesName: sf.series?.name || null,
libraryId: sf.series?.libraryId || null,
createdAt: sf.createdAt.valueOf()
}))
}
res.json(result)
}
}
module.exports = new MeController()