-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor recent food module for improved structure and functionality #1438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: marcuscastelo/issue1447-remove-shims
Are you sure you want to change the base?
Refactor recent food module for improved structure and functionality #1438
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR restructures the recent food module by introducing a clean architecture pattern with distinct layers, moving components to more appropriate locations, and preparing the groundwork for real-time synchronization capabilities.
Key Changes
- Introduced a service layer (
recentFoodCrudService) to separate business logic from use case orchestration - Consolidated recent food operations into a centralized
recentFoodUseCasesmodule with consistent error handling and user feedback - Refactored the realtime update system to use dependency-injected callbacks instead of directly coupling to a cache store
- Moved
RemoveFromRecentButtonfrom common components to the recent-food module for better cohesion - Cleaned up database schema by removing deprecated
user_id_oldfields
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/shared/supabase/supabase.ts |
Added TODO comment about implementing user-specific realtime subscriptions |
src/shared/supabase/database.types.ts |
Removed deprecated user_id_old fields from recent_foods table and added users_duplicate table |
src/sections/search/ui/openTemplateSearchModal.tsx |
Added template refetch on modal close to update recent foods list |
src/sections/search/components/TemplateSearchResultItem.tsx |
Updated import path for RemoveFromRecentButton after component relocation |
src/sections/search/components/TemplateSearchModal.tsx |
Simplified item tracking logic by delegating to recentFoodUseCases.touchRecentFoodForItem |
src/modules/template-search/application/usecases/templateSearchState.ts |
Updated to use recentFoodUseCases.fetchUserRecentFoodsAsTemplates and added TODO for reactive signals |
src/modules/template-search/application/tests/templateSearchLogic.test.ts |
Updated test mocks to use renamed fetchUserRecentFoodsAsTemplates method |
src/modules/template-search/application/templateSearchLogic.ts |
Renamed dependency from fetchUserRecentFoods to fetchUserRecentFoodsAsTemplates for clarity |
src/modules/recent-food/ui/tests/RemoveFromRecentButton.test.tsx |
Removed redundant API integration and toast promise tests, focusing on business logic tests |
src/modules/recent-food/ui/RemoveFromRecentButton.tsx |
Simplified to use recentFoodUseCases and removed empty catch block |
src/modules/recent-food/infrastructure/supabase/supabaseRecentFoodGateway.ts |
Added type export for RecentFoodGateway to support dependency injection |
src/modules/recent-food/infrastructure/supabase/realtime.ts |
Refactored to accept callback functions as parameters instead of directly coupling to cache store |
src/modules/recent-food/infrastructure/signals/recentFoodCacheStore.ts |
Deleted entire file as part of cache refactoring |
src/modules/recent-food/infrastructure/recentFoodRepository.ts |
Converted to factory function accepting gateway as parameter; improved error log messages with [NON-FATAL] prefix |
src/modules/recent-food/domain/recentFood.ts |
Removed comment-only line (cleanup) |
src/modules/recent-food/application/usecases/recentFoodUseCases.ts |
New consolidated use cases module with consistent error handling, toast notifications, and touchRecentFoodForItem orchestration |
src/modules/recent-food/application/usecases/recentFoodCrud.ts |
Deleted entire file, functionality moved to recentFoodUseCases.ts |
src/modules/recent-food/application/services/recentFoodCrudService.ts |
New service layer that wraps repository methods without side effects (no toasts/errors) |
src/modules/recent-food/application/usecases/recentFoodUseCases.ts
Outdated
Show resolved
Hide resolved
src/modules/recent-food/application/usecases/recentFoodUseCases.ts
Outdated
Show resolved
Hide resolved
src/modules/recent-food/application/usecases/recentFoodUseCases.ts
Outdated
Show resolved
Hide resolved
src/modules/recent-food/application/usecases/recentFoodUseCases.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 79 out of 82 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
src/modules/diet/recent-food/infrastructure/supabase/realtime.ts:14
- The new callback-based realtime initialization improves flexibility. Consider adding JSDoc to document the expected behavior of each callback and when they're invoked (e.g., "Called when a new recent food record is inserted by any client").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Removed the display of guest terms acceptance status in the warning message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 38 out of 39 changed files in this pull request and generated 6 comments.
| await recentFoodCrudService.insertRecentFood(newRecentFoodData) | ||
| } else { | ||
| // TODO: Remove client-side user check after implementing row-level security (RLS) | ||
| if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { | ||
| throw new Error('BUG: recentFood fetched does not match current user') | ||
| } | ||
|
|
||
| if ( | ||
| currentRecentFood.type !== recentFoodRef.type || | ||
| currentRecentFood.reference_id !== recentFoodRef.referenceId | ||
| ) { | ||
| throw new Error('BUG: recentFood fetched does not match type/reference') | ||
| } | ||
|
|
||
| await recentFoodCrudService.updateRecentFood( | ||
| currentRecentFood.id, | ||
| newRecentFoodData, | ||
| ) | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling: The insertRecentFood and updateRecentFood operations can fail (returning null on error as seen in the repository implementation), but these failures are not handled here. If either operation fails, the function silently succeeds without throwing an error or notifying the caller.
This is problematic because the caller in touchRecentFoodForItem shows a generic error message to the user, but it won't be triggered if this function doesn't throw.
Recommendation: Check the return values and throw errors on failure:
const result = await recentFoodCrudService.insertRecentFood(newRecentFoodData)
if (result === null) {
throw new Error('Failed to insert recent food record')
}Similarly for the update operation at line 42-45.
| export async function touchRecentFoodForItem(item: Item) { | ||
| const [recentFoodRef] = extractRecentFoodReferenceFromItem(item) | ||
| if (recentFoodRef === undefined) { | ||
| logging.warn( | ||
| 'Cannot touch recent food for item - no trackable reference found', | ||
| { item }, | ||
| ) | ||
| showError('Não foi possível adicionar alimento aos alimentos recentes.') | ||
| return | ||
| } | ||
|
|
||
| for (const recentFoodRef of extractRecentFoodReferenceFromItem(item)) { | ||
| await touchRecentFood(recentFoodRef) | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new touchRecentFoodForItem function lacks test coverage. While extractRecentFoodReference.test.ts exists and covers the extraction logic, there are no tests for:
- The error handling when no references are found
- The loop that calls
touchRecentFoodfor each reference - The integration with
showErrorfor user feedback - The behavior when
touchRecentFoodfails for one or more references
Given that other similar files in the codebase have comprehensive test coverage (as evidenced by the test file structure), this function should have corresponding tests to ensure the error scenarios and multi-reference handling work correctly.
| void recentFoodUseCases | ||
| .deleteRecentFoodOfTemplate(props.template) | ||
| .then(props.refetch) |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling: The deleteRecentFoodOfTemplate promise is marked with void and errors are silently ignored. If the delete operation fails, the user won't see any error message, but refetch will still be called (which could show stale data).
The old implementation (visible in the deleted file) wrapped this in showPromise which provided user feedback with loading/success/error messages. The new implementation has lost this user feedback.
Recommendation: Add proper error handling:
void recentFoodUseCases
.deleteRecentFoodOfTemplate(props.template)
.then(props.refetch)
.catch((err) => {
logging.error('Failed to delete recent food', err)
showError('Erro ao remover item da lista de recentes.')
})Note: The deleteRecentFoodOfTemplate implementation already shows toast messages via showPromise, so this may already be handled. However, it's not clear from the code here.
| onInsert: (_: unknown) => undefined, | ||
| onUpdate: (_: unknown) => undefined, | ||
| onDelete: (_: unknown) => undefined, |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TODO indicates that realtime updates are initialized but not yet implemented (callbacks are no-ops). While this sets up the infrastructure, the realtime subscription is active and consuming resources without providing any functionality.
Consider either:
- Not initializing realtime until the cache is implemented
- Or adding a feature flag to conditionally enable it
- Or at minimum, add logging to indicate when realtime events are received but ignored
This would make it clearer that the feature is WIP and prevent confusion about why realtime isn't working.
| onInsert: (_: unknown) => undefined, | |
| onUpdate: (_: unknown) => undefined, | |
| onDelete: (_: unknown) => undefined, | |
| onInsert: (event: unknown) => { | |
| console.log('[recent-food][realtime] onInsert event received but ignored (feature not yet implemented):', event) | |
| }, | |
| onUpdate: (event: unknown) => { | |
| console.log('[recent-food][realtime] onUpdate event received but ignored (feature not yet implemented):', event) | |
| }, | |
| onDelete: (event: unknown) => { | |
| console.log('[recent-food][realtime] onDelete event received but ignored (feature not yet implemented):', event) | |
| }, |
| export async function touchRecentFood(recentFoodRef: RecentFoodReference) { | ||
| const authUseCases = useCases.authUseCases() | ||
| const currentRecentFood = | ||
| await recentFoodCrudService.fetchRecentFoodByUserTypeAndReferenceId( | ||
| authUseCases.currentUserIdOrGuestId(), | ||
| recentFoodRef.type, | ||
| recentFoodRef.referenceId, | ||
| ) | ||
|
|
||
| const timesCurrentlyUsed = currentRecentFood?.times_used ?? 0 | ||
| const newRecentFoodData: NewRecentFood = createNewRecentFood({ | ||
| user_id: authUseCases.currentUserIdOrGuestId(), | ||
| type: recentFoodRef.type, | ||
| reference_id: recentFoodRef.referenceId, | ||
| last_used: new Date(), | ||
| times_used: timesCurrentlyUsed + 1, | ||
| }) | ||
|
|
||
| if (currentRecentFood === null) { | ||
| await recentFoodCrudService.insertRecentFood(newRecentFoodData) | ||
| } else { | ||
| // TODO: Remove client-side user check after implementing row-level security (RLS) | ||
| if (currentRecentFood.user_id !== authUseCases.currentUserIdOrGuestId()) { | ||
| throw new Error('BUG: recentFood fetched does not match current user') | ||
| } | ||
|
|
||
| if ( | ||
| currentRecentFood.type !== recentFoodRef.type || | ||
| currentRecentFood.reference_id !== recentFoodRef.referenceId | ||
| ) { | ||
| throw new Error('BUG: recentFood fetched does not match type/reference') | ||
| } | ||
|
|
||
| await recentFoodCrudService.updateRecentFood( | ||
| currentRecentFood.id, | ||
| newRecentFoodData, | ||
| ) | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The touchRecentFood function lacks test coverage. This is a critical function that handles:
- Fetching existing recent food records
- Creating new records or updating existing ones
- Validating user/type/reference matches (security checks at lines 31-40)
- Integration with the CRUD service
Given the comprehensive test structure in this codebase, this function should have tests covering:
- Successful insert when no existing record
- Successful update when record exists
- Error cases when validation fails (mismatched user, type, or reference)
- Behavior when CRUD operations return null (failures)
- Proper incrementing of times_used counter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 58 out of 59 changed files in this pull request and generated 4 comments.
| }) { | ||
| const svc = deps?.crudService?.() ?? createMacroProfileCrudService() | ||
| const localCache = deps?.cache ?? cache | ||
| const localCache = () => deps?.cache ?? cache |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the issue in macroTargetUseCases.ts, wrapping localCache in a function is inconsistent with the handling of svc (line 22) and causes unnecessary function calls on every cache operation (lines 31, 35, 46, 65, 79).
The svc dependency is resolved once at creation time, but localCache is wrapped in a function that gets called every time the cache is accessed. This creates:
- Inconsistent dependency resolution patterns
- Unnecessary function call overhead
- Confusion for maintainers
Recommendation: Store the cache directly for consistency:
const localCache = deps?.cache ?? cacheThen use it directly without calling it as a function:
localCache.upsertManyToCache(profiles)| async deleteRecentFoodOfTemplate(template: Template) { | ||
| const [recentFoodReference, ...rest] = | ||
| extractRecentFoodReferenceFromTemplate(template) | ||
|
|
||
| const authUseCases = useCases.authUseCases() | ||
|
|
||
| if (recentFoodReference === undefined) { | ||
| return | ||
| } | ||
| if (rest.length > 0) { | ||
| logging.warn( | ||
| 'Expected only one recent food reference for template, but found multiple.', | ||
| { | ||
| template, | ||
| recentFoodReferences: [recentFoodReference, ...rest], | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| await recentFoodUseCases.deleteRecentFoodByReference( | ||
| authUseCases.currentUserIdOrGuestId(), | ||
| recentFoodReference.type, | ||
| recentFoodReference.referenceId, | ||
| ) | ||
| }, |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deleteRecentFoodOfTemplate function has inconsistent error handling compared to other methods in this object. It silently returns early if recentFoodReference is undefined (line 103-105) without showing any error to the user or logging, but it does log a warning if multiple references are found (lines 106-114).
Additionally, the function doesn't wrap the deletion in showPromise or handle errors from the deleteRecentFoodByReference call (line 116), which could silently fail. This is inconsistent with how other methods in this object handle user-facing operations.
Recommendation: Add proper error handling and user feedback:
async deleteRecentFoodOfTemplate(template: Template) {
const [recentFoodReference, ...rest] = extractRecentFoodReferenceFromTemplate(template)
if (recentFoodReference === undefined) {
logging.warn('Cannot delete recent food - template has no trackable reference', { template })
return
}
if (rest.length > 0) {
logging.warn(
'Expected only one recent food reference for template, but found multiple.',
{ template, recentFoodReferences: [recentFoodReference, ...rest] },
)
}
const authUseCases = useCases.authUseCases()
await recentFoodUseCases.deleteRecentFoodByReference(
authUseCases.currentUserIdOrGuestId(),
recentFoodReference.type,
recentFoodReference.referenceId,
)
}| @@ -1,10 +1,362 @@ | |||
| { | |||
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The $schema field has been removed from the Biome configuration. While not strictly required, including the schema reference provides IDE autocompletion and validation benefits. Consider keeping it:
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
// ...
}
}This helps developers get inline documentation and prevents configuration errors.
| { | |
| { | |
| "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", |
| const localWeightUseCases = () => | ||
| deps?.weightUseCases ?? useCases.weightUseCases() |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapping localWeightUseCases in a function creates an inconsistency in the dependency handling pattern and causes it to be called every time it's used (line 28).
The other dependencies (localUserMacroProfiles and localGetEffectiveMacroProfile) are stored as direct values, not functions. This inconsistency can lead to:
- Unnecessary function calls on every invocation
- Pattern confusion for developers maintaining this code
- Potential issues if
useCases.weightUseCases()has side effects
Since WeightUseCases appears to be a stable object/service, you should either:
- Store it directly like the other deps:
const localWeightUseCases = deps?.weightUseCases ?? useCases.weightUseCases() - Or wrap ALL dependencies consistently in provider functions
The first option is recommended for consistency with the existing pattern in this file.
Restructure the recent food module by moving components, simplifying data management, and enhancing real-time updates. This refactor improves code organization and reduces unnecessary exports.
Fix #1404