From 38029d1202ebcc5f7a43f6059e7b2229815b960f Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 11 Aug 2023 17:49:06 -0500 Subject: [PATCH] Update library collections api endpoint to use libraryItems from db --- server/controllers/CollectionController.js | 13 +-- server/controllers/LibraryController.js | 47 ++++---- server/models/Collection.js | 101 ++++++++++++++++++ server/models/Feed.js | 5 + server/models/LibraryItem.js | 12 ++- server/routers/ApiRouter.js | 6 +- server/utils/queries/libraryFilters.js | 11 +- .../utils/queries/libraryItemsBookFilters.js | 45 +++++++- 8 files changed, 209 insertions(+), 31 deletions(-) diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 682088cc5b..30bb2a619d 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -8,22 +8,23 @@ class CollectionController { constructor() { } async create(req, res) { - var newCollection = new Collection() + const newCollection = new Collection() req.body.userId = req.user.id - var success = newCollection.setData(req.body) - if (!success) { + if (!newCollection.setData(req.body)) { return res.status(500).send('Invalid collection data') } - var jsonExpanded = newCollection.toJSONExpanded(Database.libraryItems) + + const libraryItemsInCollection = await Database.models.libraryItem.getForCollection(newCollection) + const jsonExpanded = newCollection.toJSONExpanded(libraryItemsInCollection) await Database.createCollection(newCollection) SocketAuthority.emitter('collection_added', jsonExpanded) res.json(jsonExpanded) } async findAll(req, res) { - const collections = await Database.models.collection.getOldCollections() + const collectionsExpanded = await Database.models.collection.getOldCollectionsJsonExpanded(req.user) res.json({ - collections: collections.map(c => c.toJSONExpanded(Database.libraryItems)) + collections: collectionsExpanded }) } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index fac9df9b3a..d63e834b1d 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -535,8 +535,6 @@ class LibraryController { // api/libraries/:id/collections async getCollectionsForLibrary(req, res) { - const libraryItems = req.libraryItems - const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) const payload = { @@ -551,23 +549,8 @@ class LibraryController { include: include.join(',') } - const collectionsForLibrary = await Database.models.collection.getAllForLibrary(req.library.id) - - let collections = await Promise.all(collectionsForLibrary.map(async c => { - const expanded = c.toJSONExpanded(libraryItems, payload.minified) - - // If all books restricted to user in this collection then hide this collection - if (!expanded.books.length && c.books.length) return null - - if (include.includes('rssfeed')) { - const feedData = await this.rssFeedManager.findFeedForEntityId(c.id) - expanded.rssFeed = feedData?.toJSONMinified() || null - } - - return expanded - })) - - collections = collections.filter(c => !!c) + // TODO: Create paginated queries + let collections = await Database.models.collection.getOldCollectionsJsonExpanded(req.user, req.library.id, include) payload.total = collections.length @@ -964,6 +947,12 @@ class LibraryController { res.send(opmlText) } + /** + * TODO: Replace with middlewareNew + * @param {*} req + * @param {*} res + * @param {*} next + */ async middleware(req, res, next) { if (!req.user.checkCanAccessLibrary(req.params.id)) { Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`) @@ -980,5 +969,25 @@ class LibraryController { }) next() } + + /** + * Middleware that is not using libraryItems from memory + * @param {*} req + * @param {*} res + * @param {*} next + */ + async middlewareNew(req, res, next) { + if (!req.user.checkCanAccessLibrary(req.params.id)) { + Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`) + return res.sendStatus(403) + } + + const library = await Database.models.library.getOldById(req.params.id) + if (!library) { + return res.status(404).send('Library not found') + } + req.library = library + next() + } } module.exports = new LibraryController() diff --git a/server/models/Collection.js b/server/models/Collection.js index 3c7c13867a..ebe6a59722 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -5,6 +5,10 @@ const { areEquivalent } = require('../utils/index') module.exports = (sequelize) => { class Collection extends Model { + /** + * Get all old collections + * @returns {Promise} + */ static async getOldCollections() { const collections = await this.findAll({ include: { @@ -16,6 +20,103 @@ module.exports = (sequelize) => { return collections.map(c => this.getOldCollection(c)) } + /** + * Get all old collections toJSONExpanded, items filtered for user permissions + * @param {[oldUser]} user + * @param {[string]} libraryId + * @param {[string[]]} include + * @returns {Promise} oldCollection.toJSONExpanded + */ + static async getOldCollectionsJsonExpanded(user, libraryId, include) { + let collectionWhere = null + if (libraryId) { + collectionWhere = { + libraryId + } + } + + // Optionally include rssfeed for collection + const collectionIncludes = [] + if (include.includes('rssfeed')) { + collectionIncludes.push({ + model: sequelize.models.feed + }) + } + + const collections = await this.findAll({ + where: collectionWhere, + include: [ + { + model: sequelize.models.book, + include: [ + { + model: sequelize.models.libraryItem + }, + { + model: sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: sequelize.models.series, + through: { + attributes: ['sequence'] + } + }, + + ] + }, + ...collectionIncludes + ], + order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']] + }) + // TODO: Handle user permission restrictions on initial query + return collections.map(c => { + const oldCollection = this.getOldCollection(c) + + // Filter books using user permissions + const books = c.books?.filter(b => { + if (user) { + if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { + return false + } + if (b.explicit === true && !user.canAccessExplicitContent) { + return false + } + } + return true + }) || [] + + // Map to library items + const libraryItems = books.map(b => { + const libraryItem = b.libraryItem + delete b.libraryItem + libraryItem.media = b + return sequelize.models.libraryItem.getOldLibraryItem(libraryItem) + }) + + // Users with restricted permissions will not see this collection + if (!books.length && oldCollection.books.length) { + return null + } + + const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) + + // Map feed if found + if (c.feeds?.length) { + collectionExpanded.rssFeed = sequelize.models.feed.getOldFeed(c.feeds[0]) + } + + return collectionExpanded + }).filter(c => c) + } + + /** + * Get old collection from Collection + * @param {Collection} collectionExpanded + * @returns {oldCollection} + */ static getOldCollection(collectionExpanded) { const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || [] return new oldCollection({ diff --git a/server/models/Feed.js b/server/models/Feed.js index a78c7723ce..25248b3c05 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -16,6 +16,11 @@ module.exports = (sequelize) => { return feeds.map(f => this.getOldFeed(f)) } + /** + * Get old feed from Feed and optionally Feed with FeedEpisodes + * @param {Feed} feedExpanded + * @returns {oldFeed} + */ static getOldFeed(feedExpanded) { const episodes = feedExpanded.feedEpisodes?.map((feedEpisode) => feedEpisode.getOldEpisode()) return new oldFeed({ diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 76e10b8c3e..59d4643156 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -661,13 +661,23 @@ module.exports = (sequelize) => { * Get book library items for author, optional use user permissions * @param {oldAuthor} author * @param {[oldUser]} user - * @returns {oldLibraryItem[]} + * @returns {Promise} */ static async getForAuthor(author, user = null) { const { libraryItems } = await libraryFilters.getLibraryItemsForAuthor(author, user, undefined, undefined) return libraryItems.map(li => this.getOldLibraryItem(li)) } + /** + * Get book library items in a collection + * @param {oldCollection} collection + * @returns {Promise} + */ + static async getForCollection(collection) { + const libraryItems = await libraryFilters.getLibraryItemsForCollection(collection) + return libraryItems.map(li => this.getOldLibraryItem(li)) + } + getMedia(options) { if (!this.mediaType) return Promise.resolve(null) const mixinMethodName = `get${sequelize.uppercaseFirst(this.mediaType)}` diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 9f811ad413..fdd3c994d3 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -74,16 +74,16 @@ class ApiRouter { this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this)) this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this)) - this.router.get('/libraries/:id/items2', LibraryController.middleware.bind(this), LibraryController.getLibraryItemsNew.bind(this)) + this.router.get('/libraries/:id/items2', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryItemsNew.bind(this)) this.router.get('/libraries/:id/items', LibraryController.middleware.bind(this), LibraryController.getLibraryItems.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.get('/libraries/:id/episode-downloads', LibraryController.middleware.bind(this), LibraryController.getEpisodeDownloadQueue.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series/:seriesId', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this)) - this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) + this.router.get('/libraries/:id/collections', LibraryController.middlewareNew.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this)) this.router.get('/libraries/:id/albums', LibraryController.middleware.bind(this), LibraryController.getAlbumsForLibrary.bind(this)) - this.router.get('/libraries/:id/personalized2', LibraryController.middleware.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) + this.router.get('/libraries/:id/personalized2', LibraryController.middlewareNew.bind(this), LibraryController.getUserPersonalizedShelves.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this)) this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index 811be8574f..3229de3ed4 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -365,7 +365,7 @@ module.exports = { * @param {[oldUser]} user * @param {number} limit * @param {number} offset - * @returns {object} { libraryItems:LibraryItem[], count:number } + * @returns {Promise} { libraryItems:LibraryItem[], count:number } */ async getLibraryItemsForAuthor(author, user, limit, offset) { const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(author.libraryId, user, 'authors', author.id, 'addedAt', true, false, [], limit, offset) @@ -373,5 +373,14 @@ module.exports = { count, libraryItems } + }, + + /** + * Get book library items in a collection + * @param {oldCollection} collection + * @returns {Promise} + */ + getLibraryItemsForCollection(collection) { + return libraryItemsBookFilters.getLibraryItemsForCollection(collection) } } \ No newline at end of file diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 8b4a72219b..07c7f2b5f9 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -548,7 +548,7 @@ module.exports = { replacements, benchmark: true, logging: (sql, timeMs) => { - console.log(`[Query] Elapsed ${timeMs}ms.`) + console.log(`[Query] Elapsed ${timeMs}ms`) }, include: [ { @@ -870,5 +870,48 @@ module.exports = { libraryItems, count } + }, + + /** + * Get book library items in a collection + * @param {oldCollection} collection + * @returns {Promise} + */ + async getLibraryItemsForCollection(collection) { + if (!collection?.books?.length) { + Logger.error(`[libraryItemsBookFilters] Invalid collection`, collection) + return [] + } + const books = await Database.models.book.findAll({ + where: { + id: { + [Sequelize.Op.in]: collection.books + } + }, + include: [ + { + model: Database.models.libraryItem + }, + { + model: sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ] + }) + + return books.map((book) => { + const libraryItem = book.libraryItem + delete book.libraryItem + libraryItem.media = book + return libraryItem + }) } } \ No newline at end of file