feat(zod): validation for exercises#911
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly improves the application's data integrity and developer experience by integrating Zod for API schema validation. It involves creating a comprehensive set of Zod schemas for exercise-related entities in the shared library and subsequently updating the frontend's type definitions and API interaction logic to leverage these new schemas. This refactoring ensures that data passed between the frontend and backend adheres to strict validation rules, leading to more predictable behavior and fewer runtime errors. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant and valuable refactoring by integrating shared Zod schemas for API data validation across the frontend. This greatly improves type safety and consistency with the backend. The changes are extensive, touching many files to adopt the new request and response types.
My review focuses on a couple of areas for improvement:
- I've identified a case of code duplication that could be refactored for better maintainability.
- There's a redundancy in how a request payload is constructed, which should be cleaned up to avoid confusion.
Overall, this is a solid step forward for the codebase's robustness.
| const parsedEntries: GroupedExerciseEntry[] = (response || []).map( | ||
| (entry) => { | ||
| if (entry.type === 'preset') { | ||
| return { | ||
| ...entry, | ||
| exercises: entry.exercises | ||
| ? entry.exercises.map((ex) => ({ | ||
| ...ex, | ||
| sets: ex.sets ? ex.sets : [], | ||
| exercise_snapshot: { | ||
| ...ex.exercise_snapshot, | ||
| id: ex.exercise_snapshot.id ?? '', | ||
| name: ex.exercise_snapshot.name ?? '', | ||
| description: ex.exercise_snapshot.description ?? null, | ||
| calories_per_hour: | ||
| ex.exercise_snapshot.calories_per_hour ?? 0, | ||
| category: ex.exercise_snapshot.category ?? '', | ||
| equipment: parseJsonArray(ex.exercise_snapshot.equipment), | ||
| primary_muscles: parseJsonArray( | ||
| ex.exercise_snapshot.primary_muscles | ||
| ), | ||
| secondary_muscles: parseJsonArray( | ||
| ex.exercise_snapshot.secondary_muscles | ||
| ), | ||
| instructions: parseJsonArray( | ||
| ex.exercise_snapshot.instructions | ||
| ), | ||
| images: parseJsonArray(ex.exercise_snapshot.images), | ||
| } as Exercise, | ||
| activity_details: ex.activity_details | ||
| ? ex.activity_details.map((detail) => ({ | ||
| id: detail.id ?? '', | ||
| key: detail.detail_type ?? '', | ||
| value: | ||
| typeof detail.detail_data === 'object' | ||
| ? JSON.stringify(detail.detail_data, null, 2) | ||
| : String(detail.detail_data), | ||
| provider_name: detail.provider_name, | ||
| detail_type: detail.detail_type ?? '', | ||
| })) | ||
| : [], | ||
| })) | ||
| : [], | ||
| }; | ||
| } else { | ||
| return { | ||
| ...entry, | ||
| sets: entry.sets ? entry.sets : [], | ||
| exercise_snapshot: (entry.exercise_snapshot | ||
| ? { | ||
| ...entry.exercise_snapshot, | ||
| id: entry.exercise_snapshot.id ?? '', | ||
| name: entry.exercise_snapshot.name ?? '', | ||
| description: entry.exercise_snapshot.description ?? null, | ||
| category: entry.exercise_snapshot.category ?? '', | ||
| calories_per_hour: | ||
| entry.exercise_snapshot.calories_per_hour ?? 0, | ||
| equipment: parseJsonArray(entry.exercise_snapshot.equipment), | ||
| primary_muscles: parseJsonArray( | ||
| ex.exercise_snapshot.primary_muscles | ||
| entry.exercise_snapshot.primary_muscles | ||
| ), | ||
| secondary_muscles: parseJsonArray( | ||
| ex.exercise_snapshot.secondary_muscles | ||
| entry.exercise_snapshot.secondary_muscles | ||
| ), | ||
| instructions: parseJsonArray(ex.exercise_snapshot.instructions), | ||
| images: parseJsonArray(ex.exercise_snapshot.images), | ||
| }, | ||
| activity_details: ex.activity_details | ||
| ? ex.activity_details.map((detail) => ({ | ||
| id: detail.id ?? '', | ||
| key: detail.detail_type ?? '', | ||
| value: | ||
| typeof detail.detail_data === 'object' | ||
| ? JSON.stringify(detail.detail_data, null, 2) | ||
| : String(detail.detail_data), | ||
| provider_name: detail.provider_name, | ||
| detail_type: detail.detail_type ?? '', | ||
| })) | ||
| : [], | ||
| })) | ||
| : [], | ||
| }; | ||
| } else { | ||
| return { | ||
| ...entry, | ||
| sets: entry.sets ? entry.sets : [], // Parse sets if it's a JSON string | ||
| exercise_snapshot: { | ||
| ...entry.exercise_snapshot, // Use the existing snapshot | ||
| id: entry.exercise_snapshot?.id ?? '', | ||
| name: entry.exercise_snapshot?.name ?? '', | ||
| category: entry.exercise_snapshot?.category ?? '', | ||
| calories_per_hour: entry.exercise_snapshot?.calories_per_hour ?? 0, | ||
| equipment: parseJsonArray(entry.exercise_snapshot?.equipment), | ||
| primary_muscles: parseJsonArray( | ||
| entry.exercise_snapshot?.primary_muscles | ||
| ), | ||
| secondary_muscles: parseJsonArray( | ||
| entry.exercise_snapshot?.secondary_muscles | ||
| ), | ||
| instructions: parseJsonArray(entry.exercise_snapshot?.instructions), | ||
| images: parseJsonArray(entry.exercise_snapshot?.images), | ||
| }, | ||
| activity_details: entry.activity_details | ||
| ? entry.activity_details.map((detail) => ({ | ||
| id: detail.id ?? '', | ||
| key: detail.detail_type ?? '', | ||
| value: | ||
| typeof detail.detail_data === 'object' | ||
| ? JSON.stringify(detail.detail_data, null, 2) | ||
| : String(detail.detail_data), | ||
| provider_name: detail.provider_name, | ||
| detail_type: detail.detail_type ?? '', | ||
| })) | ||
| : [], | ||
| }; | ||
| instructions: parseJsonArray( | ||
| entry.exercise_snapshot.instructions | ||
| ), | ||
| images: parseJsonArray(entry.exercise_snapshot.images), | ||
| } | ||
| : { | ||
| id: '', | ||
| name: '', | ||
| category: '', | ||
| calories_per_hour: 0, | ||
| description: null, | ||
| equipment: [], | ||
| primary_muscles: [], | ||
| secondary_muscles: [], | ||
| instructions: [], | ||
| images: [], | ||
| user_id: null, | ||
| is_custom: false, | ||
| created_at: '', | ||
| updated_at: '', | ||
| }) as Exercise, | ||
| activity_details: entry.activity_details | ||
| ? entry.activity_details.map((detail) => ({ | ||
| id: detail.id ?? '', | ||
| key: detail.detail_type ?? '', | ||
| value: | ||
| typeof detail.detail_data === 'object' | ||
| ? JSON.stringify(detail.detail_data, null, 2) | ||
| : String(detail.detail_data), | ||
| provider_name: detail.provider_name, | ||
| detail_type: detail.detail_type ?? '', | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
| } | ||
| }); | ||
| ) as GroupedExerciseEntry[]; |
There was a problem hiding this comment.
The logic for parsing exercise_snapshot is duplicated for both preset and individual exercise entries. This can be refactored into a helper function to improve maintainability and reduce code duplication.
Additionally, the parsing for preset exercises (ex.exercise_snapshot) assumes the snapshot object exists, which could lead to a runtime error if it's null or undefined. The logic for individual entries is safer as it checks for existence.
Here's a suggested helper function that you can define within this file:
const parseExerciseSnapshot = (snapshot: any): Exercise => {
if (!snapshot) {
return {
id: '',
name: '',
category: '',
calories_per_hour: 0,
description: null,
equipment: [],
primary_muscles: [],
secondary_muscles: [],
instructions: [],
images: [],
user_id: null,
is_custom: false,
created_at: '',
updated_at: '',
};
}
return {
...snapshot,
id: snapshot.id ?? '',
name: snapshot.name ?? '',
description: snapshot.description ?? null,
calories_per_hour: snapshot.calories_per_hour ?? 0,
category: snapshot.category ?? '',
equipment: parseJsonArray(snapshot.equipment),
primary_muscles: parseJsonArray(snapshot.primary_muscles),
secondary_muscles: parseJsonArray(snapshot.secondary_muscles),
instructions: parseJsonArray(snapshot.instructions),
images: parseJsonArray(snapshot.images),
} as Exercise;
};You can then use it like this:
exercise_snapshot: parseExerciseSnapshot(ex.exercise_snapshot)exercise_snapshot: parseExerciseSnapshot(entry.exercise_snapshot)
7824926 to
c7a4249
Compare
Tip
Help us review and merge your PR faster!
Please ensure you have completed the Checklist below.
For Frontend changes, please run
pnpm run validateto check for any errors.PRs that include tests and clear screenshots are highly preferred!
Description
Provide a brief summary of your changes.
Related Issue
PR type [ ] Issue [ ] New Feature [ ] Documentation
Linked Issue: #
Checklist
Please check all that apply:
pnpm run validate(especially for Frontend).en) translation file (if applicable).rls_policies.sqlfor any new user-specific tables.Screenshots (if applicable)
Before
[Insert screenshot/GIF here]
After
[Insert screenshot/GIF here]