fix(api): stop embedding task rows in the project list response - #1423
fix(api): stop embedding task rows in the project list response#1423alexmakarski wants to merge 1 commit into
Conversation
GET /api/project used `with: { tasks: true }` to compute three summary
numbers per project (total tasks, completion percentage, earliest due date),
then spread the loaded rows into the response via `...project`. Every task in
the workspace was fetched into memory and serialised to the client on every
call, even though the endpoint only needs aggregates.
The surrounding code suggests this was unintended: the same object explicitly
returns `archivedTasks: []`, `plannedTasks: []` and `columns: []`, so nested
collections were already being deliberately omitted here. `tasks` slipped
through the spread.
Replace the join with a grouped aggregate (count, filtered count for
done/archived, min(dueDate)). Projects with no tasks fall back to zeroed
statistics. Semantics are unchanged, including ignoring null due dates when
finding the earliest, which is what SQL min() does natively.
Measured on 10 projects / 2000 tasks, median of 5 runs:
before 946.4 KB 31.3 ms
after 3.8 KB 5.0 ms
The response is now flat with respect to task count rather than linear.
Adds tests/api-integration/project-list.test.ts covering payload shape,
statistics accuracy, empty projects, and per-project isolation. These fail
against the previous implementation.
Related: usekaneo#1037 applied the same principle to the task list endpoint.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe project list endpoint now computes task statistics with a grouped database query, omits embedded task rows, and supplies zero-valued statistics for projects without tasks. Integration tests cover payload shape, aggregation, empty projects, and project isolation. ChangesProject statistics
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant getProjects
participant getProjectStatistics
participant taskTable
Client->>getProjects: request project list
getProjects->>getProjectStatistics: provide project IDs
getProjectStatistics->>taskTable: grouped statistics query
taskTable-->>getProjectStatistics: aggregated statistics
getProjectStatistics-->>getProjects: statistics map
getProjects-->>Client: project summaries with statistics
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoFix project list API to return task aggregates without embedding tasks
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
Context used 1. Large IN query
|
| .from(taskTable) | ||
| .where(inArray(taskTable.projectId, projectIds)) | ||
| .groupBy(taskTable.projectId); |
There was a problem hiding this comment.
1. Large in query 🐞 Bug ☼ Reliability
getProjectStatistics() filters tasks via inArray(taskTable.projectId, projectIds), which expands the entire workspace’s project ID list into a single IN predicate. For large workspaces this can produce very large SQL/bind-parameter lists (slow planning/execution or even DB parameter-limit failures) and turn GET /api/project into a 500/timeout.
Agent Prompt
## Issue description
`getProjectStatistics()` currently filters tasks with `inArray(taskTable.projectId, projectIds)` where `projectIds` is the full set of workspace project IDs. This generates a potentially massive `IN (...)` predicate, which can become slow or exceed PostgreSQL bind-parameter limits for large workspaces.
## Issue Context
The tasks table is keyed to projects via `task.projectId`, and projects are already scoped by `workspaceId` in the list query. We can compute the same aggregates while filtering by `workspaceId` in SQL (join/subquery) or by chunking the ID list to keep statement size bounded.
## Fix Focus Areas
- apps/api/src/project/controllers/get-projects.ts[17-54]
- apps/api/src/project/controllers/get-projects.ts[56-76]
## Suggested fix approaches
1. **Preferable (single query, no ID explosion):** join `taskTable` to `projectTable` and filter `projectTable.workspaceId = workspaceId` (and `archivedAt` if needed), then `groupBy(taskTable.projectId)`.
2. **Alternative:** chunk `projectIds` into batches (e.g., 500–2000), run the aggregate query per chunk, and merge into the map.
Keep the existing empty-project fallback behavior unchanged.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
@alexmakarski Thank you for your contribution! Love seeing enhancements like this. Would you mind taking a look at this suggestion? Thank you!
GET /api/projectuseswith: { tasks: true }to compute three summary numbers per project (total tasks, completion percentage, earliest due date), then spreads the loaded rows into the response via...project. Every task in the workspace is fetched into memory and serialised to the client on every call, even though the endpoint only needs the aggregates.The surrounding code suggests this was unintended: the same return object explicitly sets
archivedTasks: [],plannedTasks: []andcolumns: [], so nested collections were already being deliberately omitted here.tasksslipped through the spread.Change
Replaces the join with a grouped aggregate:
count(), a filtered count fordone/archived, andmin(dueDate). Projects with no tasks fall back to zeroed statistics.Semantics are unchanged, including ignoring null due dates when finding the earliest — which is what SQL
min()does natively.Measurements
10 projects / 2,000 tasks, median of 5 runs:
The response is now flat with respect to task count rather than linear, so it stays constant as a workspace fills up.
Tests
Adds
tests/api-integration/project-list.test.tscovering payload shape, statistics accuracy, empty projects, and per-project isolation. These fail against the previous implementation.Locally: integration 68/68, unit 201/201,
biome checkandtsc --noEmitclean.Note on the response shape
Consumers reading
.tasksoff this endpoint would stop receiving it. Nothing inapps/webdoes — the board and list views readcolumn.tasksfrom a different endpoint.This follows the same principle as #1037, where the task list endpoint stopped forcing clients to download everything and filter locally. This endpoint appears to have been missed by that pass.
Happy to rework this as an opt-in (
?includeTasks=true) instead if you'd prefer to keep the current shape.Summary by CodeRabbit
New Features
Bug Fixes
Tests