- add cursor-based pagination to getListAgentsByAccess and update handler

- add index on updatedAt and _id in agent schema for improved query performance
This commit is contained in:
Atef Bellaaj 2025-06-12 17:17:25 +02:00 committed by Danny Avila
parent e97444a863
commit 25b97ba388
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
3 changed files with 81 additions and 34 deletions

View file

@ -438,29 +438,59 @@ const deleteAgent = async (searchParameter) => {
}; };
/** /**
* Get agents by accessible IDs (combines ownership and ACL permissions). * Get agents by accessible IDs with optional cursor-based pagination.
* @param {Object} params - The parameters for getting accessible agents. * @param {Object} params - The parameters for getting accessible agents.
* @param {string} params.userId - The user ID to get agents for.
* @param {Array} [params.accessibleIds] - Array of agent ObjectIds the user has ACL access to. * @param {Array} [params.accessibleIds] - Array of agent ObjectIds the user has ACL access to.
* @param {Object} [params.otherParams] - Additional query parameters. * @param {Object} [params.otherParams] - Additional query parameters (including author filter).
* @param {number} [params.limit] - Number of agents to return (max 100). If not provided, returns all agents.
* @param {string} [params.after] - Cursor for pagination - get agents after this cursor. // base64 encoded JSON string with updatedAt and _id.
* @returns {Promise<Object>} A promise that resolves to an object containing the agents data and pagination info. * @returns {Promise<Object>} A promise that resolves to an object containing the agents data and pagination info.
*/ */
const getListAgentsByAccess = async ({ userId, accessibleIds = [], otherParams = {} }) => { const getListAgentsByAccess = async ({
// Build query for owned agents and ACL accessible agents accessibleIds = [],
const queries = [ otherParams = {},
// Agents where user is author (owned) limit = null,
{ author: userId, ...otherParams }, after = null
]; }) => {
const isPaginated = limit !== null && limit !== undefined;
const normalizedLimit = isPaginated ? Math.min(Math.max(1, parseInt(limit) || 20), 100) : null;
// Build base query combining ACL accessible agents with other filters
const baseQuery = { ...otherParams };
// Add ACL accessible agents if any
if (accessibleIds.length > 0) { if (accessibleIds.length > 0) {
queries.push({ _id: { $in: accessibleIds }, ...otherParams }); baseQuery._id = { $in: accessibleIds };
} }
const query = queries.length > 1 ? { $or: queries } : queries[0]; // Add cursor condition
if (after) {
try {
const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8'));
const { updatedAt, _id } = cursor;
const agents = ( const cursorCondition = {
await Agent.find(query, { $or: [
{ updatedAt: { $lt: new Date(updatedAt) } },
{ updatedAt: new Date(updatedAt), _id: { $gt: mongoose.Types.ObjectId(_id) } }
]
};
// Merge cursor condition with base query
if (Object.keys(baseQuery).length > 0) {
baseQuery.$and = [{ ...baseQuery }, cursorCondition];
// Remove the original conditions from baseQuery to avoid duplication
Object.keys(baseQuery).forEach(key => {
if (key !== '$and') delete baseQuery[key];
});
} else {
Object.assign(baseQuery, cursorCondition);
}
} catch (error) {
logger.warn('Invalid cursor:', error.message);
}
}
let query = Agent.find(baseQuery, {
id: 1, id: 1,
_id: 1, _id: 1,
name: 1, name: 1,
@ -468,26 +498,41 @@ const getListAgentsByAccess = async ({ userId, accessibleIds = [], otherParams =
author: 1, author: 1,
projectIds: 1, projectIds: 1,
description: 1, description: 1,
}).lean() updatedAt: 1,
).map((agent) => { }).sort({ updatedAt: -1, _id: 1 });
if (agent.author?.toString() !== userId) {
delete agent.author; // Only apply limit if pagination is requested
if (isPaginated) {
query = query.limit(normalizedLimit + 1);
} }
const agents = await query.lean();
const hasMore = isPaginated ? agents.length > normalizedLimit : false;
const data = (isPaginated ? agents.slice(0, normalizedLimit) : agents).map((agent) => {
if (agent.author) { if (agent.author) {
agent.author = agent.author.toString(); agent.author = agent.author.toString();
} }
return agent; return agent;
}); });
const hasMore = agents.length > 0; // Generate next cursor only if paginated
const firstId = agents.length > 0 ? agents[0].id : null; let nextCursor = null;
const lastId = agents.length > 0 ? agents[agents.length - 1].id : null; if (isPaginated && hasMore && data.length > 0) {
const lastAgent = agents[normalizedLimit - 1];
nextCursor = Buffer.from(JSON.stringify({
updatedAt: lastAgent.updatedAt.toISOString(),
_id: lastAgent._id.toString()
})).toString('base64');
}
return { return {
data: agents, object: 'list',
data,
first_id: data.length > 0 ? data[0].id : null,
last_id: data.length > 0 ? data[data.length - 1].id : null,
has_more: hasMore, has_more: hasMore,
first_id: firstId, after: nextCursor
last_id: lastId,
}; };
}; };

View file

@ -395,9 +395,9 @@ const getListAgentsHandler = async (req, res) => {
// Use the new ACL-aware function // Use the new ACL-aware function
const data = await getListAgentsByAccess({ const data = await getListAgentsByAccess({
userId,
accessibleIds, accessibleIds,
otherParams: {}, // Can add query params here if needed otherParams: {}, // Can add query params here if needed
}); });
return res.json(data); return res.json(data);

View file

@ -98,4 +98,6 @@ const agentSchema = new Schema<IAgent>(
}, },
); );
agentSchema.index({ updatedAt: -1, _id: 1 });
export default agentSchema; export default agentSchema;