Skip to content

feat(zod): validation for exercises#911

Closed
Sim-sat wants to merge 1 commit intoCodeWithCJ:mainfrom
Sim-sat:zod-validation-apikey
Closed

feat(zod): validation for exercises#911
Sim-sat wants to merge 1 commit intoCodeWithCJ:mainfrom
Sim-sat:zod-validation-apikey

Conversation

@Sim-sat
Copy link
Contributor

@Sim-sat Sim-sat commented Mar 14, 2026

Tip

Help us review and merge your PR faster!
Please ensure you have completed the Checklist below.
For Frontend changes, please run pnpm run validate to 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:

  • [MANDATORY for new feature] Alignment: I have raised a GitHub issue and it was reviewed/approved by maintainers
  • Tests: I have included automated tests for my changes.
  • [MANDATORY for UI changes] Screenshots: I have attached "Before" vs "After" screenshots below.
  • [MANDATORY for Frontend changes] Quality: I have run pnpm run validate (especially for Frontend).
  • Translations: I have only updated the English (en) translation file (if applicable).
  • Architecture: My code follows the existing architecture standards.
  • Database Security: I have updated rls_policies.sql for any new user-specific tables.
  • [MANDATORY - ALL] Integrity & License: I certify this is my own work, free of malicious code(phishing, malware, etc.) and I agree to the License terms.

Screenshots (if applicable)

Before

[Insert screenshot/GIF here]

After

[Insert screenshot/GIF here]

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, 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

  • Zod Schema Integration: Introduced Zod schemas for various exercise-related API requests and responses in the shared workspace, enhancing data validation and type safety across the application.
  • Frontend Type Refactoring: Refactored frontend type definitions for Exercise, ExerciseEntry, WorkoutPreset, WorkoutPlanAssignment, and WorkoutPlanTemplate to derive from the newly created Zod-generated API response schemas.
  • API Service Function Updates: Updated API service functions for creating and updating exercises, exercise entries, workout presets, and workout plan templates to utilize the new Zod-derived request and response types.
  • Improved Nullish Handling in UI: Implemented more robust handling of potentially null or undefined values in various UI components (e.g., EditExerciseDatabaseDialog, EditExerciseEntryDialog, LogExerciseEntryDialog) using the nullish coalescing operator (??).
