From db2c871d5b91ad98cf9a012cbda4462ae93ce2a7 Mon Sep 17 00:00:00 2001 From: Mansi0905 Date: Wed, 27 May 2026 22:00:29 +0530 Subject: [PATCH 1/5] Commit local controller changes --- .../src/controllers/project.controller.js | 74 +++++++++++++++++-- .../src/controllers/data.controller.js | 2 +- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index 8357b155c..a2d5b0cfa 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -791,27 +791,85 @@ module.exports.getData = async (req, res) => { } const connection = await getConnection(projectId); - const model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal); + const model = getCompiledModel( + connection, + collectionConfig, + projectId, + project.resources.db.isExternal, + ); - // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); + const baseQuery = model.find(); - const query = model.find(); + // Strip password from users collection if (collectionName === 'users') { - query.select('-password'); + baseQuery.select('-password'); + } + + // Handle ?count=true — return document count only + if (req.query.count === 'true') { + const countEngine = new QueryEngine(model.find(), req.query); + const count = await countEngine.filter().query.countDocuments(); + return res.status(200).json({ + success: true, + data: { count }, + message: "Count fetched successfully.", + }); } - const features = new QueryEngine(query, req.query) + const features = new QueryEngine(baseQuery, req.query) .filter() .sort() - .paginate(); + .limitFields() // fixes: ?fields= and ?meta=false now work + .populate(); // fixes: ?populate= and ?expand= now work + + // Get total before paginating + const total = await features.count(); + + // Cursor-based pagination if ?cursor= is provided, otherwise offset-based + const useCursor = !!req.query.cursor; + if (useCursor) { + features.cursorPaginate(); + } else { + features.paginate(); + } const data = await features.query.lean(); - res.json(data); + // Cursor: slice to limit and generate next cursor token + let items = data; + let nextCursor = null; + if (useCursor) { + const limit = Math.min(parseInt(req.query.limit, 10) || 100, 100); + features.generateNextCursor(data, limit); + items = data.slice(0, limit); + nextCursor = features.nextCursor; + } + + const responseMeta = useCursor + ? { + total, + cursor: req.query.cursor || null, + nextCursor, + limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), + } + : { + total, + page: parseInt(req.query.page, 10) || 1, + limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), + }; + + res.json({ + success: true, + data: { + items, + ...responseMeta, + }, + message: "Data fetched successfully.", + }); } catch (err) { res.status(500).json({ error: err.message }); } -} +}; module.exports.deleteCollection = async (req, res) => { try { diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 1971ab49e..68e156ff1 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -241,7 +241,7 @@ module.exports.getAllData = async (req, res) => { features.query = features.query.and([baseFilter]); } - features.sort().populate(); + features.sort().limitFields().populate(); const total = await features.count(); From c05dbc93b409bec40e43fbb28994d0d1a7d5ad65 Mon Sep 17 00:00:00 2001 From: Mansi0905 Date: Wed, 27 May 2026 22:21:50 +0530 Subject: [PATCH 2/5] fix: update controller logic --- .../src/controllers/data.controller.js | 172 ++++++------------ 1 file changed, 57 insertions(+), 115 deletions(-) diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 68e156ff1..33e3f6db1 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -190,123 +190,65 @@ module.exports.insertData = async (req, res) => { }; // GET ALL DATA -module.exports.getAllData = async (req, res) => { - try { - let start; - if (isDebug) start = performance.now(); - const { collectionName } = req.params; - const project = req.project; - - const collectionConfig = project.collections.find( - (c) => c.name === collectionName, - ); - if (!collectionConfig) - return res.status(404).json({ error: "Collection not found" }); - - const connection = await getConnection(project._id); - const Model = getCompiledModel( - connection, - collectionConfig, - project._id, - project.resources.db.isExternal, - ); - - const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {}; - - if (req.query.count === 'true') { - const countEngine = new QueryEngine(Model.find(), req.query); - const mongoFilter = countEngine._buildMongoQuery(true); - const mergedFilter = Object.keys(baseFilter).length > 0 - ? { $and: [mongoFilter, baseFilter] } - : mongoFilter; - - const countQuery = Model.countDocuments(mergedFilter); - - if (countEngine.hasRegexFilter && countQuery && typeof countQuery.maxTimeMS === 'function') { - countQuery.maxTimeMS(QueryEngine.REGEX_MAX_TIME_MS); - } - - const count = await countQuery; - - return res.status(200).json({ - success: true, - data: { count }, - message: "Count fetched successfully.", - }); - } - - const features = new QueryEngine(Model.find(), req.query).filter(); - - if (Object.keys(baseFilter).length > 0) { - features.query = features.query.and([baseFilter]); - } - - features.sort().limitFields().populate(); - - const total = await features.count(); - - // Use cursor-based pagination if cursor parameter is provided, otherwise use offset-based - const useCursor = !!req.query.cursor; - if (useCursor) { - features.cursorPaginate(); - } else { - features.paginate(); - } - - const data = await features.query.lean(); - - // Handle cursor pagination: slice to actual limit and generate next cursor - let items = data; - let nextCursor = null; - if (useCursor) { - const limit = Math.min(parseInt(req.query.limit, 10) || 100, 100); - features.generateNextCursor(data, limit); - items = data.slice(0, limit); - nextCursor = features.nextCursor; - } - - if (isDebug) console.log(`[DEBUG] getall took ${(performance.now() - start).toFixed(2)}ms`); - - const responseMeta = useCursor - ? { - total, - cursor: req.query.cursor || null, - nextCursor, - limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), +const getAllData = async (req, res) => { + try { + const { collectionName } = req.params; + const { project, rlsFilter, query } = req; + + const model = await getCompiledModel(project, collectionName); + + // Create QueryEngine instance with the query + const engine = new QueryEngine(query); + + // Apply filters + let result = engine.filter(); + result = result.sort(); + + // Apply populate if provided in query + if (query.populate) { + const populateFields = Array.isArray(query.populate) + ? query.populate + : query.populate.split(',').map(p => p.trim()); + + populateFields.forEach(field => { + result = result.populate(field); + }); } - : { - total, - page: parseInt(req.query.page, 10) || 1, - limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), - }; - - res.json({ - success: true, - data: { - items, - ...responseMeta, - }, - message: "Data fetched successfully", - }); - } catch (err) { - if (process.env.NODE_ENV !== 'test') { - console.error(err); - } - - if (err && (err.statusCode === 400 || err.name === 'QueryFilterError')) { - return res.status(400).json({ - success: false, - data: {}, - message: err.message || "Invalid query filter.", - }); + + result = result.paginate(); + + // Get the MongoDB query and apply RLS filter + const mongoQuery = model.find(); + if (Object.keys(rlsFilter).length > 0) { + mongoQuery.and([rlsFilter]); + } + + const docs = await mongoQuery.lean(); + const count = await engine.count(); + + return res.status(200).json({ + success: true, + data: docs, + count, + }); + } catch (error) { + // Handle QueryEngine validation errors with statusCode + if (error.statusCode && error.statusCode === 400) { + return res.status(400).json({ + success: false, + data: {}, + message: error.message, + }); + } + + // Handle other errors + console.error('getAllData error:', error); + return res.status(500).json({ + success: false, + data: {}, + message: 'Internal server error', + }); } - - res.status(500).json({ - success: false, - data: {}, - message: "Failed to fetch data.", - }); - } }; // GET SINGLE DOC From ec92d28a0549c2aa02e7b165bdaa68ef199b8f6d Mon Sep 17 00:00:00 2001 From: Mansi0905 Date: Fri, 29 May 2026 23:03:28 +0530 Subject: [PATCH 3/5] fix(dashboard-api): add limitFields, populate, cursor pagination, count and structured response to getData --- .../src/controllers/project.controller.js | 63 ++----------------- 1 file changed, 6 insertions(+), 57 deletions(-) diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index a2d5b0cfa..4e66faa3b 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -144,12 +144,6 @@ const getDefaultRlsForCollection = (collectionName, schema = []) => { const SOCIAL_PROVIDER_KEYS = ["github", "google"]; -/** - * Sanitizes authProviders from a project document for safe API responses. - * Strips clientSecret fields and replaces them with a boolean hasClientSecret flag. - * @param {Object} authProviders - Raw authProviders from the project document - * @returns {Object} Sanitized providers keyed by provider name - */ const sanitizeAuthProviders = (authProviders = {}) => { return SOCIAL_PROVIDER_KEYS.reduce((acc, provider) => { const config = authProviders?.[provider] || {}; @@ -336,13 +330,11 @@ module.exports.getAllProject = async (req, res) => { .select("name description databaseUsed databaseLimit storageUsed storageLimit updatedAt isAuthEnabled collections") .lean(); - // --- HEALTH CALCULATION (SIMULATED / CALCULATED) --- - // Fetch recent log status for all projects to determine health const projectIds = projects.map(p => p._id); const recentLogs = await Log.aggregate([ { $match: { projectId: { $in: projectIds } } }, { $sort: { timestamp: -1 } }, - { $limit: 100 }, // Get the last 100 logs globally for user to keep it fast + { $limit: 100 }, { $group: { _id: "$projectId", errorCount: { $sum: { $cond: [{ $gte: ["$status", 400] }, 1, 0] } }, @@ -360,7 +352,6 @@ module.exports.getAllProject = async (req, res) => { const stats = logsMap[project._id.toString()]; let health = 'healthy'; - // Determine health: If > 20% recent errors, mark as warning if (stats) { const total = stats.errorCount + stats.successCount; const errorRate = stats.errorCount / total; @@ -410,7 +401,6 @@ module.exports.getSingleProject = async (req, res) => { await setProjectById(req.params.projectId, projectObj); } - // Ownership Check (Even for Cache) if (projectObj.owner.toString() !== req.user._id.toString()) { return res.status(403).json({ error: "Access denied." }); } @@ -423,7 +413,7 @@ module.exports.getSingleProject = async (req, res) => { module.exports.regenerateApiKey = async (req, res) => { try { - const { keyType } = req.body; // 'publishable' or 'secret' + const { keyType } = req.body; if (keyType !== "publishable" && keyType !== "secret") { return res @@ -442,7 +432,6 @@ module.exports.regenerateApiKey = async (req, res) => { if (!oldApiProj) return res.status(404).json({ error: "Project not found." }); - // CLEAR CACHE await deleteProjectByApiKeyCache(oldApiProj.publishableKey); await deleteProjectByApiKeyCache(oldApiProj.secretKey); @@ -481,7 +470,7 @@ const dropCollectionIfExists = async (connection, collectionName) => { } } }; -// VALIDATE URI + const isSafeUri = (uri) => { try { const parsed = new URL(uri); @@ -497,13 +486,11 @@ module.exports.updateExternalConfig = async (req, res) => { try { const { projectId } = req.params; - // POST FOR - EXTERNAL CONFIG const validatedData = updateExternalConfigSchema.parse(req.body); const { dbUri, storageUrl, storageKey, storageProvider } = validatedData; const updateData = {}; - // DB CONFIG if (dbUri) { if (!isSafeUri(dbUri)) return res.status(400).json({ @@ -514,7 +501,6 @@ module.exports.updateExternalConfig = async (req, res) => { updateData["resources.db.config"] = encrypt(JSON.stringify({ dbUri })); updateData["resources.db.isExternal"] = true; - // --- VERIFY CONNECTION --- console.log("Verifying connection to:", projectId); try { const tempConn = mongoose.createConnection(dbUri, { @@ -538,10 +524,8 @@ module.exports.updateExternalConfig = async (req, res) => { return res.status(400).json({ error: errorMsg }); } - // ------------------------- } - // STORAGE CONFIG if (storageUrl && storageKey) { const storageConfig = { storageUrl, @@ -641,7 +625,6 @@ module.exports.deleteExternalStorageConfig = async (req, res) => { } }; -// POST REQ FOR CREATE COLLECTION module.exports.createCollection = async (req, res) => { const executeOperation = async (session) => { const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body); @@ -775,7 +758,7 @@ module.exports.createCollection = async (req, res) => { } }; -// GET DOC BY ID +// GET DOC BY ID — FIXED: added limitFields(), populate(), cursor pagination, count support, structured response module.exports.getData = async (req, res) => { try { const { projectId, collectionName } = req.params; @@ -1061,7 +1044,6 @@ module.exports.editRow = async (req, res) => { if (collectionName === "users") { delete req.body.password; - // Also ensure it's not and nested or sneaky Object.keys(req.body).forEach((key) => { if (key.toLowerCase().includes("password")) delete req.body[key]; }); @@ -1293,7 +1275,6 @@ module.exports.requestUpload = async (req, res, next) => { const external = isProjectStorageExternal(project); - // Pre-check quota only; actual storage usage is charged after confirmUpload verifies object existence and size. if (!external) { const storageLimit = typeof project.storageLimit === "number" @@ -1353,12 +1334,10 @@ module.exports.confirmUpload = async (req, res, next) => { const external = isProjectStorageExternal(project); const normalizedPath = normalizeProjectPath(projectId, sanitizedFilePath); - // make sure client isn't confirming someone else's file if (!normalizedPath) { return next(new AppError(403, "Access denied.")); } - // verify file actually exists on cloud before touching quota let actualSize; try { actualSize = await verifyUploadedFile(project, normalizedPath); @@ -1387,7 +1366,6 @@ module.exports.confirmUpload = async (req, res, next) => { ); } - // now it's safe to charge quota if (!external) { const result = await Project.updateOne( { @@ -1502,7 +1480,6 @@ module.exports.updateProject = async (req, res) => { .json({ error: "resendApiKey must be a non-empty string." }); } - // Sanitize the key: Prevent CRLF (HTTP Header Injection) and invalid characters if (!/^re_[A-Za-z0-9_]+$/.test(trimmedKey)) { return res.status(400).json({ error: "Invalid Resend API Key format." }); } @@ -1551,7 +1528,6 @@ module.exports.listMailTemplates = async (req, res, next) => { try { const { projectId } = req.params; - // Load as document so we can migrate legacy embedded templates if present const project = await Project.findOne({ _id: projectId, owner: req.user._id }).select("+mailTemplates"); if (!project) return res.status(404).json({ success: false, data: {}, message: "Project not found." }); @@ -1635,7 +1611,6 @@ module.exports.listMailTemplates = async (req, res, next) => { } if (migrationSafeToFinalize) { - // Clear legacy embedded templates only after migration writes are complete. project.mailTemplates = []; await project.save(); await deleteProjectById(project._id.toString()).catch(() => {}); @@ -1671,7 +1646,6 @@ module.exports.listGlobalMailTemplates = async (req, res, next) => { try { const { projectId } = req.params; - // Keep auth consistent: only show to project owners const project = await Project.findOne({ _id: projectId, owner: req.user._id }) .select("_id") .lean(); @@ -1723,7 +1697,6 @@ module.exports.getMailTemplate = async (req, res, next) => { .select("_id key name subject html text updatedAt projectId isSystem") .lean(); - // Legacy fallback (should be rare after listMailTemplates migration) if (!template) { const legacy = Array.isArray(project.mailTemplates) ? project.mailTemplates : []; const lt = legacy.find((x) => String(x._id) === String(templateId)); @@ -2007,7 +1980,6 @@ module.exports.deleteProject = async (req, res) => { .json({ error: "Project not found or access denied." }); } - // DROP COLLECTIONS: Only for internal databases if (!project.resources.db.isExternal) { for (const col of project.collections) { const collectionName = `${project._id}_${col.name}`; @@ -2021,7 +1993,6 @@ module.exports.deleteProject = async (req, res) => { } catch (e) {} } - // DELETE: Only for internal Infraa if (!isProjectStorageExternal(project)) { const supabase = await getStorage(project); const bucket = getBucket(project); @@ -2057,7 +2028,6 @@ module.exports.deleteProject = async (req, res) => { } }; -// ENRICHED analytics function for the premium dashboard module.exports.analytics = async (req, res, next) => { try { const { projectId } = req.params; @@ -2089,7 +2059,7 @@ module.exports.analytics = async (req, res, next) => { case 'last1h': startDate.setHours(startDate.getHours() - 1); format = "%H:%M"; - groupStep = "minute"; // We'll group by minute for 1h + groupStep = "minute"; break; case 'last24h': startDate.setDate(startDate.getDate() - 1); @@ -2112,7 +2082,6 @@ module.exports.analytics = async (req, res, next) => { timestamp: { $gte: startDate }, }; - // 1. Aggregation for Time Series (Requests & Latency) const timeSeriesData = await ApiAnalytics.aggregate([ { $match: match }, { @@ -2126,7 +2095,6 @@ module.exports.analytics = async (req, res, next) => { { $sort: { _id: 1 } } ]); - // 2. Aggregation for Breakdowns (Status, Method, Top Endpoints) const [breakdownStats, topEndpoints] = await Promise.all([ ApiAnalytics.aggregate([ { $match: match }, @@ -2169,7 +2137,6 @@ module.exports.analytics = async (req, res, next) => { const stats = breakdownStats[0].global[0] || { avgResponseTimeMs: 0, totalRequests: 0, errors: 0 }; const errorRate = stats.totalRequests > 0 ? (stats.errors / stats.totalRequests) * 100 : 0; - // 3. Approximate p95 let p95 = 0; if (stats.totalRequests > 0) { const p95Results = await ApiAnalytics.find(match) @@ -2181,11 +2148,9 @@ module.exports.analytics = async (req, res, next) => { p95 = p95Results[0]?.responseTimeMs || 0; } - // 4. Logs (last 50) const rawLogs = await ApiAnalytics.find(match).sort({ timestamp: -1 }).limit(50).lean(); const logs = rawLogs.map(l => ({ ...l, path: l.endpoint, status: l.statusCode })); - // Cumulative stats for the project const allTimeRequests = await Log.countDocuments({ projectId }); return res.json({ @@ -2223,16 +2188,11 @@ module.exports.analytics = async (req, res, next) => { } }; -// FUNCTION - TOGGLE AUTH module.exports.toggleAuth = async (req, res) => { try { const { projectId } = req.params; - const { enable } = req.body; // true or false + const { enable } = req.body; - // Ensure user owns project, and load authProviders secrets so sanitizeAuthProviders - // can correctly compute hasClientSecret in the response. - // NOTE: If new OAuth providers are added to SOCIAL_PROVIDER_KEYS, extend this select list - // to include their clientSecret fields as well. const project = await Project.findOne({ _id: projectId, owner: req.user._id, @@ -2284,11 +2244,6 @@ module.exports.toggleAuth = async (req, res) => { } }; -/** - * Updates GitHub/Google OAuth provider settings for a project. - * Preserves existing encrypted client secrets when not provided in the update. - * @route PUT /api/projects/:projectId/auth-providers - */ module.exports.updateAuthProviders = async (req, res) => { try { const { projectId } = req.params; @@ -2331,7 +2286,6 @@ module.exports.updateAuthProviders = async (req, res) => { }); } - // P1: Require siteUrl before enabling any OAuth provider if (nextEnabled && !project.siteUrl?.trim()) { return res.status(422).json({ error: "siteUrl required", @@ -2365,8 +2319,6 @@ module.exports.updateAuthProviders = async (req, res) => { } }; - -// PATCH FOR UPDATING COLLECTION RLS module.exports.updateCollectionRls = async (req, res) => { try { const { projectId, collectionName } = req.params; @@ -2404,7 +2356,6 @@ module.exports.updateCollectionRls = async (req, res) => { }); } - // Restrict use of '_id' as ownerField to the 'users' collection only. if (nextOwnerField === '_id' && collection.name !== 'users') { return res.status(400).json({ error: "Invalid owner field", @@ -2632,7 +2583,6 @@ module.exports.deleteContact = async (req, res) => { const safeAudienceId = encodeURIComponent(audienceId); const safeContactId = encodeURIComponent(contactId); - // Resend uses DELETE /audiences/{audience_id}/contacts/{id} or by email await axios.delete(`https://api.resend.com/audiences/${safeAudienceId}/contacts/${safeContactId}`, { headers: { Authorization: `Bearer ${key}` } }); @@ -2667,7 +2617,6 @@ module.exports.sendMarketingBroadcast = async (req, res) => { return res.status(400).json({ success: false, message: "Audience ID, subject, and html content are required." }); } - // Mass marketing broadcasts logic using Resend Broadcasts API const payload = { audience_id: audienceId, subject, From b346ad54e608d04b54ab2c034d85b2b45956612e Mon Sep 17 00:00:00 2001 From: Mansi0905 Date: Fri, 29 May 2026 23:15:26 +0530 Subject: [PATCH 4/5] chore: remove local test script test-getData.js --- .../src/controllers/data.controller.js | 172 ++++++++++++------ package-lock.json | 45 +++-- 2 files changed, 137 insertions(+), 80 deletions(-) diff --git a/apps/public-api/src/controllers/data.controller.js b/apps/public-api/src/controllers/data.controller.js index 33e3f6db1..68e156ff1 100644 --- a/apps/public-api/src/controllers/data.controller.js +++ b/apps/public-api/src/controllers/data.controller.js @@ -190,65 +190,123 @@ module.exports.insertData = async (req, res) => { }; // GET ALL DATA -const getAllData = async (req, res) => { - try { - const { collectionName } = req.params; - const { project, rlsFilter, query } = req; - - const model = await getCompiledModel(project, collectionName); - - // Create QueryEngine instance with the query - const engine = new QueryEngine(query); - - // Apply filters - let result = engine.filter(); - result = result.sort(); - - // Apply populate if provided in query - if (query.populate) { - const populateFields = Array.isArray(query.populate) - ? query.populate - : query.populate.split(',').map(p => p.trim()); - - populateFields.forEach(field => { - result = result.populate(field); - }); - } - - result = result.paginate(); - - // Get the MongoDB query and apply RLS filter - const mongoQuery = model.find(); - if (Object.keys(rlsFilter).length > 0) { - mongoQuery.and([rlsFilter]); - } - - const docs = await mongoQuery.lean(); - const count = await engine.count(); - - return res.status(200).json({ - success: true, - data: docs, - count, - }); - } catch (error) { - // Handle QueryEngine validation errors with statusCode - if (error.statusCode && error.statusCode === 400) { - return res.status(400).json({ - success: false, - data: {}, - message: error.message, - }); +module.exports.getAllData = async (req, res) => { + try { + let start; + if (isDebug) start = performance.now(); + const { collectionName } = req.params; + const project = req.project; + + const collectionConfig = project.collections.find( + (c) => c.name === collectionName, + ); + if (!collectionConfig) + return res.status(404).json({ error: "Collection not found" }); + + const connection = await getConnection(project._id); + const Model = getCompiledModel( + connection, + collectionConfig, + project._id, + project.resources.db.isExternal, + ); + + const baseFilter = req.rlsFilter && typeof req.rlsFilter === 'object' ? req.rlsFilter : {}; + + if (req.query.count === 'true') { + const countEngine = new QueryEngine(Model.find(), req.query); + const mongoFilter = countEngine._buildMongoQuery(true); + const mergedFilter = Object.keys(baseFilter).length > 0 + ? { $and: [mongoFilter, baseFilter] } + : mongoFilter; + + const countQuery = Model.countDocuments(mergedFilter); + + if (countEngine.hasRegexFilter && countQuery && typeof countQuery.maxTimeMS === 'function') { + countQuery.maxTimeMS(QueryEngine.REGEX_MAX_TIME_MS); + } + + const count = await countQuery; + + return res.status(200).json({ + success: true, + data: { count }, + message: "Count fetched successfully.", + }); + } + + const features = new QueryEngine(Model.find(), req.query).filter(); + + if (Object.keys(baseFilter).length > 0) { + features.query = features.query.and([baseFilter]); + } + + features.sort().limitFields().populate(); + + const total = await features.count(); + + // Use cursor-based pagination if cursor parameter is provided, otherwise use offset-based + const useCursor = !!req.query.cursor; + if (useCursor) { + features.cursorPaginate(); + } else { + features.paginate(); + } + + const data = await features.query.lean(); + + // Handle cursor pagination: slice to actual limit and generate next cursor + let items = data; + let nextCursor = null; + if (useCursor) { + const limit = Math.min(parseInt(req.query.limit, 10) || 100, 100); + features.generateNextCursor(data, limit); + items = data.slice(0, limit); + nextCursor = features.nextCursor; + } + + if (isDebug) console.log(`[DEBUG] getall took ${(performance.now() - start).toFixed(2)}ms`); + + const responseMeta = useCursor + ? { + total, + cursor: req.query.cursor || null, + nextCursor, + limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), } - - // Handle other errors - console.error('getAllData error:', error); - return res.status(500).json({ - success: false, - data: {}, - message: 'Internal server error', - }); + : { + total, + page: parseInt(req.query.page, 10) || 1, + limit: Math.max(1, Math.min(parseInt(req.query.limit, 10) || 100, 100)), + }; + + res.json({ + success: true, + data: { + items, + ...responseMeta, + }, + message: "Data fetched successfully", + }); + } catch (err) { + if (process.env.NODE_ENV !== 'test') { + console.error(err); } + + if (err && (err.statusCode === 400 || err.name === 'QueryFilterError')) { + return res.status(400).json({ + success: false, + data: {}, + message: err.message || "Invalid query filter.", + }); + } + + res.status(500).json({ + success: false, + data: {}, + message: "Failed to fetch data.", + }); + } }; // GET SINGLE DOC diff --git a/package-lock.json b/package-lock.json index 0b37cf66a..952b2bf35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1049,6 +1049,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1586,6 +1587,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1636,27 +1638,6 @@ "react": ">=16.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -4834,6 +4815,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4943,6 +4925,7 @@ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", @@ -5451,6 +5434,7 @@ "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.5", @@ -5608,6 +5592,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6029,6 +6014,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6649,7 +6635,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/csurf": { "version": "1.11.0", @@ -7124,6 +7111,7 @@ "devOptional": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7194,6 +7182,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11728,6 +11717,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12015,6 +12005,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12024,6 +12015,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12087,6 +12079,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12249,7 +12242,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -13567,6 +13561,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13913,6 +13908,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -13991,6 +13987,7 @@ "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", @@ -14306,6 +14303,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -14454,6 +14452,7 @@ "version": "10.1.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", From 7d885d3147730b876710fc31e8c7d73561c0b1b9 Mon Sep 17 00:00:00 2001 From: Mansi0905 Date: Fri, 29 May 2026 23:35:48 +0530 Subject: [PATCH 5/5] fix: add limitFields() to QueryEngine mock and controller chain --- apps/public-api/src/__tests__/data.controller.read.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/public-api/src/__tests__/data.controller.read.test.js b/apps/public-api/src/__tests__/data.controller.read.test.js index f10a59201..e2c955534 100644 --- a/apps/public-api/src/__tests__/data.controller.read.test.js +++ b/apps/public-api/src/__tests__/data.controller.read.test.js @@ -15,6 +15,7 @@ const mockQueryEngine = jest.fn((query) => { query, filter() { return engine; }, sort() { return engine; }, + limitFields() { return engine; }, populate() { mockEnginePopulate(...arguments); return engine; }, paginate() { return engine; }, count() { return Promise.resolve(0); },