From c720ca3bb8a048ea503b5f359edea0f557f136b7 Mon Sep 17 00:00:00 2001 From: ra939 Date: Mon, 6 Oct 2025 07:47:36 +0000 Subject: [PATCH] Revert "Commit backend changes for recommandation feature" This reverts commit f60449ed6ab54c9ae100b6b5c75ae7192fb48970. --- server/models/BookRecommendation.js | 77 ---------- server/models/RecommendationTag.js | 20 --- server/routers/recommendations.js | 213 ---------------------------- 3 files changed, 310 deletions(-) delete mode 100644 server/models/BookRecommendation.js delete mode 100644 server/models/RecommendationTag.js delete mode 100644 server/routers/recommendations.js diff --git a/server/models/BookRecommendation.js b/server/models/BookRecommendation.js deleted file mode 100644 index dc6df3fa8..000000000 --- a/server/models/BookRecommendation.js +++ /dev/null @@ -1,77 +0,0 @@ -// server/models/BookRecommendation.js -'use strict' - -module.exports.init = (sequelize) => { - const { DataTypes } = require('sequelize') - - const BookRecommendation = sequelize.define( - 'bookRecommendation', - { - // LibraryItem id (UUID string) - bookId: { type: DataTypes.STRING, allowNull: false }, - - // -> users.id (UUID string) — the recommender (sender) - recommenderUserId: { type: DataTypes.STRING, allowNull: false }, - - // -> users.id (UUID string), nullable — the recipient (if any) - recipientUserId: { type: DataTypes.STRING, allowNull: true }, - - // -> recommendation_tags.id (INTEGER) - tagId: { type: DataTypes.INTEGER, allowNull: false }, - - note: { type: DataTypes.TEXT, allowNull: true, validate: { len: [0, 1000] } }, - - visibility: { - type: DataTypes.ENUM('public', 'recipient-only'), - allowNull: false, - defaultValue: 'public' - } - }, - { - tableName: 'book_recommendations', - // helpful indexes for common queries - indexes: [ - { fields: ['bookId'] }, - { fields: ['recommenderUserId'] }, - { fields: ['recipientUserId'] }, - { fields: ['tagId'] }, - { fields: ['visibility'] } - ] - } - ) - - BookRecommendation.associate = (models) => { - const { user: User, recommendationTag: RecommendationTag } = models - - if (User) { - // Canonical aliases - BookRecommendation.belongsTo(User, { - as: 'recommender', - foreignKey: 'recommenderUserId' - }) - BookRecommendation.belongsTo(User, { - as: 'recipient', - foreignKey: 'recipientUserId' - }) - // (removed the duplicate alias "user") - } - - if (RecommendationTag) { - BookRecommendation.belongsTo(RecommendationTag, { - as: 'tag', - foreignKey: 'tagId' - }) - } - - // Link to libraryItem so router can include media title - if (models.libraryItem) { - BookRecommendation.belongsTo(models.libraryItem, { - as: 'item', - foreignKey: 'bookId', - targetKey: 'id' - }) - } - } - - return BookRecommendation -} diff --git a/server/models/RecommendationTag.js b/server/models/RecommendationTag.js deleted file mode 100644 index c8a361a79..000000000 --- a/server/models/RecommendationTag.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' -module.exports.init = (sequelize) => { - const { DataTypes } = require('sequelize') - const RecommendationTag = sequelize.define( - 'recommendationTag', - { - slug: { type: DataTypes.STRING, allowNull: false, unique: true, validate: { is: /^[a-z0-9-]+$/ } }, - label: { type: DataTypes.STRING, allowNull: false }, - isActive: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true } - }, - { tableName: 'recommendation_tags' } - ) - - RecommendationTag.associate = (models) => { - if (models.bookRecommendation) { - RecommendationTag.hasMany(models.bookRecommendation, { as: 'recommendations', foreignKey: 'tagId' }) - } - } - return RecommendationTag -} diff --git a/server/routers/recommendations.js b/server/routers/recommendations.js deleted file mode 100644 index 70db4cfeb..000000000 --- a/server/routers/recommendations.js +++ /dev/null @@ -1,213 +0,0 @@ -// server/routers/recommendations.js -// Community recommendations API (minimal) -const express = require('express') -const { Op } = require('sequelize') -const Database = require('../Database') -const sessionOrJwt = require('../middleware/sessionOrJwt') -const requireAuthFactory = require('../middleware/requireAuth') - -const DEFAULT_TAGS = [ - { slug: 'fiction', label: 'Fiction' }, - { slug: 'non-fiction', label: 'Non-Fiction' }, - { slug: 'mystery', label: 'Mystery' }, - { slug: 'romance', label: 'Romance' }, - { slug: 'science-fiction', label: 'Science Fiction' }, - { slug: 'fantasy', label: 'Fantasy' }, - { slug: 'biography', label: 'Biography' }, - { slug: 'history', label: 'History' }, - { slug: 'self-help', label: 'Self-Help' }, - { slug: 'young-adult', label: 'Young Adult' } -] - -function enabled(res) { - if (String(process.env.RECOMMENDATIONS_ENABLED) !== 'true') { - res.status(501).json({ message: 'Recommendations disabled' }) - return false - } - return true -} -const sanitizeNote = (n) => { if (!n) return null; n = String(n).trim(); return n.length > 1000 ? n.slice(0, 1000) : n } - -// Common include that also brings back the media title -const BASE_INCLUDE = [ - { association: 'tag', attributes: ['id', 'slug', 'label'] }, - { association: 'recommender', attributes: ['id', 'username'] }, - { association: 'recipient', attributes: ['id', 'username'] }, - { - association: 'item', - attributes: ['id'], - include: [ - { association: 'book', attributes: ['id', 'title'] }, - { association: 'podcast', attributes: ['id', 'title'] } - ] - } -] - -// helper to flatten title -const addBookTitle = (rowsOrInstance) => { - const toJSON = (r) => (typeof r?.toJSON === 'function' ? r.toJSON() : r || {}) - const shape = (j) => ({ ...j, bookTitle: j?.item?.book?.title || j?.item?.podcast?.title || null }) - - if (Array.isArray(rowsOrInstance)) return rowsOrInstance.map((r) => shape(toJSON(r))) - if (rowsOrInstance) return shape(toJSON(rowsOrInstance)) - return rowsOrInstance -} - -module.exports = (auth) => { - const router = express.Router() - const requireAuth = requireAuthFactory(auth) - - router.use(sessionOrJwt(auth)) - - // GET /tags (public) - router.get('/tags', async (req, res) => { - if (!enabled(res)) return - const Tag = Database.recommendationTagModel - if ((await Tag.count()) === 0) { - const now = new Date() - await Tag.bulkCreate(DEFAULT_TAGS.map((t) => ({ ...t, isActive: true, createdAt: now, updatedAt: now }))) - } - const tags = await Tag.findAll({ - where: { isActive: true }, - order: [['label', 'ASC']], - attributes: ['id', 'slug', 'label'] - }) - res.json(tags) - }) - - // POST / (create; auth) - router.post('/', requireAuth, async (req, res) => { - if (!enabled(res)) return - const { bookId, tagSlug, tagId, recipientUserId, note, visibility } = req.body || {} - if (!bookId) return res.status(400).json({ message: 'bookId is required' }) - if (!tagSlug && !tagId) return res.status(400).json({ message: 'tagSlug or tagId is required' }) - - const Tag = Database.recommendationTagModel - const tag = tagId - ? await Tag.findByPk(tagId) - : await Tag.findOne({ where: { slug: String(tagSlug).toLowerCase() } }) - if (!tag || tag.isActive === false) return res.status(400).json({ message: 'Invalid tag' }) - - let recip = null - if (recipientUserId) { - recip = await Database.userModel.findByPk(recipientUserId) - if (!recip) return res.status(400).json({ message: 'Recipient not found' }) - } - - const created = await Database.bookRecommendationModel.create({ - bookId: String(bookId), - tagId: tag.id, - recommenderUserId: req.user.id, - recipientUserId: recip ? recip.id : null, - note: sanitizeNote(note), - visibility: visibility === 'recipient-only' ? 'recipient-only' : 'public' - }) - - const full = await Database.bookRecommendationModel.findByPk(created.id, { include: BASE_INCLUDE }) - res.status(201).json(addBookTitle(full)) - }) - - // GET /book/:bookId - router.get('/book/:bookId', async (req, res) => { - if (!enabled(res)) return - const me = req.user || null - const where = { - bookId: String(req.params.bookId), - [Op.or]: [ - { visibility: 'public' }, - me - ? { - [Op.and]: [ - { visibility: 'recipient-only' }, - { [Op.or]: [{ recommenderUserId: me.id }, { recipientUserId: me.id }] } - ] - } - : { id: null } - ] - } - const rows = await Database.bookRecommendationModel.findAll({ - where, - order: [['createdAt', 'DESC']], - include: BASE_INCLUDE, - limit: Math.min(Number(req.query.limit) || 50, 100), - offset: Number(req.query.offset) || 0 - }) - res.json(addBookTitle(rows)) - }) - - // GET /inbox (auth) - router.get('/inbox', requireAuth, async (req, res) => { - if (!enabled(res)) return - const me = req.user.id - const { bookId, tagSlug } = req.query - - const where = { - [Op.or]: [ - { recipientUserId: me }, - { [Op.and]: [{ visibility: 'public' }, { recommenderUserId: { [Op.ne]: me } }] } - ] - } - if (bookId) where.bookId = String(bookId) - if (tagSlug) { - const tag = await Database.recommendationTagModel.findOne({ - where: { slug: String(tagSlug).toLowerCase() } - }) - if (!tag) return res.json([]) - where.tagId = tag.id - } - - const rows = await Database.bookRecommendationModel.findAll({ - where, - order: [['createdAt', 'DESC']], - include: BASE_INCLUDE, - limit: Math.min(Number(req.query.limit) || 50, 100), - offset: Number(req.query.offset) || 0 - }) - res.json(addBookTitle(rows)) - }) - - // GET /sent (auth) - router.get('/sent', requireAuth, async (req, res) => { - if (!enabled(res)) return - const rows = await Database.bookRecommendationModel.findAll({ - where: { recommenderUserId: req.user.id }, - order: [['createdAt', 'DESC']], - include: BASE_INCLUDE, - limit: Math.min(Number(req.query.limit) || 50, 100), - offset: Number(req.query.offset) || 0 - }) - res.json(addBookTitle(rows)) - }) - - // PUT /:id (auth + owner) - router.put('/:id', requireAuth, async (req, res) => { - if (!enabled(res)) return - const rec = await Database.bookRecommendationModel.findByPk(req.params.id) - if (!rec) return res.sendStatus(404) - if (rec.recommenderUserId !== req.user.id) return res.sendStatus(403) - - const update = {} - if (typeof req.body.note !== 'undefined') update.note = sanitizeNote(req.body.note) - if (['public', 'recipient-only'].includes(req.body.visibility)) update.visibility = req.body.visibility - if (req.body.tagId) { - const tag = await Database.recommendationTagModel.findByPk(req.body.tagId) - if (!tag || tag.isActive === false) return res.status(400).json({ message: 'Invalid tag' }) - update.tagId = tag.id - } - await rec.update(update) - const full = await Database.bookRecommendationModel.findByPk(rec.id, { include: BASE_INCLUDE }) - res.json(addBookTitle(full)) - }) - - // DELETE /:id (auth + owner) - router.delete('/:id', requireAuth, async (req, res) => { - if (!enabled(res)) return - const rec = await Database.bookRecommendationModel.findByPk(req.params.id) - if (!rec) return res.sendStatus(404) - if (rec.recommenderUserId !== req.user.id) return res.sendStatus(403) - await rec.destroy() - res.sendStatus(204) - }) - - return router -}