fix(fixes): replace N+1 getSuggestions loop with batch query#2512
fix(fixes): replace N+1 getSuggestions loop with batch query#2512mimchome wants to merge 9 commits into
Conversation
Use getAllFixesWithSuggestionsByOpportunityId to fetch all fixes and suggestions in 2-3 queries instead of one per fix. Fixes without mapping table entries are now returned with empty suggestions instead of being silently dropped. SITES-45307
|
This PR will trigger a patch release when merged. |
…ons-SITES-45307-v2
There was a problem hiding this comment.
Hey @mimchome,
Strengths
- N+1 elimination follows existing pattern (
src/controllers/fixes.js:150-151): Aligns withgetAllFixesWithSuggestionByCreatedAtalready used on thefixCreatedDatepath, reducing the number of distinct query shapes the team needs to reason about. - Corrects silent data loss: Fixes without junction table entries (58% on dev per PR description) now return with empty suggestions instead of being silently dropped. The right contract for a list endpoint.
- Integration tests assert on actual IDs (
test/it/shared/tests/fixes.js:64-71): Verifying specific suggestion IDs per fix, not just array lengths. This is the kind of test that would have caught the original silent-drop bug. - Tightly scoped change: One controller method, its tests, and seed data. No unrelated cleanup mixed in.
- New unit test for early-return path (
test/controllers/fixes.test.js:194-203): Clean AAA structure, asserts on both status and body. - Conventions compliance: PR title follows conventional commit format, unit and integration tests are present, DTOs are used correctly.
Issues
Important (Should Fix)
-
src/controllers/fixes.js:153-155- Early return on empty results bypasses ownership checkif (fixEntitiesWithSuggestions.length === 0) { return ok([]); }
The old code called
checkOwnership(fixEntities[0], opportunityId, siteId, this.#Opportunity)regardless of whether fixes existed. With the new early return, if an opportunity has zero fixes but belongs to a different site, the caller gets200 []instead of the 404 thatcheckOwnershipwould have produced.Why it matters: A caller with a valid
siteIdthey own and a guessedopportunityIdfrom a different site now gets 200 instead of 404 when that opportunity has no fixes. Returning 200 for a resource the caller does not own is incorrect regardless of what data it carries. The unit test attest/controllers/fixes.test.js:176previously tested this scenario (empty result + wrong site = 404) but was changed to return a non-empty array, masking the behavioral change.How to fix: Validate the opportunity-to-site relationship before the early return. One approach - move the ownership check above the empty guard:
const fixEntitiesWithSuggestions = await this.#FixEntity .getAllFixesWithSuggestionsByOpportunityId(opportunityId); res = await checkOwnership( fixEntitiesWithSuggestions[0]?.fixEntity, opportunityId, siteId, this.#Opportunity ); if (res) { return res; } if (fixEntitiesWithSuggestions.length === 0) { return ok([]); }
If
checkOwnershiprequires a non-null first argument to trace the relationship, validate the opportunity'ssiteIddirectly before the early return instead.
Recommendations
- The integration test verifies the happy path (2 fixes, each with 1 suggestion). Consider adding a seed fixture where a fix has no junction entry to validate the stated behavior change at the integration level ("fixes without mapping table entries now return with empty suggestions").
Assessment
Ready to merge? With fixes
Reasoning: The N+1 elimination and the silent-drop bug fix are both correct and well-tested. The ownership bypass on the empty-results path is a behavioral regression in access control that should be addressed before merge - it is a reordering fix, not a redesign.
Next Steps
- Address the ownership check ordering so the empty-results path still validates opportunity-site ownership.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 42s | Cost: $5.81 | Commit: 3efd6010aa2796dce436c3e614475380e70be5ca
If this code review was useful, please react with 👍. Otherwise, react with 👎.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…return Move checkOwnership above the length===0 guard in both the main and fixCreatedDate paths. checkOwnership falls back to Opportunity.findById when no fix is passed, so an opportunity belonging to a different site now returns 404 instead of 200 [] when it has no fixes. Add unit test for empty-result + wrong-site = 404, and an IT fixture (FIX_3, no junction entry) asserting fixes without mapping table entries are returned with empty suggestions.
…5307-v2' into fix/n-plus-1-suggestions-SITES-45307-v2
|
@MysticatBot-Dev issues are addressed |
SITES-45307
Summary
getSuggestions()calls with singlegetAllFixesWithSuggestionsByOpportunityIdbatch query (2-3 DB calls instead of N+1)Test plan
78c9837breturns 3 fixes (was 2)