diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index adf8efb5..83755958 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -44,38 +44,40 @@ router.get('/', async (req: AuthRequest, res, next) => { whereClause += ` AND (p.is_archived = false OR p.is_archived IS NULL)`; } - const countResult = await query( - `SELECT COUNT(*) as total FROM projects p ${whereClause}`, - params - ) as { total: string }[]; - const total = parseInt(countResult?.[0]?.total || '0'); - const allowedSortFields = ['created_at', 'updated_at', 'name']; const safeSortBy = allowedSortFields.includes(sortBy as string) ? sortBy as string : 'created_at'; - // Buscar projetos com contagem de tarefas - const projects = await query( - `SELECT - p.*, - COALESCE(t.task_count, 0)::int as task_count, - COALESCE(t.completed_count, 0)::int as completed_count, - COALESCE(t.pending_count, 0)::int as pending_count - FROM projects p - LEFT JOIN ( - SELECT - project_id, - COUNT(*) as task_count, - COUNT(*) FILTER (WHERE status = 'completed') as completed_count, - COUNT(*) FILTER (WHERE status != 'completed') as pending_count - FROM tasks - WHERE user_id = $1 - GROUP BY project_id - ) t ON p.id = t.project_id - ${whereClause} - ORDER BY p.${safeSortBy} ${order}, p.created_at DESC - LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, - [...params, limit, offset] - ); + // Buscar contagem e projetos em paralelo + const [countResult, projects] = await Promise.all([ + query( + `SELECT COUNT(*) as total FROM projects p ${whereClause}`, + params + ) as Promise<{ total: string }[]>, + query( + `SELECT + p.*, + COALESCE(t.task_count, 0)::int as task_count, + COALESCE(t.completed_count, 0)::int as completed_count, + COALESCE(t.pending_count, 0)::int as pending_count + FROM projects p + LEFT JOIN ( + SELECT + project_id, + COUNT(*) as task_count, + COUNT(*) FILTER (WHERE status = 'completed') as completed_count, + COUNT(*) FILTER (WHERE status != 'completed') as pending_count + FROM tasks + WHERE user_id = $1 + GROUP BY project_id + ) t ON p.id = t.project_id + ${whereClause} + ORDER BY p.${safeSortBy} ${order}, p.created_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, + [...params, limit, offset] + ) + ]); + + const total = parseInt(countResult?.[0]?.total || '0'); res.json(buildPaginatedResponse(projects || [], total, { page, limit, sortBy, order })); } catch (error: unknown) { @@ -272,21 +274,23 @@ router.get('/:id/tasks', validateIdParam('id'), async (req: AuthRequest, res, ne paramIndex++; } - const countResult = await query( - `SELECT COUNT(*) as total FROM tasks ${whereClause}`, - params - ) as { total: string }[]; - const total = parseInt(countResult?.[0]?.total || '0'); - const allowedSortFields = ['created_at', 'updated_at', 'due_date', 'priority', 'title']; const safeSortBy = allowedSortFields.includes(sortBy as string) ? sortBy as string : 'created_at'; - const tasks = await query( - `SELECT * FROM tasks ${whereClause} - ORDER BY ${safeSortBy} ${order}, created_at DESC - LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, - [...params, limit, offset] - ); + const [countResult, tasks] = await Promise.all([ + query( + `SELECT COUNT(*) as total FROM tasks ${whereClause}`, + params + ) as Promise<{ total: string }[]>, + query( + `SELECT * FROM tasks ${whereClause} + ORDER BY ${safeSortBy} ${order}, created_at DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`, + [...params, limit, offset] + ) + ]); + + const total = parseInt(countResult?.[0]?.total || '0'); res.json(buildPaginatedResponse(tasks || [], total, { page, limit, sortBy, order })); } catch (error: unknown) { diff --git a/backend/src/tests/routes/projects-benchmark.test.ts b/backend/src/tests/routes/projects-benchmark.test.ts new file mode 100644 index 00000000..cbea5f1c --- /dev/null +++ b/backend/src/tests/routes/projects-benchmark.test.ts @@ -0,0 +1,66 @@ +import request from "supertest"; +import express from "express"; +import { jest } from "@jest/globals"; + +// Mock database with delay +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// Try using alias with .ts extension +jest.unstable_mockModule("@/services/database.ts", () => ({ + query: jest.fn(async (sql: string, params: any[]) => { + await sleep(50); // Simulate 50ms DB latency + if (sql.includes("COUNT(*)")) { + return [{ total: "10" }]; + } + if (sql.includes("FROM projects")) { + return Array(10).fill({ + id: "project-id", + name: "Project", + user_id: "test-user-id", + task_count: 5, + completed_count: 2, + pending_count: 3 + }); + } + return []; + }), + queryOne: jest.fn(), +})); + +jest.unstable_mockModule("@/middleware/auth.ts", () => ({ + authenticateToken: (req: any, res: any, next: any) => { + req.userId = "test-user-id"; + next(); + }, +})); + +jest.unstable_mockModule("@/middleware/validateId.ts", () => ({ + validateIdParam: (paramName: string) => (req: any, res: any, next: any) => { + next(); + }, +})); + +// Import projectsRoutes +const { projectsRoutes } = await import("../../routes/projects.js"); + +const app = express(); +app.use(express.json()); +app.use("/api/projects", projectsRoutes); + +describe("Projects API Performance", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("GET /api/projects", () => { + it("should measure execution time of list projects", async () => { + const start = Date.now(); + const res = await request(app).get("/api/projects"); + const duration = Date.now() - start; + + expect(res.status).toBe(200); + console.log(`GET /api/projects duration: ${duration}ms`); + expect(duration).toBeLessThan(90); + }); + }); +});