Skip to content

Conversation

@marcuscastelo
Copy link
Owner

@marcuscastelo marcuscastelo commented Dec 3, 2025

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

Copilot AI review requested due to automatic review settings December 3, 2025 18:33
@marcuscastelo marcuscastelo self-assigned this Dec 3, 2025
@vercel
Copy link

vercel bot commented Dec 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
macroflows Ready Ready Preview Comment Dec 6, 2025 6:53pm

@github-actions github-actions bot added backend Backend-specific issue or change complexity-medium Medium implementation complexity refactor Code refactoring required subissue labels Dec 3, 2025
Copy link
Contributor

Copilot AI left a 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 recentFoodUseCases module 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 RemoveFromRecentButton from common components to the recent-food module for better cohesion
  • Cleaned up database schema by removing deprecated user_id_old fields

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)

Copy link
Contributor

Copilot AI left a 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").

Copy link
Contributor

Copilot AI left a 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.
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 28 to 46
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,
)
}
Copy link

Copilot AI Dec 6, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +21
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)
}
}
Copy link

Copilot AI Dec 6, 2025

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:

  1. The error handling when no references are found
  2. The loop that calls touchRecentFood for each reference
  3. The integration with showError for user feedback
  4. The behavior when touchRecentFood fails 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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
void recentFoodUseCases
.deleteRecentFoodOfTemplate(props.template)
.then(props.refetch)
Copy link

Copilot AI Dec 6, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +20
onInsert: (_: unknown) => undefined,
onUpdate: (_: unknown) => undefined,
onDelete: (_: unknown) => undefined,
Copy link

Copilot AI Dec 6, 2025

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:

  1. Not initializing realtime until the cache is implemented
  2. Or adding a feature flag to conditionally enable it
  3. 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.

Suggested change
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)
},

Copilot uses AI. Check for mistakes.
Comment on lines 9 to 47
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,
)
}
}
Copy link

Copilot AI Dec 6, 2025

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:

  1. Fetching existing recent food records
  2. Creating new records or updating existing ones
  3. Validating user/type/reference matches (security checks at lines 31-40)
  4. 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

Copilot uses AI. Check for mistakes.
@marcuscastelo marcuscastelo changed the base branch from rc/v0.16.0 to marcuscastelo/issue1447-remove-shims December 6, 2025 18:31
Copy link
Contributor

Copilot AI left a 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
Copy link

Copilot AI Dec 6, 2025

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:

  1. Inconsistent dependency resolution patterns
  2. Unnecessary function call overhead
  3. Confusion for maintainers

Recommendation: Store the cache directly for consistency:

const localCache = deps?.cache ?? cache

Then use it directly without calling it as a function:

localCache.upsertManyToCache(profiles)

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +121
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,
)
},
Copy link

Copilot AI Dec 6, 2025

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,
  )
}

Copilot uses AI. Check for mistakes.
@@ -1,10 +1,362 @@
{
Copy link

Copilot AI Dec 6, 2025

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.

Suggested change
{
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 21
const localWeightUseCases = () =>
deps?.weightUseCases ?? useCases.weightUseCases()
Copy link

Copilot AI Dec 6, 2025

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:

  1. Unnecessary function calls on every invocation
  2. Pattern confusion for developers maintaining this code
  3. 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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend-specific issue or change complexity-medium Medium implementation complexity refactor Code refactoring required subissue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

recent-food: Encapsulate infra exports — export only factories

2 participants