Changelog
  • SparkyFitnessFrontend/src/api/Exercises/exerciseEntryService.ts
    • Updated imports to include new Zod-derived types for exercise entries.
    • Refactored createExerciseEntry and updateExerciseEntry payloads to use Zod-derived request types.
    • Improved nullish coalescing for exercise snapshot properties during data parsing.
  • SparkyFitnessFrontend/src/api/Exercises/exerciseService.ts
    • Replaced ExercisePayload interface with CreateExercisesRequest and UpdateExercisesRequest from Zod schemas.
    • Updated createExercise and updateExercise functions to use the new Zod-derived types.
  • SparkyFitnessFrontend/src/api/Exercises/workoutPlanTemplates.ts
    • Imported new Zod-derived types for workout plan templates.
    • Updated createWorkoutPlanTemplate and updateWorkoutPlanTemplate function signatures to use Zod-derived request types.
  • SparkyFitnessFrontend/src/api/Exercises/workoutPresets.ts
    • Imported new Zod-derived types for workout presets.
    • Updated createWorkoutPreset and updateWorkoutPreset function signatures to use Zod-derived request types.
  • SparkyFitnessFrontend/src/hooks/Exercises/useWorkoutPlans.ts
    • Modified useCreateWorkoutPlanTemplateMutation to explicitly include user_id in the payload.
    • Adjusted useUpdateWorkoutPlanTemplateMutation to destructure and pass only relevant data to the API.
  • SparkyFitnessFrontend/src/hooks/Exercises/useWorkoutPresets.ts
    • Modified useUpdateWorkoutPresetMutation to destructure and pass only relevant data to the API.
  • SparkyFitnessFrontend/src/pages/Diary/EditExerciseDatabaseDialog.tsx
    • Updated UI components to use the nullish coalescing operator (??) for calories_per_hour, category, level, force, and mechanic fields.
  • SparkyFitnessFrontend/src/pages/Diary/EditExerciseEntryDialog.tsx
    • Adjusted calculations for distanceInput and caloriesPerHour to use nullish coalescing.
    • Refined activity_details mapping and conditional submission logic.
  • SparkyFitnessFrontend/src/pages/Diary/ExerciseCard.tsx
    • Updated type casting for exerciseEntry to unknown as ExerciseEntry for broader compatibility.
  • SparkyFitnessFrontend/src/pages/Diary/LogExerciseEntryDialog.tsx
    • Integrated useAuth hook to access user information.
    • Updated caloriesBurnedInput calculation with nullish coalescing.
    • Ensured user.id is passed to createExerciseEntry.
  • SparkyFitnessFrontend/src/pages/Exercises/AddWorkoutPlanDialog.tsx
    • Ensured start_date is explicitly set to null if undefined when saving workout plans.
  • SparkyFitnessFrontend/src/pages/Exercises/ExerciseSearch.tsx
    • Updated calories_per_hour usage with nullish coalescing in display logic.
  • SparkyFitnessFrontend/src/pages/Exercises/Exercises.tsx
    • Updated setEditExerciseCategory to handle potential null or undefined values for category.
  • SparkyFitnessFrontend/src/pages/Exercises/WorkoutPlansManager.tsx
    • Updated checked prop for Switch component to use nullish coalescing for is_active.
  • SparkyFitnessFrontend/src/types/exercises.ts
    • Refactored Exercise and ExerciseEntry types to derive from new Zod-generated API response schemas.
    • Explicitly handled JSON-serialized fields and added frontend-only fields.
    • Removed old interface definitions for Exercise and ExerciseEntry.
  • SparkyFitnessFrontend/src/types/workout.ts
    • Refactored WorkoutPreset, WorkoutPlanAssignment, and WorkoutPlanTemplate types to derive from new Zod-generated API response schemas.
  • shared/src/index.ts
    • Exported new Zod API schemas for various exercise-related entities.
  • shared/src/schemas/api/ExerciseEntries.api.zod.ts
    • Added new Zod schemas for ExerciseEntriesResponse, CreateExerciseEntriesRequest, and UpdateExerciseEntriesRequest.
  • shared/src/schemas/api/ExerciseEntryActivityDetails.api.zod.ts
    • Added new Zod schemas for ExerciseEntryActivityDetailsResponse, CreateExerciseEntryActivityDetailsRequest, and UpdateExerciseEntryActivityDetailsRequest.
  • shared/src/schemas/api/ExerciseEntrySets.api.zod.ts
    • Added new Zod schemas for ExerciseEntrySetsResponse, CreateExerciseEntrySetsRequest, and UpdateExerciseEntrySetsRequest.
  • shared/src/schemas/api/ExercisePresetEntries.api.zod.ts
    • Added new Zod schemas for ExercisePresetEntriesResponse, CreateExercisePresetEntriesRequest, and UpdateExercisePresetEntriesRequest.
  • shared/src/schemas/api/Exercises.api.zod.ts
    • Added new Zod schemas for ExercisesResponse, CreateExercisesRequest, and UpdateExercisesRequest.
  • shared/src/schemas/api/WorkoutPlanAssignmentSets.api.zod.ts
    • Added new Zod schemas for WorkoutPlanAssignmentSetsResponse, CreateWorkoutPlanAssignmentSetsRequest, and UpdateWorkoutPlanAssignmentSetsRequest.
  • shared/src/schemas/api/WorkoutPlanTemplateAssignments.api.zod.ts
    • Added new Zod schemas for WorkoutPlanTemplateAssignmentsResponse, CreateWorkoutPlanTemplateAssignmentsRequest, and UpdateWorkoutPlanTemplateAssignmentsRequest.
  • shared/src/schemas/api/WorkoutPlanTemplates.api.zod.ts
    • Added new Zod schemas for WorkoutPlanTemplatesResponse, CreateWorkoutPlanTemplatesRequest, and UpdateWorkoutPlanTemplatesRequest.
  • shared/src/schemas/api/WorkoutPresetExerciseSets.api.zod.ts
    • Added new Zod schemas for WorkoutPresetExerciseSetsResponse, CreateWorkoutPresetExerciseSetsRequest, and UpdateWorkoutPresetExerciseSetsRequest.
  • shared/src/schemas/api/WorkoutPresetExercises.api.zod.ts
    • Added new Zod schemas for WorkoutPresetExercisesResponse, CreateWorkoutPresetExercisesRequest, and UpdateWorkoutPresetExercisesRequest.
  • shared/src/schemas/api/WorkoutPresets.api.zod.ts
    • Added new Zod schemas for WorkoutPresetsResponse, CreateWorkoutPresetsRequest, and UpdateWorkoutPresetsRequest.
Activity
  • The pull request was opened by Sim-sat with the title 'feat(zod): validation for exercises'.
  • The author provided a detailed checklist for review, including mandatory items for new features, tests, UI changes, quality, translations, architecture, database security, and integrity.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +136
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[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)

@Sim-sat Sim-sat marked this pull request as draft March 14, 2026 17:22
@Sim-sat Sim-sat force-pushed the zod-validation-apikey branch from 7824926 to c7a4249 Compare March 14, 2026 18:22
@Sim-sat Sim-sat closed this Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant