diff --git a/server/api/books.ts b/server/api/books.ts index c3d7fa7a..abec86f8 100644 --- a/server/api/books.ts +++ b/server/api/books.ts @@ -11,10 +11,13 @@ import PeerReview from "../models/peerreview.js"; import Tag from "../models/tag.js"; import CIDDescriptor from "../models/ciddescriptor.js"; import conductorErrors from "../conductor-errors.js"; -import { getSubdomainFromUrl, +import { + getSubdomainFromUrl, getPaginationOffset, isEmptyString, - isValidDateObject, sleep, + isValidDateObject, + sleep, + getRandomOffset, } from "../util/helpers.js"; import { checkBookIDFormat, @@ -745,45 +748,161 @@ async function getCommonsCatalog( ) { try { const orgID = process.env.ORG_ID; - const activePage = req.query.activePage ? parseInt(req.query.activePage) : 1; - const limit = req.query.limit ? parseInt(req.query.limit) : 10; - const offset = getPaginationOffset(activePage, limit); + const activePage = req.query.activePage + ? parseInt(req.query.activePage.toString()) + : 1; + const limit = req.query.limit ? parseInt(req.query.limit.toString()) : 10; let sortObj = {}; - if(req.query.sort && req.query.sort === 'author'){ + if (req.query.sort && req.query.sort === "author") { sortObj = { - author: 1 + author: 1, }; - } - if(req.query.sort && req.query.sort === 'title'){ + } + if (req.query.sort && req.query.sort === "title") { sortObj = { - title: 1 + title: 1, }; } + const searchQueries = []; + + // Find books associated with projects + const projectWithAssociatedBookQuery = { + $expr: { + $and: [ + { $eq: ["$orgID", orgID] }, + { $ne: [{ $type: "$libreLibrary" }, "missing"] }, + { $gt: [{ $strLenBytes: "$libreLibrary" }, 0] }, + { $ne: [{ $type: "$libreCoverID" }, "missing"] }, + { $gt: [{ $strLenBytes: "$libreCoverID" }, 0] }, + ], + }, + }; + const projectWithAssociatedBookProjection = { + _id: 0, + libreLibrary: 1, + libreCoverID: 1, + }; + + const projResults = await Project.aggregate([ + { + $match: projectWithAssociatedBookQuery, + }, + { + $project: projectWithAssociatedBookProjection, + }, + ]); + + if (!Array.isArray(projResults) || projResults.length === 0) { + return res.send({ + err: false, + numFound: 0, + numTotal: 0, + books: [], + }); + } + + const projBookIDs = projResults.map( + (proj) => `${proj.libreLibrary}-${proj.libreCoverID}` + ); + const idMatchObj = { bookID: { $in: projBookIDs } }; + const pipeline: PipelineStage[] = [ - { $match: {} }, + { + $match: idMatchObj, + }, { $project: BOOK_PROJECTION }, - ] + ]; - if(Object.keys(sortObj).length > 0){ + if (Object.keys(sortObj).length > 0) { pipeline.push({ $sort: sortObj }); } - const aggResults = await Book.aggregate(pipeline).skip(offset).limit(limit); + searchQueries.push(Book.aggregate(pipeline)); + + // Find books in org's custom catalog + if (orgID !== "libretexts") { + const orgData = await Organization.findOne( + { orgID }, + { + _id: 0, + orgID: 1, + name: 1, + shortName: 1, + abbreviation: 1, + aliases: 1, + catalogMatchingTags: 1, + } + ).lean(); + + const customCatalog = await CustomCatalog.findOne( + { orgID }, + { + _id: 0, + orgID: 1, + resources: 1, + } + ).lean(); + + if (orgData) { + const hasCustomEntries = + customCatalog && + Array.isArray(customCatalog.resources) && + customCatalog.resources.length > 0; + const hasCatalogMatchingTags = + Array.isArray(orgData?.catalogMatchingTags) && + orgData?.catalogMatchingTags.length > 0; + + if (hasCustomEntries || hasCatalogMatchingTags) { + let searchAreaObj = {}; + const idMatchObj = { bookID: { $in: customCatalog?.resources } }; + const tagMatchObj = { + libraryTags: { $in: orgData.catalogMatchingTags }, + }; + if (hasCustomEntries && hasCatalogMatchingTags) { + searchAreaObj = { $or: [idMatchObj, tagMatchObj] }; + } else if (hasCustomEntries) { + searchAreaObj = idMatchObj; + } else { + searchAreaObj = tagMatchObj; + } + + searchQueries.push( + Book.aggregate([ + { $match: searchAreaObj }, + { $project: BOOK_PROJECTION }, + ]) + ); + } + } + } + + const results = await Promise.all(searchQueries); const totalNumBooks = await Book.estimatedDocumentCount(); + const aggResults = results.reduce((acc, curr) => { + if (Array.isArray(curr)) { + return acc.concat(curr); + } + return acc; + }, []); + // Ensure no duplicates const resultBookIDs = [...new Set(aggResults.map((book) => book.bookID))]; const resultBooks = [ ...aggResults.filter((book) => resultBookIDs.includes(book.bookID)), - ]; + ] + + const offset = getRandomOffset(resultBooks.length) + + const randomized = resultBooks.slice(offset, offset + limit) return res.send({ err: false, numFound: resultBooks.length, numTotal: totalNumBooks, - books: resultBooks, + books: randomized, }); } catch (e) { debugError(e); @@ -1117,23 +1236,27 @@ async function createBook( const project = await Project.findOne({ projectID }).orFail(); const libraryApp = await centralIdentity.getApplicationById(library); - if(!libraryApp) { + if (!libraryApp) { throw new Error("badlibrary"); } const subdomain = getSubdomainFromUrl(libraryApp.main_url); - if(!subdomain) { + if (!subdomain) { throw new Error("badlibrary"); } // Check project permissions - const canCreate = projectsAPI.checkProjectMemberPermission(project, user) - if(!canCreate) { + const canCreate = projectsAPI.checkProjectMemberPermission(project, user); + if (!canCreate) { throw new Error(conductorErrors.err8); } - const hasLibAccess = await centralIdentity.checkUserApplicationAccessInternal(user.centralID, libraryApp.id); - if(!hasLibAccess) { + const hasLibAccess = + await centralIdentity.checkUserApplicationAccessInternal( + user.centralID, + libraryApp.id + ); + if (!hasLibAccess) { throw new Error(conductorErrors.err8); } @@ -1148,7 +1271,7 @@ async function createBook( method: "POST", body: MindTouch.Templates.POST_CreateBook, }, - query: { abort: 'exists'} + query: { abort: "exists" }, }).catch((e) => { const err = new Error(conductorErrors.err86); err.name = "CreateBookError"; @@ -1244,11 +1367,12 @@ async function createBook( newBookID ); - if(!permsUpdated) { - console.log(`[createBook] Failed to update permissions for ${projectID}.`) // Silent fail + if (!permsUpdated) { + console.log( + `[createBook] Failed to update permissions for ${projectID}.` + ); // Silent fail } - console.log(`[createBook] Created ${bookPath}.`); return res.send({ err: false, @@ -1256,14 +1380,14 @@ async function createBook( url: bookURL, }); } catch (err: any) { - if(err.name === "DocumentNotFoundError" || err.name === "badlibrary") { + if (err.name === "DocumentNotFoundError" || err.name === "badlibrary") { return res.status(404).send({ err: true, errMsg: conductorErrors.err11, }); } debugError(err); - if(["CreateBookError", 'badlibrary'].includes(err.name)) { + if (["CreateBookError", "badlibrary"].includes(err.name)) { return res.status(400).send({ err: true, errMsg: err.message, diff --git a/server/api/projectfiles.ts b/server/api/projectfiles.ts index 172312f3..6fdf7409 100644 --- a/server/api/projectfiles.ts +++ b/server/api/projectfiles.ts @@ -19,7 +19,7 @@ import { } from "../util/projectutils.js"; import { isObjectIdOrHexString } from "mongoose"; import async from "async"; -import { assembleUrl, getPaginationOffset } from "../util/helpers.js"; +import { assembleUrl, getPaginationOffset, getRandomOffset } from "../util/helpers.js"; import { CopyObjectCommand, DeleteObjectCommand, @@ -1017,7 +1017,6 @@ async function getPublicProjectFiles( try { const page = parseInt(req.query.page.toString()) || 1; const limit = parseInt(req.query.limit.toString()) || 24; - const offset = getPaginationOffset(page, limit); const aggRes = await ProjectFile.aggregate([ { @@ -1038,6 +1037,7 @@ async function getPublicProjectFiles( $eq: ["$projectID", "$$searchID"], }, visibility: "public", + orgID: process.env.ORG_ID, }, }, { @@ -1168,21 +1168,16 @@ async function getPublicProjectFiles( _id: -1, }, }, - { - $skip: offset, - }, - { - $limit: limit, - }, ]); - const totalCount = await ProjectFile.countDocuments({ - access: "public", - }); + const totalCount = aggRes.length; + const offset = getRandomOffset(totalCount); + + const paginatedRes = aggRes.slice(offset, offset + limit); return res.send({ err: false, - files: aggRes || [], + files: paginatedRes || [], totalCount: totalCount || 0, }); } catch (e) { diff --git a/server/api/projects.js b/server/api/projects.js index 1c33e641..df15844c 100644 --- a/server/api/projects.js +++ b/server/api/projects.js @@ -1343,6 +1343,7 @@ async function getPublicProjects(req, res) { const offset = getPaginationOffset(page, limit); const projects = await Project.find({ + orgID: process.env.ORG_ID, visibility: "public", }).select({ notes: 0, leads: 0 , liaisons: 0, members: 0, auditors: 0, a11yReview: 0, flag: 0, flagDescrip: 0 diff --git a/server/util/helpers.js b/server/util/helpers.js index c136f286..ee539a38 100644 --- a/server/util/helpers.js +++ b/server/util/helpers.js @@ -304,6 +304,15 @@ export function getPaginationOffset(page, offsetMultiplier = 25) { return offset; } +/** + * Generates a random number between 0 and max + * @param {Number} max - The maximum number to generate a random offset for + * @returns {Number} - A random number between 0 and max + */ +export function getRandomOffset(max){ + return Math.floor(Math.random() * max); +} + /** * Breaks up a url into the subdomain and path * @param {string} url