refactor: unify agent marketplace to single endpoint with cursor pagination

- Replace multiple marketplace routes with unified /marketplace endpoint
  - Add query string controls: category, search, limit, cursor, promoted, requiredPermission
  - Implement cursor-based pagination replacing page-based system
  - Integrate ACL permissions for proper access control
  - Fix ObjectId constructor error in Agent model
  - Update React components to use unified useGetMarketplaceAgentsQuery hook
  - Enhance type safety and remove deprecated useDynamicAgentQuery
  - Update tests for new marketplace architecture
  -Known issues:
  see more button after category switching + Unit tests
This commit is contained in:
Danny Avila 2025-06-23 11:42:24 -04:00
parent be7476d530
commit 2eef94d58d
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
22 changed files with 458 additions and 1128 deletions

View file

@ -1,175 +1,132 @@
const mongoose = require('mongoose');
const { logger } = require('~/config');
const { findCategoryByValue, getCategoriesWithCounts } = require('~/models');
const { getListAgentsByAccess } = require('~/models/Agent');
const {
findAccessibleResources,
findPubliclyAccessibleResources,
} = require('~/server/services/PermissionService');
// Get the Agent model
const Agent = mongoose.model('Agent');
// Default page size for agent browsing
const DEFAULT_PAGE_SIZE = 6;
/**
* Common pagination utility for agent queries
*
* @param {Object} filter - MongoDB filter object
* @param {number} page - Page number (1-based)
* @param {number} limit - Items per page
* @returns {Promise<Object>} Paginated results with agents and pagination info
*/
const paginateAgents = async (filter, page = 1, limit = DEFAULT_PAGE_SIZE) => {
const skip = (page - 1) * limit;
// Get total count for pagination
const total = await Agent.countDocuments(filter);
// Get agents with pagination
const agents = await Agent.find(filter)
.select('id name description avatar category support_contact authorName')
.sort({ updatedAt: -1 })
.skip(skip)
.limit(limit)
.lean();
// Calculate if there are more agents to load
const hasMore = total > page * limit;
return {
agents,
pagination: {
current: page,
hasMore,
total,
},
};
};
/**
* Get promoted/top picks agents with pagination
* Can also return all agents when showAll=true parameter is provided
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getPromotedAgents = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
// Check if this is a request for "all" agents via query parameter
const showAllAgents = req.query.showAll === 'true';
// Base filter for shared agents only
const filter = {
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
};
// Only add promoted filter if not requesting all agents
if (!showAllAgents) {
filter.is_promoted = true; // Only get promoted agents
}
const result = await paginateAgents(filter, page, limit);
res.status(200).json(result);
} catch (error) {
logger.error('[/Agents/Marketplace] Error fetching promoted agents:', error);
res.status(500).json({
error: 'Failed to fetch promoted agents',
userMessage: 'Unable to load agents. Please try refreshing the page.',
suggestion: 'Try refreshing the page or check your network connection',
const getAgentsPagedByAccess = async (
userId,
requiredPermission,
filter,
limit = DEFAULT_PAGE_SIZE,
cursor,
) => {
const accessibleIds = await findAccessibleResources({
userId,
resourceType: 'agent',
requiredPermissions: requiredPermission,
});
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
resourceType: 'agent',
requiredPermissions: requiredPermission,
});
// Use the new ACL-aware function
const data = await getListAgentsByAccess({
accessibleIds,
otherParams: filter,
limit,
after: cursor,
});
if (data?.data?.length) {
data.data = data.data.map((agent) => {
if (publiclyAccessibleIds.some((id) => id.equals(agent._id))) {
agent.isPublic = true;
}
return agent;
});
}
return data;
};
/**
* Get agents by category with pagination
* Unified marketplace agents endpoint with query string controls
* Query parameters:
* - category: string (filter by specific category - if undefined, no category filter applied)
* - search: string (search term for name/description)
* - limit: number (page size, default 6)
* - cursor: base64 string (for cursor-based pagination)
* - promoted: 0|1 (filter promoted agents, 1=promoted only, 0=exclude promoted)
* - requiredPermission: number (permission level required to access agents, default 1)
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getAgentsByCategory = async (req, res) => {
const getMarketplaceAgents = async (req, res) => {
try {
const { category } = req.params;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
const filter = {
const {
category,
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
};
search,
limit = DEFAULT_PAGE_SIZE,
cursor,
promoted,
requiredPermission = 1,
} = req.query;
const result = await paginateAgents(filter, page, limit);
const parsedLimit = parseInt(limit) || DEFAULT_PAGE_SIZE;
const parsedRequiredPermission = parseInt(requiredPermission) || 1;
// Get category description from database
const categoryDoc = await findCategoryByValue(category);
const categoryInfo = {
name: category,
description: categoryDoc?.description || '',
total: result.pagination.total,
};
// Base filter
const filter = {};
res.status(200).json({
...result,
category: categoryInfo,
});
} catch (error) {
logger.error(
`[/Agents/Marketplace] Error fetching agents for category ${req.params.category}:`,
error,
);
res.status(500).json({
error: 'Failed to fetch agents by category',
userMessage: `Unable to load agents for this category. Please try a different category.`,
suggestion: 'Try selecting a different category or refresh the page',
});
}
};
/**
* Search agents with filters
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const searchAgents = async (req, res) => {
try {
const { q, category } = req.query;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || DEFAULT_PAGE_SIZE;
if (!q || q.trim() === '') {
return res.status(400).json({
error: 'Search query is required',
userMessage: 'Please enter a search term to find agents',
suggestion: 'Enter a search term to find agents by name or description',
});
}
// Build search filter
const filter = {
projectIds: { $exists: true, $ne: [] }, // Only get shared agents
$or: [
{ name: { $regex: q, $options: 'i' } }, // Case-insensitive name search
{ description: { $regex: q, $options: 'i' } },
],
};
// Add category filter if provided
if (category && category !== 'all') {
// Handle category filter - only apply if category is defined
if (category !== undefined && category.trim() !== '') {
filter.category = category;
}
const result = await paginateAgents(filter, page, limit);
// Handle promoted filter - only from query param
if (promoted === '1') {
filter.is_promoted = true;
} else if (promoted === '0') {
filter.is_promoted = { $ne: true };
}
res.status(200).json({
...result,
query: q,
});
// Handle search filter
if (search && search.trim() !== '') {
filter.$or = [
{ name: { $regex: search.trim(), $options: 'i' } },
{ description: { $regex: search.trim(), $options: 'i' } },
];
}
// Use ACL-aware function for proper permission handling
const result = await getAgentsPagedByAccess(
req.user.id,
parsedRequiredPermission, // Required permission as number
filter,
parsedLimit,
cursor,
);
// Add category info if category was specified
if (category !== undefined && category.trim() !== '') {
const categoryDoc = await findCategoryByValue(category);
result.category = {
name: category,
description: categoryDoc?.description || '',
total: result.pagination?.total || result.data?.length || 0,
};
}
// Add search info if search was performed
if (search && search.trim() !== '') {
result.query = search.trim();
}
res.status(200).json(result);
} catch (error) {
logger.error('[/Agents/Marketplace] Error searching agents:', error);
logger.error('[/Agents/Marketplace] Error fetching marketplace agents:', error);
res.status(500).json({
error: 'Failed to search agents',
userMessage: 'Search is temporarily unavailable. Please try again.',
suggestion: 'Try a different search term or check your network connection',
error: 'Failed to fetch marketplace agents',
userMessage: 'Unable to load agents. Please try refreshing the page.',
suggestion: 'Try refreshing the page or check your network connection',
});
}
};
@ -187,7 +144,6 @@ const getAgentCategories = async (_req, res) => {
// Get count of promoted agents for Top Picks
const promotedCount = await Agent.countDocuments({
projectIds: { $exists: true, $ne: [] },
is_promoted: true,
});
@ -233,23 +189,7 @@ const getAgentCategories = async (_req, res) => {
}
};
/**
* Get all agents with pagination (for "all" category)
* This is an alias for getPromotedAgents with showAll=true for backwards compatibility
*
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
const getAllAgents = async (req, res) => {
// Set showAll parameter and delegate to getPromotedAgents
req.query.showAll = 'true';
return getPromotedAgents(req, res);
};
module.exports = {
getPromotedAgents,
getAgentsByCategory,
searchAgents,
getMarketplaceAgents,
getAgentCategories,
getAllAgents,
};