Skip to content

Commit 48ff163

Browse files
authored
Fix/dashboard get data query engine (#230)
* Commit local controller changes * fix: update controller logic * Export getAllData function to fix failing tests * Fix: Correct getAllData function to properly initialize getCompiledModel with required parameters - Add collection config lookup before calling getCompiledModel - Pass all 4 required parameters: connection, collectionConfig, projectId, isExternal - Handle collection not found error properly - Fixes TypeError: getAllData is not a function in tests * Fix test mocks to properly support getAllData function calls - Update getCompiledModel mock to accept connection, collectionConfig, projectId, isExternal params - Make mock return a model object with find() method immediately (synchronously) - Ensure QueryEngine is properly constructed with query parameter * Refactor getAllData to properly handle QueryEngine and fix test compatibility - Move QueryEngine operations into try-catch to handle validation errors with statusCode - Ensure engine variable is accessible after filter/sort/populate operations - Fix variable scope issue where engine was not accessible for count() call * fix(dashboard-api): add limitFields, populate, cursor pagination, count and structured response to getData * chore: remove local test script test-getData.js * fix: add limitFields() to QueryEngine mock and controller chain * fix: normalize getData errors and fix per-project log aggregation * fix: address coderabbit and reviewer comments - restore jsdocs, fix password bypass, reorder populate before limitFields * fix: normalize getData error responses to standard contract * fix: normalize getData error responses to standard contract * fix: reorder populate before limitFields, use topN for log aggregation, remove duplicate comment * chore: resolve package-lock.json merge conflict * fix: fix syntax error in log aggregation pipeline * fix: remove stray brace causing syntax error in log aggregation * ci: trigger rerun * fix: fix unclosed template literal and missing closing brace in getData
1 parent 53c9e9a commit 48ff163

3 files changed

Lines changed: 41 additions & 28 deletions

File tree

apps/dashboard-api/src/controllers/project.controller.js

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -332,16 +332,29 @@ module.exports.getAllProject = async (req, res) => {
332332

333333
const projectIds = projects.map(p => p._id);
334334
const recentLogs = await Log.aggregate([
335-
{ $match: { projectId: { $in: projectIds } } },
336-
{ $sort: { timestamp: -1 } },
337-
{ $limit: 100 },
338-
{ $group: {
339-
_id: "$projectId",
340-
errorCount: { $sum: { $cond: [{ $gte: ["$status", 400] }, 1, 0] } },
341-
successCount: { $sum: { $cond: [{ $lt: ["$status", 400] }, 1, 0] } }
335+
{ $match: { projectId: { $in: projectIds } } },
336+
{ $sort: { timestamp: -1 } },
337+
{
338+
$group: {
339+
_id: "$projectId",
340+
logs: { $topN: { n: 100, sortBy: { timestamp: -1 }, output: { status: "$status" } } }
341+
}
342+
},
343+
{
344+
$project: {
345+
errorCount: {
346+
$size: {
347+
$filter: { input: "$logs", as: "l", cond: { $gte: ["$$l.status", 400] } }
348+
}
349+
},
350+
successCount: {
351+
$size: {
352+
$filter: { input: "$logs", as: "l", cond: { $lt: ["$$l.status", 400] } }
342353
}
343354
}
344-
]);
355+
}
356+
}
357+
]);
345358

346359
const logsMap = recentLogs.reduce((acc, log) => {
347360
acc[log._id.toString()] = log;
@@ -395,8 +408,7 @@ module.exports.getSingleProject = async (req, res) => {
395408
"+resendApiKey.iv " +
396409
"+resendApiKey.tag",
397410
);
398-
if (!project)
399-
return res.status(404).json({ error: "Project not found." });
411+
if (!project) return res.status(404).json({ success: false, data: {}, message: "Project not found." });
400412
projectObj = project.toObject();
401413
await setProjectById(req.params.projectId, projectObj);
402414
}
@@ -763,14 +775,13 @@ module.exports.getData = async (req, res) => {
763775
try {
764776
const { projectId, collectionName } = req.params;
765777
const project = await Project.findOne({ _id: projectId, owner: req.user._id });
766-
if (!project) return res.status(404).json({ error: "Project not found." });
778+
if (!project) return res.status(404).json({ success: false, data: {}, message: "Project not found." });
779+
780+
767781

768782
const collectionConfig = project.collections.find(c => c.name === collectionName);
769783
if (!collectionConfig) {
770-
return res.status(404).json({
771-
error: "Collection not found",
772-
collection: collectionName
773-
});
784+
return res.status(404).json({ success: false, data: {}, message: `Collection ${collectionName} not found.` });
774785
}
775786

776787
const connection = await getConnection(projectId);
@@ -850,10 +861,11 @@ module.exports.getData = async (req, res) => {
850861
message: "Data fetched successfully.",
851862
});
852863
} catch (err) {
853-
res.status(500).json({ error: err.message });
864+
if (err?.statusCode === 400 || err?.name === 'QueryFilterError') {
865+
return res.status(400).json({ success: false, data: {}, message: err.message || "Invalid query filter." });
854866
}
855867
};
856-
868+
};
857869
module.exports.deleteCollection = async (req, res) => {
858870
try {
859871
const { projectId, collectionName } = req.params;
@@ -925,10 +937,8 @@ module.exports.insertData = async (req, res) => {
925937
(c) => c.name === collectionName,
926938
);
927939
if (!collectionConfig) {
928-
return res
929-
.status(404)
930-
.json({ error: "Collection configuration not found." });
931-
}
940+
return res.status(404).json({ success: false, data: {}, message: `Collection ${collectionName} not found.` });
941+
}
932942

933943
// Prevent manual injection of soft-delete fields
934944
delete incomingData.isDeleted;

apps/public-api/src/__tests__/data.controller.read.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jest.mock('@urbackend/common', () => ({
2727
sanitize: (v) => v,
2828
Project: {},
2929
getConnection: jest.fn().mockResolvedValue({}),
30-
getCompiledModel: jest.fn(() => ({
30+
getCompiledModel: jest.fn((connection, collectionConfig, projectId, isExternal) => ({
3131
find: (...args) => {
3232
mockFind(...args);
3333
return {

apps/public-api/src/controllers/data.controller.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,14 @@ module.exports.getAllData = async (req, res) => {
217217
const collectionConfig = project.collections.find(
218218
(c) => c.name === collectionName,
219219
);
220-
if (!collectionConfig)
221-
return res.status(404).json({ error: "Collection not found" });
220+
221+
if (!collectionConfig) {
222+
return res.status(404).json({
223+
success: false,
224+
data: {},
225+
message: "Collection not found",
226+
});
227+
}
222228

223229
const connection = await getConnection(project._id);
224230
const Model = getCompiledModel(
@@ -264,7 +270,6 @@ module.exports.getAllData = async (req, res) => {
264270
const parsedLimit = parseInt(req.query.limit, 10);
265271
const limit = Math.max(1, Math.min(Number.isNaN(parsedLimit) ? 100 : parsedLimit, 100));
266272

267-
// Use cursor-based pagination if cursor parameter is provided, otherwise use offset-based
268273
const useCursor = !!req.query.cursor;
269274
if (useCursor) {
270275
features.cursorPaginate();
@@ -274,7 +279,6 @@ module.exports.getAllData = async (req, res) => {
274279

275280
const data = await features.query.lean();
276281

277-
// Handle cursor pagination: slice to actual limit and generate next cursor
278282
let items = data;
279283
let nextCursor = null;
280284
if (useCursor) {
@@ -327,7 +331,6 @@ module.exports.getAllData = async (req, res) => {
327331
});
328332
}
329333
};
330-
331334
// GET SINGLE DOC
332335
module.exports.getSingleDoc = async (req, res) => {
333336
try {
@@ -737,4 +740,4 @@ module.exports.recoverSingleDoc = async (req, res, next) => {
737740
}
738741
return next(new AppError(500, "Failed to recover document."));
739742
}
740-
};
743+
};

0 commit comments

Comments
 (0)