Skip to content

Conversation

@ivan-dalmet
Copy link
Member

@ivan-dalmet ivan-dalmet commented Oct 9, 2025

  • Update dependencies
  • Migrate from zod 3 to zod 4
  • Enable React compiler
  • Rename env var AUTH_TRUSTED_ORIGIN to AUTH_TRUSTED_ORIGINS

Summary by CodeRabbit

  • New Features

    • Added new locale messages (EN/FR/AR/SW) including "Required" and "User already exists".
  • Bug Fixes / UX

    • Standardized form validation and error messaging across many forms.
    • Improved search input debouncing and state synchronization.
    • Demo mode now blocks user create/update actions.
  • Refactor

    • Consolidated field validation utilities into a unified API.
  • Chores

    • Broad dependency upgrades; CI now fails on lint warnings.
  • Tests

    • Removed legacy validation unit tests.
  • Localization cleanup

    • Added/removed several locale keys and removed embedded Zod i18n mappings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

Project-wide migration of Zod field helpers to zu.fieldText, many zod schema simplifications and .default(...).prefault(...) changes, ESLint/CI tightening and config export change, dependency upgrades, locale import/export adjustments, SearchInput debounce migrated to useEffectEvent, and auth gained database hooks with DEMO checks.

Changes

Cohort / File(s) Summary
CI & Tooling
.github/workflows/code-quality.yml, eslint.config.ts, package.json, .storybook/preview.tsx, .vscode/settings.example.json
ESLint step now fails on warnings (--max-warnings 0); exported ESLint config switched to defineConfig(...); broad dependency upgrades; Storybook backgrounds.disabledisabled; VSCode i18n-ally localesPaths simplified.
Zod utils & tests
src/lib/zod/zod-utils.ts, src/lib/zod/zod-utils.unit.spec.ts (deleted)
Replaced legacy zu.string/zu.array APIs with zu.fieldText.{required,nullable,nullish,optional} including empty-string transforms; removed legacy unit tests.
Env & server/client schemas
src/env/client.ts, src/env/server.ts, .env.example
Zod adjustments (z.string().url()z.url(), many .default(...).prefault(...)), renamed auth env keys (e.g., BETTER_AUTH_SECRETAUTH_SECRET), AUTH_TRUSTED_ORIGINS parsing to array, and refine error key updates.
Server routers & fetching
src/server/routers/book.ts, src/server/routers/genre.ts, src/server/routers/user.ts
Schema tweaks (e.g., z.string().cuid()z.string()), defaults → prefault(...), limit prefaults, moved from context.db.$transaction([...]) to Promise.all([...]); book router uses zFormFieldsBook; delete permission scope changed to book.
Feature schemas
src/features/*.ts (book, genre, user, auth, account)
Field validators migrated to zu.fieldText.*; email/name validation composition updated; some exported schema field shapes changed (IDs/emails simplified).
Forms, stories & tests
src/components/form/*.stories.tsx, src/components/form/field-checkbox-group/docs.stories.tsx, src/components/form/field-checkbox/field-checkbox.browser.spec.tsx, src/components/form/form-test-utils.tsx
Stories/tests updated to use zu.fieldText and native Zod array APIs; FormMocked generic tightened to T extends ZodType<FieldValues>.
SearchInput debounce refactor
src/components/ui/search-input.tsx
Debounce changed from ref-based approach to React 18 useEffectEvent with simplified state sync.
Lint annotations
src/components/ui/calendar.tsx, src/components/ui/calendar.browser.spec.tsx, src/components/ui/date-input.tsx, src/components/ui/sidebar.tsx, src/components/ui/theme-switcher.tsx, src/hooks/use-value-has-changed.ts
Added/expanded ESLint disable comments for various react-hooks rules; no runtime logic changes.
Locales & i18n
src/locales/*/*, src/lib/i18n/index.ts, src/locales/*/index.ts
Added USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL entries, added common.errors.required in locales, removed select FR/SW keys, changed JSON import syntax, removed zod locale exports and zod-i18n-map usage.
OpenAPI helper import
src/routes/api/openapi/app.schema.ts
Switched import @orpc/zod@orpc/zod/zod4.
Vite / Prisma copy path
vite.config.ts
Added babel-plugin-react-compiler to viteReact config; prisma binary copy destination changed from chunks/_chunks.
Auth hooks & config
src/server/auth.tsx, src/server/auth.tsx additions, .env.example
Session fields use new env names, trusted origins use AUTH_TRUSTED_ORIGINS, and databaseHooks.user.create.before / update.before added to throw in DEMO mode or return { data: user }.
UI tweaks / NavSidebar
src/layout/manager/nav-sidebar.tsx
Replaced useMatchRoute checks with Link render-prop isActive to determine active state and adjusted JSX accordingly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as SearchInput
  participant Evt as useEffectEvent(onChange)
  participant App as Consumer

  User->>UI: types characters
  UI->>UI: update local state, restart debounce timer
  Note right of UI #f0f9ff: debouncing via useEffectEvent\n(no ref indirection)
  UI-->>Evt: debounce expires
  Evt->>App: onChange(search)
  App->>App: handle search (fetch/update)
Loading
sequenceDiagram
  autonumber
  actor Caller
  participant AuthSvc as auth.databaseHooks
  participant DB as Database

  Caller->>AuthSvc: create/update user request
  AuthSvc->>AuthSvc: if DEMO MODE -> throw
  alt not demo
    AuthSvc->>DB: persist user (return { data })
    DB-->>AuthSvc: persisted user
    AuthSvc-->>Caller: success
  else demo
    AuthSvc-->>Caller: throw DEMO MODE error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

v3, enhancement

Suggested reviewers

  • yoannfleurydev
  • ntatoud
  • DecampsRenan

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is a simple bullet list and does not follow the repository’s template—there are no section headings (e.g., “Describe your changes”), no issue reference, no screenshots or documentation links, and the checklist is missing. Please update the PR description to match the template by adding the “Describe your changes” section with details and issue reference, including screenshots if relevant, linking to any documentation, and completing the provided checklist.
Title Check ❓ Inconclusive The title “V3/update deps” is generic and uses a branch‐style slash, which does not clearly convey the scope or main changes such as migrating to Zod 4 or enabling the React compiler; it reads more like an internal branch name than a descriptive summary. Please rename the title to a concise sentence that highlights the key changes, for example “Migrate to Zod 4, enable React compiler, and update project dependencies.”
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v3/update-deps

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/server/routers/genre.ts (1)

37-37: Incorrect log message (“books” instead of “genres”)

Small mismatch; adjust for clarity.

-      context.logger.info('Getting books from database');
+      context.logger.info('Getting genres from database');
src/server/routers/book.ts (1)

211-215: Wrong permission scope for deleteById (likely security/authorization bug)

Router is “book”, but permission checks “user: ['delete']”. This can allow unintended access or block legit access.

   deleteById: protectedProcedure({
     permission: {
-      user: ['delete'],
+      book: ['delete'],
     },
   })
♻️ Duplicate comments (1)
src/routes/manager/users/index.tsx (1)

11-11: Verify prefault() usage (see related comment in src/env/server.ts).

This uses the same prefault() method that needs verification. See the detailed comment in src/env/server.ts for the verification script.

🧹 Nitpick comments (7)
src/routes/login/verify.index.tsx (1)

10-13: Approve z.email() migration in verify.index.tsx

  • Verified zod@4.1.12 and @tanstack/zod-adapter@1.132.47 support z.email().
  • Confirmed PageLoginVerify still expects email: string.
  • Optional: align with zu.field* helpers for consistency across schemas.
src/components/ui/search-input.tsx (1)

59-62: Consider using useEffect for prop synchronization.

The conditional setSearch call during render is valid for prop synchronization in React, but it's somewhat unconventional and can be harder to reason about. Consider refactoring to use useEffect for clarity:

  const externalValueHasChanged = useValueHasChanged(value);
- if (externalValueHasChanged && value !== search) {
-   setSearch(value ?? '');
- }
+ 
+ useEffect(() => {
+   if (externalValueHasChanged && value !== search) {
+     setSearch(value ?? '');
+   }
+ }, [externalValueHasChanged, value, search]);

This makes the synchronization logic more explicit and groups it with other effects. However, the current implementation is functionally correct.

src/hooks/use-value-has-changed.ts (1)

5-8: Consider using useEffect to avoid lint suppression.

While mutating valueRef.current during render is safe for this specific use case, consider refactoring to use useEffect to avoid suppressing the linting rule, which improves code clarity.

Alternative approach:

 export const useValueHasChanged = (value: unknown) => {
   const valueRef = useRef(value);
+  const prevValueRef = useRef(value);
-  // eslint-disable-next-line react-hooks/refs
-  const valueHasChanged = valueRef.current !== value;
-  // eslint-disable-next-line react-hooks/refs
-  valueRef.current = value;
+  
+  const valueHasChanged = prevValueRef.current !== value;
+  
+  useEffect(() => {
+    prevValueRef.current = value;
+  });
 
   return valueHasChanged;
 };
eslint.config.ts (1)

32-35: Document why these SonarJS rules are disabled.

Four new SonarJS rules have been disabled:

  • prefer-read-only-props
  • function-return-type
  • no-redundant-optional
  • no-useless-intersection

These rules can catch useful code quality issues. If they're producing false positives in your codebase, consider:

  1. Adding inline suppressions for legitimate cases rather than global disables
  2. Documenting why each rule is problematic for your codebase
  3. Creating a custom rule configuration that better fits your patterns

This helps future maintainers understand the reasoning.

src/server/routers/genre.ts (1)

25-27: Align searchTerm handling with book.ts to avoid undefined in Prisma filter

Currently searchTerm is optional without a default but is used in contains. Prefer trimming and defaulting to '' like in book.ts to avoid undefined and keep behavior consistent.

Apply:

-          searchTerm: z.string().optional(),
+          searchTerm: z.string().trim().optional().prefault(''),
src/server/routers/book.ts (1)

134-146: Stricter and consistent validation for genreId (use cuid; optional on create)

  • create: handler treats genreId as optional (?? undefined) but schema requires nonempty string.
  • update: schema allows any nonempty string; should be cuid to match DB id type.
-        .extend({ genreId: z.string().nonempty() })
+        .extend({ genreId: z.cuid().optional() })
-        .extend({ genreId: z.string().nonempty() })
+        .extend({ genreId: z.cuid() })

The create handler line can stay as-is; the schema change makes it coherent:

genreId: input.genreId ?? undefined

Also applies to: 174-182, 189-194

package.json (1)

81-82: Nitro nightly in production dependencies

Pinned to a nightly build. Nightlies can introduce breaking changes unexpectedly.

  • Consider switching to a stable Nitro release before merging to main.
  • Verify peer-dep compatibility and runtime by running e2e and a smoke deploy in a staging env.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7caaef and 00914a5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (54)
  • .github/workflows/code-quality.yml (1 hunks)
  • .storybook/preview.tsx (1 hunks)
  • .vscode/settings.example.json (1 hunks)
  • eslint.config.ts (3 hunks)
  • package.json (1 hunks)
  • src/components/form/docs.stories.tsx (1 hunks)
  • src/components/form/field-checkbox-group/docs.stories.tsx (1 hunks)
  • src/components/form/field-checkbox/docs.stories.tsx (1 hunks)
  • src/components/form/field-checkbox/field-checkbox.browser.spec.tsx (1 hunks)
  • src/components/form/field-number/docs.stories.tsx (1 hunks)
  • src/components/form/field-otp/docs.stories.tsx (1 hunks)
  • src/components/form/field-radio-group/docs.stories.tsx (1 hunks)
  • src/components/form/field-text/docs.stories.tsx (1 hunks)
  • src/components/form/form-test-utils.tsx (2 hunks)
  • src/components/ui/calendar.browser.spec.tsx (0 hunks)
  • src/components/ui/calendar.tsx (1 hunks)
  • src/components/ui/date-input.tsx (1 hunks)
  • src/components/ui/popover.stories.tsx (1 hunks)
  • src/components/ui/search-input.tsx (2 hunks)
  • src/components/ui/sidebar.tsx (2 hunks)
  • src/components/ui/theme-switcher.tsx (1 hunks)
  • src/env/client.ts (1 hunks)
  • src/env/server.ts (2 hunks)
  • src/features/account/schema.ts (1 hunks)
  • src/features/auth/schema.ts (2 hunks)
  • src/features/book/schema.ts (2 hunks)
  • src/features/genre/schema.ts (1 hunks)
  • src/features/user/schema.ts (1 hunks)
  • src/hooks/use-value-has-changed.ts (1 hunks)
  • src/lib/i18n/index.ts (0 hunks)
  • src/lib/zod/zod-utils.ts (1 hunks)
  • src/lib/zod/zod-utils.unit.spec.ts (0 hunks)
  • src/locales/ar/auth.json (1 hunks)
  • src/locales/ar/common.json (1 hunks)
  • src/locales/ar/index.ts (0 hunks)
  • src/locales/en/auth.json (1 hunks)
  • src/locales/en/common.json (1 hunks)
  • src/locales/en/index.ts (0 hunks)
  • src/locales/fr/auth.json (1 hunks)
  • src/locales/fr/book.json (0 hunks)
  • src/locales/fr/common.json (1 hunks)
  • src/locales/fr/index.ts (0 hunks)
  • src/locales/sw/auth.json (1 hunks)
  • src/locales/sw/book.json (0 hunks)
  • src/locales/sw/common.json (1 hunks)
  • src/locales/sw/index.ts (0 hunks)
  • src/routes/app/account.index.tsx (1 hunks)
  • src/routes/login/verify.index.tsx (1 hunks)
  • src/routes/manager/account.index.tsx (1 hunks)
  • src/routes/manager/books/index.tsx (1 hunks)
  • src/routes/manager/users/index.tsx (1 hunks)
  • src/server/routers/book.ts (2 hunks)
  • src/server/routers/genre.ts (1 hunks)
  • src/server/routers/user.ts (2 hunks)
💤 Files with no reviewable changes (9)
  • src/locales/sw/index.ts
  • src/components/ui/calendar.browser.spec.tsx
  • src/lib/zod/zod-utils.unit.spec.ts
  • src/locales/sw/book.json
  • src/locales/ar/index.ts
  • src/lib/i18n/index.ts
  • src/locales/fr/book.json
  • src/locales/fr/index.ts
  • src/locales/en/index.ts
🧰 Additional context used
🧬 Code graph analysis (13)
src/components/ui/popover.stories.tsx (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/components/form/field-radio-group/docs.stories.tsx (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/components/form/docs.stories.tsx (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/server/routers/genre.ts (1)
src/features/genre/schema.ts (1)
  • zGenre (8-15)
src/features/auth/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/features/user/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/components/form/field-text/docs.stories.tsx (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/features/book/schema.ts (2)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/features/genre/schema.ts (1)
  • zGenre (8-15)
src/components/ui/search-input.tsx (1)
src/hooks/use-value-has-changed.ts (1)
  • useValueHasChanged (3-11)
src/components/form/field-otp/docs.stories.tsx (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/server/routers/book.ts (1)
src/features/book/schema.ts (1)
  • zBook (10-19)
src/features/genre/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/features/account/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 🔬 Tests (lts/*)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: Playwright E2E Tests
🔇 Additional comments (42)
src/components/ui/search-input.tsx (2)

51-57: LGTM! Clean debouncing implementation.

The debouncing logic correctly uses useEffectEvent to avoid including the onChange callback in the effect dependencies, which prevents unnecessary effect re-runs when the callback identity changes. The effect will only re-run when search or delay changes, which is the desired behavior.


5-5: useEffectEvent support confirmed – React 19.2.0 includes this hook as a stable API; no further action required.

.github/workflows/code-quality.yml (1)

31-31: LGTM! Stricter CI linting enforced.

Adding --max-warnings 0 is a good practice that prevents warnings from accumulating and ensures consistent code quality across the codebase.

.vscode/settings.example.json (1)

9-9: LGTM! Simplified locales configuration.

Consolidating to a single locales path aligns with the removal of zod-i18n-map dependency and simplifies the i18n setup.

src/components/ui/sidebar.tsx (2)

76-77: LGTM! Appropriate lint suppression for private state.

The eslint-disable is justified here since _open and _setOpen are intentionally prefixed with underscore to indicate internal/private state that can be overridden by props.


636-637: LGTM! Intentional impurity for random skeleton width.

The react-hooks/purity suppression is appropriate here since the intentional use of Math.random() creates visual variety in skeleton loading states, and the randomness doesn't affect functional correctness.

src/locales/sw/common.json (1)

26-28: LGTM! Swahili translation added.

The addition of the "errors.required" key with the Swahili translation "Inahitajika" is consistent with similar updates across other locale files in this PR.

src/routes/manager/books/index.tsx (1)

11-11: prefault() is valid in Zod v4
prefault() is part of the official Zod v4 API (documented and covered by tests), so this change is correct and requires no further action.

src/locales/en/common.json (1)

25-28: LGTM!

The addition of the errors.required localization key is consistent with similar additions across other locale files (ar, fr, sw) and follows proper JSON structure.

src/components/ui/date-input.tsx (1)

36-36: LGTM!

The extended ESLint suppression is a mechanical update aligning with broader lint configuration changes in this PR. The underlying code pattern (updating state in useEffect when dependencies change) is valid for this synchronization use case.

src/components/ui/theme-switcher.tsx (1)

31-31: LGTM!

The extended ESLint suppression is consistent with similar updates across the codebase. The underlying pattern (setting hydration state on mount) is appropriate for detecting client-side rendering.

src/locales/fr/common.json (1)

25-28: LGTM!

The French translation for the required error message is correct and maintains consistency with the structure introduced in other locale files.

src/env/client.ts (3)

31-31: Verify prefault() usage in environment configuration.

Similar to the route changes, this uses prefault() instead of default(). Ensure this is the correct Zod v4 API for default value handling in environment validation.


27-27: No action required: z.url() is correct for Zod v4.
Zod v4 introduces top-level format validators like z.url(), superseding z.string().url().


37-40: Confirm z.emoji() is correct in Zod v4
In Zod v4, z.emoji() is the standalone validator (the old z.string().emoji() still works but is deprecated).

src/locales/ar/common.json (1)

25-28: LGTM!

The Arabic translation for the required error message is properly added and maintains consistency with other locale files.

src/locales/en/auth.json (1)

149-150: LGTM!

The new error code for duplicate user email is clearly worded and follows the existing pattern. The addition is consistent with similar updates across other locale files.

src/locales/ar/auth.json (1)

106-107: LGTM!

The new error code translation is properly formatted and consistent with the existing error messages in the locale file.

src/env/server.ts (2)

11-11: LGTM!

Using z.url() is the idiomatic way to validate URLs in Zod, replacing the more verbose z.string().url().


45-45: Ignore refine property suggestion Zod v4’s refine method uses error (replacing message from v3), so the current code is correct.

Likely an incorrect or invalid review comment.

src/locales/fr/auth.json (1)

119-120: LGTM!

The French translation is properly formatted and consistent with other error messages.

src/locales/sw/auth.json (1)

106-107: LGTM!

The Swahili translation is properly formatted and consistent with the error code additions in other locales.

src/components/form/form-test-utils.tsx (1)

14-14: LGTM!

The updated generic constraint to ZodType<FieldValues> properly aligns with the Zod migration and provides more specific type safety for form schemas.

src/components/ui/calendar.tsx (1)

37-37: LGTM!

The lint suppression is appropriately scoped to the specific line where the Icon component is dynamically selected and rendered. This is a reasonable exception to the static-components rule given the pattern matching logic above.

src/components/ui/popover.stories.tsx (1)

59-62: LGTM! Migration to zu.fieldText.required.

The migration from zu.string.nonEmpty to zu.fieldText.required is consistent with the broader refactor across the codebase. The fieldText helper properly handles empty string transformations and uses standardized error messaging.

src/components/form/field-radio-group/docs.stories.tsx (1)

20-25: LGTM! Consistent migration to zu.fieldText.required.

The validation has been properly updated to use the zu.fieldText.required helper with the standardized error property, maintaining consistency with the broader migration pattern.

src/components/form/field-text/docs.stories.tsx (1)

18-21: LGTM! Validation migration is consistent.

The field validation has been updated to use zu.fieldText.required, aligning with the standardized validation approach across all form fields.

src/server/routers/user.ts (2)

267-273: Same coerce and prefault pattern - verify consistency.

This endpoint uses the same coerce.number().prefault(20) pattern as the previous endpoint. If the verification for the earlier usage confirms correctness, this usage should be fine as well.


24-31: No action required for coerce + prefault defaults The prefault fallback is applied before coercion, and all current usages (user.ts, genre.ts, book.ts, env/server.ts) behave as expected.

src/components/form/docs.stories.tsx (1)

24-28: LGTM! Form validation migrated to fieldText helpers.

The form schema has been properly updated to use zu.fieldText.required() and zu.fieldText.nullish(), leveraging default error messages from the internationalization system. This maintains consistency with the broader validation refactor.

src/components/form/field-checkbox/field-checkbox.browser.spec.tsx (1)

16-18: error is correct for Zod v4 refine
Zod v4 uses the error option for custom refine messages; no change required.

src/routes/app/account.index.tsx (1)

10-14: No action needed: empty string defaults behave identically
Replacing .default('') with .prefault('') has no impact here—"" is already valid for both z.string() and z.enum(['', …]), so the default value passes (or is returned) unchanged. Proceed as is.

src/components/form/field-checkbox-group/docs.stories.tsx (1)

17-17: Direct string argument for .nonempty() is valid. Zod 4 accepts .nonempty('message'), so no change is needed.

Likely an incorrect or invalid review comment.

src/components/form/field-otp/docs.stories.tsx (1)

19-26: LGTM! Clean migration to zu.fieldText API.

The validation correctly migrates to the new zu.fieldText.required() pattern and pipes through additional length constraints. The approach handles empty strings appropriately via the fieldText transform.

src/features/genre/schema.ts (1)

10-11: LGTM! Consistent migration to Zod 4 APIs.

Both changes align with the migration strategy:

  • Using native z.cuid() matches Zod 4's improved type-specific validators
  • Adopting zu.fieldText.required() follows the project-wide pattern for text field validation
src/features/account/schema.ts (1)

10-10: LGTM! Consistent fieldText migration.

The update correctly adopts the zu.fieldText.required() pattern, maintaining the same validation behavior while aligning with the project-wide API changes.

src/features/auth/schema.ts (2)

10-13: LGTM! Consolidated error handling.

The simplified error function correctly distinguishes between missing input (!issue.input) and invalid input, maintaining the same validation messages with cleaner code.


20-27: LGTM! Correct email validation migration.

The migration to zu.fieldText.required().pipe(z.email(...)) properly handles empty strings via the fieldText transform and provides contextual error messages through the error function.

src/features/user/schema.ts (1)

12-19: LGTM! Proper fieldText migration for nullable and required fields.

Both updates correctly apply the migration pattern:

  • name uses zu.fieldText.nullish() for the optional field, transforming empty strings to null
  • email uses zu.fieldText.required().pipe(z.email(...)) with contextual error messages that distinguish missing vs. invalid input
src/features/book/schema.ts (3)

12-12: LGTM! Updated to Zod 4 native CUID validator.

The change to z.cuid() adopts Zod 4's built-in type-specific validator, which is more idiomatic than the previous z.string().cuid() pattern.


13-16: LGTM! Consistent fieldText migration across fields.

All text fields correctly migrate to the appropriate zu.fieldText variants:

  • title and author: zu.fieldText.required() for mandatory fields
  • publisher: zu.fieldText.nullish() for optional fields

Each variant properly handles empty strings according to field optionality.


25-25: LGTM! Consistent validation for form field.

The genreId field correctly adopts zu.fieldText.required(), maintaining consistency with the other required text fields in the schema.

import react from '@eslint-react/eslint-plugin';
import tanstackQuery from '@tanstack/eslint-plugin-query';
import tanstackRouter from '@tanstack/eslint-plugin-router';
import { defineConfig } from 'eslint/config';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Address the TypeScript errors properly rather than suppressing them.

The @ts-expect-error comments indicate type mismatches that are being suppressed rather than resolved. While these may be due to incomplete type definitions in the ESLint packages, consider:

  1. Verifying you're using compatible versions of the ESLint plugins
  2. Checking if newer versions have resolved these type issues
  3. Adding proper type declarations if the packages lack them

Suppressing type errors can hide breaking changes or incorrect usage patterns.

Also applies to: 22-23


🏁 Script executed:

#!/bin/bash
# Check the versions of ESLint packages
echo "=== Checking ESLint package versions ==="
cat package.json | jq '.devDependencies | to_entries | map(select(.key | contains("eslint"))) | from_entries'

Length of output: 459


Use defineFlatConfig and flat.configs.recommended from @eslint/js instead of suppressing TS errors
Replace

import { defineConfig } from 'eslint/config';

with

import { defineFlatConfig, flat } from '@eslint/js';

then update your config to use defineFlatConfig({ extends: [flat.configs.recommended], … }) and remove all @ts-expect-error comments.

🤖 Prompt for AI Agents
In eslint.config.ts around line 5, replace the current import from
'eslint/config' and the use of defineConfig with imports from '@eslint/js' and
defineFlatConfig; specifically import defineFlatConfig and flat from
'@eslint/js', change the exported config to use defineFlatConfig({ extends:
[flat.configs.recommended], ... }) and remove any @ts-expect-error comments
related to the old API; ensure the new shape matches flat config expectations
(top-level fields like ignores, plugins, rules, and overrides) so TypeScript
errors are resolved.

Comment on lines 26 to 28
lovesBears: z.boolean().refine((val) => val === true, {
message: 'Please say you love bears.',
error: 'Please say you love bears.',
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use refine’s message option (not error)

Zod’s refine expects { message }; { error } will be ignored and no message will surface in the form.

-    lovesBears: z.boolean().refine((val) => val === true, {
-      error: 'Please say you love bears.',
-    }),
+    lovesBears: z.boolean().refine((val) => val === true, {
+      message: 'Please say you love bears.',
+    }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lovesBears: z.boolean().refine((val) => val === true, {
message: 'Please say you love bears.',
error: 'Please say you love bears.',
}),
lovesBears: z.boolean().refine((val) => val === true, {
message: 'Please say you love bears.',
}),
🤖 Prompt for AI Agents
In src/components/form/field-checkbox/docs.stories.tsx around lines 26 to 28,
the Zod refine call uses the invalid option { error: 'Please say you love
bears.' } which is ignored; replace it with the correct option { message:
'Please say you love bears.' } so the validation message surfaces in the form.

Comment on lines 13 to 14
SESSION_EXPIRATION_IN_SECONDS: z.coerce.number().int().prefault(2592000), // 30 days by default
SESSION_UPDATE_AGE_IN_SECONDS: z.coerce.number().int().prefault(86400), // 1 day by default
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that prefault() is a valid Zod method.

The migration from default() to prefault() is used throughout the codebase, but prefault() does not appear in standard Zod documentation or the retrieved learnings. This could be:

  1. A typo (should be default()?)
  2. A custom extension defined in the codebase
  3. An undocumented Zod 4 feature

Please verify this is correct, as using an undefined method would cause runtime errors.

Run the following script to check if prefault is defined in the codebase:

Also applies to: 25-25, 28-28


🏁 Script executed:

#!/bin/bash
# Search for prefault definition or import in the codebase
echo "=== Searching for prefault definition or declaration ==="
rg -n "prefault\s*[:=]" --type ts --type tsx

echo -e "\n=== Searching for prefault in dependencies ==="
rg -n "prefault" node_modules/zod/lib -A 2 2>/dev/null || echo "Not found in zod package"

echo -e "\n=== Checking if it's a custom extension ==="
ast-grep --pattern 'prefault($$$) { $$$ }'

Length of output: 202


Replace prefault() with a valid default or define it via a Zod extension
.prefault() isn’t defined in the codebase or Zod package and will break at runtime. Either rename these calls to .default() or add the corresponding prefault() extension before use.

🤖 Prompt for AI Agents
In src/env/server.ts around lines 13 to 14, the schema uses a non-existent
.prefault() which will throw at runtime; replace those calls with
.default(2592000) and .default(86400) respectively so the lines read
z.coerce.number().int().default(2592000) and
z.coerce.number().int().default(86400); alternatively, if you intentionally need
a custom prefault behavior, add and register a Zod extension that implements
prefault() before using it.

Copy link
Member Author

Choose a reason for hiding this comment

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

zod v4

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

What is the prefault() method in Zod 4? How does it differ from default() in Zod?

💡 Result:

Short answer: prefault is a "pre-parse default" that is applied by inserting a fallback into the input before the inner schema runs; default short‑circuits and returns the default without running the inner schema.

Details and examples

  • .default(value): if the input === undefined, Zod 4 returns value immediately (parsing is short‑circuited). The default must be assignable to the schema's output type. [1][2]
  • .prefault(value): if the input === undefined, Zod will replace the input with value and then run the inner schema as usual (so transforms/checks on the schema will run against the prefaulted value). The prefault value must be assignable to the schema's input type. This reproduces Zod 3's old behavior. Example pattern shown in Zod discussions:
    const params = z.object({ limit: z.number().default(0) });
    const prefaulted = params.prefault({});
    prefaulted.parse(undefined); // => { limit: 0 } (inner parsing runs). [2][3]

When to use which

  • Use .default when you want a cheap, final fallback and you don't want the inner schema to run on the fallback.
  • Use .prefault when you want the default to be parsed/validated/transformed by the inner schema (i.e., "fill then parse").

Sources
[1] Zod v4 changelog / migration notes (explains default short‑circuit change and prefault concept).
[2] GitHub discussion/examples showing prefault pattern and why default changed.
[3] "Introducing Zod Codecs" (notes default/prefault behavior in v4).


@ivan-dalmet, thank you for the clarification! You're absolutely right. I've confirmed that prefault() is indeed a valid Zod v4 method.

For context, prefault() differs from default() in Zod v4:

  • .default(value): Short-circuits parsing and returns the default immediately when input is undefined (the default value is never validated/transformed)
  • .prefault(value): Replaces undefined input with the fallback value, then runs the full schema validation/transformation on it (reproduces Zod 3's old default() behavior)

The usage in this file is correct – prefault() ensures the default values (2592000 and 86400) still go through the .coerce.number().int() validation chain. Apologies for the false positive! 🎉


✏️ Learnings added
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/env/server.ts:13-14
Timestamp: 2025-10-09T08:30:25.898Z
Learning: In Zod v4, `prefault()` is a valid method that acts as a "pre-parse default". It differs from `default()` by applying the fallback value before the inner schema runs, whereas `default()` short-circuits parsing. Usage like `z.coerce.number().int().prefault(2592000)` is correct and ensures the default value goes through the validation chain.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment on lines 12 to 49
export const zu = {
string: {
nonEmpty(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return s
.trim()
.min(
1,
options.required_error ??
t('zod:errors.invalid_type_received_undefined')
);
},
nonEmptyNullable(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return z
.literal('')
.transform(() => null)
.or(zu.string.nonEmpty(s, options).nullable());
},
nonEmptyNullish(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return z
.literal('')
.transform(() => null)
.or(zu.string.nonEmpty(s, options).nullish());
},
email(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string
.nonEmpty(s.toLowerCase(), options)
.email(options.invalid_type_error);
},
emailNullable(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string.nonEmptyNullable(zu.string.email(s, options), options);
},
emailNullish(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string.nonEmptyNullish(zu.string.email(s, options), options);
},
},
array: {
nonEmpty<T extends Schema>(a: ZodArray<T>, message?: string) {
return a.min(
1,
message ?? t('zod:errors.invalid_type_received_undefined')
);
},
nonEmptyNullable<T extends Schema>(a: ZodArray<T>, message?: string) {
return a
.length(0)
.transform(() => null)
.or(zu.array.nonEmpty(a, message).nullable());
},
nonEmptyNullish<T extends Schema>(a: ZodArray<T>, message?: string) {
return a
.length(0)
.transform(() => null)
.or(zu.array.nonEmpty(a, message).nullish());
},
fieldText: {
required: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
nullable: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullable()
.pipe(z.string(params).nullable()),
nullish: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(params).nullish()),
optional: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsUndefined)
.optional()
.pipe(z.string(params).optional()),
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Map custom { error } to Zod’s expected params; fix type and message wiring

z.string doesn’t accept an error property; it expects required_error/invalid_type_error (or errorMap). Current default { error: ... } will not work.

Apply:

-import z from 'zod';
+import z from 'zod';

-const emptyStringAsNull = (input: string) =>
-  // Cast null value to string for React Hook Form inference
+const emptyStringAsNull = (input: string) =>
+  // Cast null value to string for React Hook Form inference
   input.trim() === '' ? (null as unknown as string) : input.trim();

-const emptyStringAsUndefined = (input: string) =>
-  // Cast null value to string for React Hook Form inference
+const emptyStringAsUndefined = (input: string) =>
+  // Cast undefined value to string for React Hook Form inference
   input.trim() === '' ? (undefined as unknown as string) : input.trim();

 export const zu = {
-  fieldText: {
-    required: (
-      params: Parameters<typeof z.string>[0] = {
-        error: t('common:errors.required'),
-      }
-    ) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
-    nullable: (
-      params: Parameters<typeof z.string>[0] = {
-        error: t('common:errors.required'),
-      }
-    ) =>
-      z
-        .string(params)
-        .transform(emptyStringAsNull)
-        .nullable()
-        .pipe(z.string(params).nullable()),
-    nullish: (
-      params: Parameters<typeof z.string>[0] = {
-        error: t('common:errors.required'),
-      }
-    ) =>
-      z
-        .string(params)
-        .transform(emptyStringAsNull)
-        .nullish()
-        .pipe(z.string(params).nullish()),
-    optional: (
-      params: Parameters<typeof z.string>[0] = {
-        error: t('common:errors.required'),
-      }
-    ) =>
-      z
-        .string(params)
-        .transform(emptyStringAsUndefined)
-        .optional()
-        .pipe(z.string(params).optional()),
-  },
+  fieldText: {
+    required: (opts?: { error?: string }) => {
+      const p = opts?.error
+        ? { required_error: opts.error, invalid_type_error: opts.error }
+        : undefined;
+      return z.string(p).transform(emptyStringAsNull).pipe(z.string(p));
+    },
+    nullable: (opts?: { error?: string }) => {
+      const p = opts?.error
+        ? { required_error: opts.error, invalid_type_error: opts.error }
+        : undefined;
+      return z.string(p).transform(emptyStringAsNull).nullable().pipe(z.string(p).nullable());
+    },
+    nullish: (opts?: { error?: string }) => {
+      const p = opts?.error
+        ? { required_error: opts.error, invalid_type_error: opts.error }
+        : undefined;
+      return z.string(p).transform(emptyStringAsNull).nullish().pipe(z.string(p).nullish());
+    },
+    optional: (opts?: { error?: string }) => {
+      const p = opts?.error
+        ? { required_error: opts.error, invalid_type_error: opts.error }
+        : undefined;
+      return z.string(p).transform(emptyStringAsUndefined).optional().pipe(z.string(p).optional());
+    },
+  },
 };

Additionally, consider standardizing on import { z } from 'zod' across the repo to avoid relying on default export nuances.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const zu = {
string: {
nonEmpty(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return s
.trim()
.min(
1,
options.required_error ??
t('zod:errors.invalid_type_received_undefined')
);
},
nonEmptyNullable(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return z
.literal('')
.transform(() => null)
.or(zu.string.nonEmpty(s, options).nullable());
},
nonEmptyNullish(
s: ZodString,
options: {
required_error?: string;
} = {}
) {
return z
.literal('')
.transform(() => null)
.or(zu.string.nonEmpty(s, options).nullish());
},
email(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string
.nonEmpty(s.toLowerCase(), options)
.email(options.invalid_type_error);
},
emailNullable(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string.nonEmptyNullable(zu.string.email(s, options), options);
},
emailNullish(
s: ZodString,
options: {
required_error?: string;
invalid_type_error?: string;
} = {}
) {
return zu.string.nonEmptyNullish(zu.string.email(s, options), options);
},
},
array: {
nonEmpty<T extends Schema>(a: ZodArray<T>, message?: string) {
return a.min(
1,
message ?? t('zod:errors.invalid_type_received_undefined')
);
},
nonEmptyNullable<T extends Schema>(a: ZodArray<T>, message?: string) {
return a
.length(0)
.transform(() => null)
.or(zu.array.nonEmpty(a, message).nullable());
},
nonEmptyNullish<T extends Schema>(a: ZodArray<T>, message?: string) {
return a
.length(0)
.transform(() => null)
.or(zu.array.nonEmpty(a, message).nullish());
},
fieldText: {
required: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
nullable: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullable()
.pipe(z.string(params).nullable()),
nullish: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(params).nullish()),
optional: (
params: Parameters<typeof z.string>[0] = {
error: t('common:errors.required'),
}
) =>
z
.string(params)
.transform(emptyStringAsUndefined)
.optional()
.pipe(z.string(params).optional()),
},
import z from 'zod';
const emptyStringAsNull = (input: string) =>
// Cast null value to string for React Hook Form inference
input.trim() === '' ? (null as unknown as string) : input.trim();
const emptyStringAsUndefined = (input: string) =>
// Cast undefined value to string for React Hook Form inference
input.trim() === '' ? (undefined as unknown as string) : input.trim();
export const zu = {
fieldText: {
required: (opts?: { error?: string }) => {
const p = opts?.error
? { required_error: opts.error, invalid_type_error: opts.error }
: undefined;
return z.string(p).transform(emptyStringAsNull).pipe(z.string(p));
},
nullable: (opts?: { error?: string }) => {
const p = opts?.error
? { required_error: opts.error, invalid_type_error: opts.error }
: undefined;
return z
.string(p)
.transform(emptyStringAsNull)
.nullable()
.pipe(z.string(p).nullable());
},
nullish: (opts?: { error?: string }) => {
const p = opts?.error
? { required_error: opts.error, invalid_type_error: opts.error }
: undefined;
return z
.string(p)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(p).nullish());
},
optional: (opts?: { error?: string }) => {
const p = opts?.error
? { required_error: opts.error, invalid_type_error: opts.error }
: undefined;
return z
.string(p)
.transform(emptyStringAsUndefined)
.optional()
.pipe(z.string(p).optional());
},
},
};

Comment on lines +11 to +12
state: z.enum(['', 'change-name']).prefault(''),
newEmail: z.string().prefault(''),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the Zod v4 API and prefault() method.

The change from .default('') to .prefault('') suggests a Zod v4 API change. Please verify that:

  1. Zod v4 has been released and is the version being used
  2. prefault() is the correct Zod v4 method for default values
  3. The behavior is equivalent to v3's default()

🌐 Web query:

What is the latest version of Zod and does Zod v4 introduce a prefault() method to replace default()?

💡 Result:

As of Oct 9, 2025 the Zod 4 release line is the current stable major (latest published release in the 4.x series is v4.1.12). [1]

No — Zod v4 does not add a prefault() API. Zod 4 intentionally changed how .default() behaves (it short-circuits and returns the default rather than "prefaulting" and reparsing the default); the maintainer notes that the old "prefault" behavior was removed and can be reproduced with z.transform if needed. [2][3]

Sources:

  • Latest Zod v4 releases (release listings). [1]
  • GitHub discussion explaining removal of the "prefault" behavior and how to emulate it with z.transform. [2]
  • Zod v4 docs / announcement. [3]

Replace .prefault() with .default() for Zod v4 compatibility
prefault() was removed in v4; use .default('') (short-circuits and returns the default) or, to emulate the old “prefault” behavior, wrap in z.transform.

🤖 Prompt for AI Agents
In src/routes/manager/account.index.tsx around lines 11 to 12, the schema uses
the removed Zod v4 method `.prefault('')`; replace `.prefault('')` with
`.default('')` on both fields (e.g., state: z.enum(['',
'change-name']).default('') and newEmail: z.string().default('')), and if you
need the old “prefault” transformation behavior instead of a simple default,
wrap the schema in z.transform to apply the transformation after parsing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/features/auth/schema.ts (1)

40-40: Use zu.fieldText.required() for consistency and Zod v4 compatibility.

The name field uses z.string().nonempty(), which:

  1. Is inconsistent with the email field in this same file (line 17) that uses zu.fieldText.required()
  2. Uses .nonempty() which is deprecated in Zod v4 in favor of .min(1) or built-in error messages
  3. Lacks the empty-string-to-null transformation that fieldText provides for React Hook Form compatibility

Apply this diff for consistency:

-    name: z.string().nonempty(),
+    name: zu.fieldText.required(),
🧹 Nitpick comments (3)
src/lib/zod/zod-utils.ts (2)

2-2: Standardize on named import for consistency.

The codebase uses named imports (import { z } from 'zod') elsewhere (e.g., src/features/user/schema.ts, src/features/auth/schema.ts), but this file uses a default import.

Apply this diff to align with the rest of the codebase:

-import z from 'zod';
+import { z } from 'zod';

8-10: Fix the comment to accurately describe undefined casting.

The comment on line 9 states "Cast null value to string" but the function returns undefined, not null.

Apply this diff to fix the comment:

 const emptyStringAsUndefined = (input: string) =>
-  // Cast null value to string for React Hook Form inference
+  // Cast undefined value to string for React Hook Form inference
   input.trim() === '' ? (undefined as unknown as string) : input.trim();
src/features/auth/schema.ts (1)

9-12: Consider consistent error parameter style.

This code uses the object syntax z.string({ error: ... }) while the zu.fieldText helpers in src/lib/zod/zod-utils.ts (lines 15, 18, 26, 34) pass error strings directly. Both are valid in Zod v4, but consistency across the codebase would improve maintainability.

If you prefer consistency, apply this diff:

-  z
-    .string({
-      error: t('auth:common.otp.required'),
-    })
+  z
+    .string(t('auth:common.otp.required'))
     .length(6, t('auth:common.otp.invalidLength'));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00914a5 and e43baa7.

📒 Files selected for processing (3)
  • src/features/auth/schema.ts (2 hunks)
  • src/features/user/schema.ts (1 hunks)
  • src/lib/zod/zod-utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/features/user/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
src/features/auth/schema.ts (1)
src/lib/zod/zod-utils.ts (1)
  • zu (12-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 🔬 Tests (lts/*)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: Playwright E2E Tests
🔇 Additional comments (3)
src/lib/zod/zod-utils.ts (1)

12-42: LGTM: Past review concerns addressed!

The past review flagged that z.string doesn't accept an { error: ... } object in earlier Zod versions. The current implementation correctly passes strings directly as parameters (lines 15, 18, 26, 34), which is valid Zod v4 syntax where z.string('error message') sets the error message for all string validations.

The fieldText helpers properly:

  • Transform empty strings to null/undefined for React Hook Form inference
  • Chain with .pipe() to enforce nullability constraints (required/nullable/nullish/optional)
  • Apply consistent error messaging across the codebase
src/features/user/schema.ts (1)

12-20: LGTM: Clean migration to fieldText pattern.

The schema correctly adopts the new zu.fieldText helpers:

  • name uses nullish() for optional handling with empty-string normalization
  • email uses required() chained with z.email() and provides contextual error messages by checking issue.input to distinguish between missing input (required error) and invalid format (invalid error)

This pattern is consistent with the broader PR migration strategy.

src/features/auth/schema.ts (1)

17-24: LGTM: Email field correctly migrated to fieldText pattern.

The email validation properly uses zu.fieldText.required() chained with z.email(), and the error handler appropriately distinguishes between missing input and invalid format by checking issue.input.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/lib/zod/zod-utils.ts (1)

14-40: Wrap default parameter in { error: ... } object for Zod v4.

The default parameter value is a plain string, but Zod v4's z.string() expects an object with an error property. The current code will fail at runtime because it passes the string directly instead of wrapping it.

Based on learnings: In Zod v4, use the unified error parameter (e.g., z.string({ error: 'Required' })) instead of the older Zod v3 pattern (required_error, invalid_type_error).

Apply this diff to fix all four methods:

   fieldText: {
     required: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
+      params: Parameters<typeof z.string>[0] = { error: t('common:errors.required') }
     ) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
     nullable: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
+      params: Parameters<typeof z.string>[0] = { error: t('common:errors.required') }
     ) =>
       z
         .string(params)
         .transform(emptyStringAsNull)
         .nullable()
         .pipe(z.string(params).nullable()),
     nullish: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
+      params: Parameters<typeof z.string>[0] = { error: t('common:errors.required') }
     ) =>
       z
         .string(params)
         .transform(emptyStringAsNull)
         .nullish()
         .pipe(z.string(params).nullish()),
     optional: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
+      params: Parameters<typeof z.string>[0] = { error: t('common:errors.required') }
     ) =>
       z
         .string(params)
         .transform(emptyStringAsUndefined)
         .optional()
         .pipe(z.string(params).optional()),
   },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e43baa7 and bdbdbe5.

📒 Files selected for processing (3)
  • src/features/auth/schema.ts (2 hunks)
  • src/lib/zod/zod-utils.ts (1 hunks)
  • src/server/routers/book.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/auth/schema.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T08:29:28.532Z
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/components/form/field-number/docs.stories.tsx:23-23
Timestamp: 2025-10-09T08:29:28.532Z
Learning: In Zod v4, the unified `error` parameter is used for custom error messages. For example, `z.number({ error: 'Required' })` is the correct syntax, replacing the older Zod v3 pattern of `required_error` and `invalid_type_error`.

Applied to files:

  • src/lib/zod/zod-utils.ts
🧬 Code graph analysis (1)
src/server/routers/book.ts (1)
src/features/book/schema.ts (2)
  • zBook (10-19)
  • zFormFieldsBook (22-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: Playwright E2E Tests
🔇 Additional comments (5)
src/server/routers/book.ts (5)

4-4: LGTM! Good addition of shared schema import.

Adding zFormFieldsBook enables better schema reuse across routes, reducing duplication.


30-36: LGTM! Consistent use of z.cuid() shorthand.

The output schema correctly uses the simplified z.cuid() syntax, matching the input cursor field.


96-100: LGTM! Correct migration to z.cuid() shorthand.

The input schema correctly uses the simplified z.cuid() syntax for the id field.


127-127: LGTM! Good use of shared schema.

Replacing the inline .pick() with zFormFieldsBook() promotes code reuse and maintainability.


21-29: Approve prefault() usage: prefault provides Zod 3–compatible pre-parse defaults, so this migration is correct.

})
.extend({ genreId: z.string().nonempty() })
)
.input(zFormFieldsBook().extend({ id: z.string() }))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix inconsistent id type validation.

The id field uses z.string() here, but other routes use z.cuid():

  • Line 98 (getById): z.cuid()
  • Lines 205-207 (deleteById): z.cuid() (via zBook().pick({ id: true }))
  • Schema definition: zBook() has id: z.cuid()

This inconsistency allows updateById to accept any string, not just valid CUIDs, potentially passing invalid IDs to the database handler.

Apply this diff to fix the inconsistency:

-    .input(zFormFieldsBook().extend({ id: z.string() }))
+    .input(zFormFieldsBook().extend({ id: z.cuid() }))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.input(zFormFieldsBook().extend({ id: z.string() }))
.input(zFormFieldsBook().extend({ id: z.cuid() }))
🤖 Prompt for AI Agents
In src/server/routers/book.ts around line 165, the updateById input uses
.input(zFormFieldsBook().extend({ id: z.string() })) which allows any string for
id; change it to use z.cuid() to match other routes and the zBook() schema by
replacing z.string() with z.cuid(), or better yet extend using zBook().pick({
id: true }) or reuse the existing id schema so updateById only accepts valid
CUIDs before calling the database handler.

@ivan-dalmet ivan-dalmet marked this pull request as ready for review October 9, 2025 09:40
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/routers/genre.ts (1)

50-58: Fix pagination: add skip and deterministic tie-breaker

Without skip, the cursor item reappears on the next page; ordering only by name can be unstable. Add skip and a secondary id sort.

-        context.db.genre.findMany({
-          // Get an extra item at the end which we'll use as next cursor
-          take: input.limit + 1,
-          cursor: input.cursor ? { id: input.cursor } : undefined,
-          orderBy: {
-            name: 'asc',
-          },
-          where,
-        }),
+        context.db.genre.findMany({
+          // Get an extra item at the end which we'll use as next cursor
+          take: input.limit + 1,
+          cursor: input.cursor ? { id: input.cursor } : undefined,
+          skip: input.cursor ? 1 : 0,
+          orderBy: [{ name: 'asc' }, { id: 'asc' }],
+          where,
+        }),
♻️ Duplicate comments (1)
src/server/routers/book.ts (1)

165-165: Past review comment is outdated but underlying concern remains valid.

The previous review flagged the use of z.string() instead of z.cuid() for inconsistency with other routes. However, the zBook() schema has since been updated to use id: z.string() (see src/features/book/schema.ts:11), making this change consistent across all routes.

The underlying concern about accepting any string for IDs instead of validated CUIDs remains valid. Verify this is intentional (see comment on line 98).

🧹 Nitpick comments (1)
src/server/routers/genre.ts (1)

23-23: Cursor validation relaxed — consider minimal guard or ID-shape check

Dropping .cuid allows any string; Prisma will throw if the cursor record is missing/invalid. At least prevent empty strings (and, if applicable, restore an ID-shape check).

-          cursor: z.string().optional(),
+          cursor: z.string().trim().min(1).optional(),

If your ids are cuid2/uuid, prefer the corresponding validator instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03082bc and f289b7f.

📒 Files selected for processing (3)
  • src/components/form/field-checkbox-group/docs.stories.tsx (1 hunks)
  • src/server/routers/book.ts (6 hunks)
  • src/server/routers/genre.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/form/field-checkbox-group/docs.stories.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/server/routers/genre.ts (1)
src/features/genre/schema.ts (1)
  • zGenre (8-15)
src/server/routers/book.ts (1)
src/features/book/schema.ts (2)
  • zBook (10-19)
  • zFormFieldsBook (22-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: Playwright E2E Tests
🔇 Additional comments (8)
src/server/routers/book.ts (5)

4-4: LGTM - Import addition aligns with schema refactoring.

The addition of zFormFieldsBook import is necessary for the updated create and updateById route inputs and aligns with the schema consolidation.


98-98: Verify ID validation weakening is intentional.

The id field validation changed from z.string().cuid() to z.string(), which is now consistent with the updated zBook() schema (see src/features/book/schema.ts:11). However, this allows any string as an ID instead of requiring valid CUIDs.

Confirm this is part of the intentional Zod 4 migration strategy and not an oversight. If database IDs are still CUIDs, consider retaining CUID validation to catch invalid inputs early.


127-127: LGTM - Schema refactoring improves reusability.

Using zFormFieldsBook() consolidates the input schema and improves maintainability by reusing the shared form fields schema defined in src/features/book/schema.ts:21-24.


196-196: LGTM - Permission correction improves security.

The change from user: ['delete'] to book: ['delete'] correctly aligns the permission check with the operation. Deleting a book should require book deletion rights, not user deletion rights.


25-26: Replace .prefault() with .default() in Zod schemas
Zod 4 removed .prefault(); use .default() (short-circuits parsing) or, if you need pre-parse defaults, apply a .transform(v => v ?? default)/.pipe() after the schema.
Locations: src/server/routers/book.ts lines 25–26, 28

⛔ Skipped due to learnings
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/env/server.ts:13-14
Timestamp: 2025-10-09T08:30:25.898Z
Learning: In Zod v4, `prefault()` is a valid method that acts as a "pre-parse default". It differs from `default()` by applying the fallback value before the inner schema runs, whereas `default()` short-circuits parsing. Usage like `z.coerce.number().int().prefault(2592000)` is correct and ensures the default value goes through the validation chain.
src/server/routers/genre.ts (3)

24-24: Defaults migration LGTM

.prefault(20) and .prefault({}) read well and align with the zod v4 changes.

Also applies to: 27-27


32-32: Output nextCursor type change is consistent

Aligns with input cursor and zGenre id: string. Looks good.


37-37: Corrected log message

Accurate and clearer. LGTM.

cursor: z.string().cuid().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
searchTerm: z.string().trim().optional().default(''),
cursor: z.string().optional(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify weakened cursor validation is intentional.

The removal of .cuid() validation on cursor fields means the API now accepts any string instead of requiring valid CUIDs. Since line 64 uses input.cursor directly in the Prisma where clause { id: input.cursor }, accepting invalid cursor formats could cause runtime errors or database query failures.

Confirm this change is intentional and that the database ID column does not enforce CUID format.

Also applies to: 33-33


🏁 Script executed:

#!/bin/bash
# Check if Book.id field is defined as a CUID type in the Prisma schema

fd -e prisma -x cat {} \; | rg -n -A2 -B2 'model Book'

Length of output: 109


Reinstate CUID validation on cursor and correct .prefault() usage

  • cursor fields (lines 24, 33): use z.string().cuid().optional() to match Prisma’s @default(cuid())
  • replace all .prefault() calls with .default() (valid Zod API)
🤖 Prompt for AI Agents
In src/server/routers/book.ts around lines 24 and 33, the cursor schema should
reinstate CUID validation and any misspelled Zod default usage must be fixed:
change cursor fields from z.string().optional() to z.string().cuid().optional()
to match Prisma @default(cuid()), and replace all occurrences of the invalid
.prefault() method with the correct Zod .default() API, ensuring default values
are provided to .default(...) where appropriate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f289b7f and d8a341c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • package.json (1 hunks)
  • src/locales/ar/index.ts (1 hunks)
  • src/locales/en/index.ts (1 hunks)
  • src/locales/fr/index.ts (1 hunks)
  • src/locales/sw/index.ts (1 hunks)
  • vite.config.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • vite.config.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: 🔬 Tests (lts/*)
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: Playwright E2E Tests
🔇 Additional comments (8)
src/locales/ar/index.ts (2)

3-13: LGTM: JSON import syntax modernized.

Switching from assert { type: 'json' } to with { type: 'json' } aligns with the Stage 4 ECMAScript standard for import attributes. The deprecated assert syntax is correctly replaced.


15-27: Safe to remove zod export – searches show no imports or references to locale.zod in the codebase.

src/locales/en/index.ts (2)

3-13: LGTM: Import attributes modernized.

The JSON imports correctly adopt the with { type: 'json' } syntax, replacing the deprecated assert syntax. This is the standard import attribute syntax.


15-27: No .zod references remain. Removal of the zod export from src/locales/en/index.ts does not break existing code.

src/locales/fr/index.ts (2)

3-13: LGTM: JSON imports updated to standard syntax.

The migration from assert to with { type: 'json' } correctly adopts the Stage 4 import attributes specification.


15-27: zod removal from fr locale is safe. No code imports or accesses fr.zod.

src/locales/sw/index.ts (2)

3-13: LGTM: Import syntax standardized.

The JSON imports correctly use with { type: 'json' }, replacing the deprecated assert syntax. This change aligns with the ECMAScript standard.


15-27: No remaining zod usage in sw locale Verified absence of any .zod references across all TS/TSX files; consumers have migrated away.

Comment on lines +63 to +67
"@tanstack/react-query-devtools": "5.90.2",
"@tanstack/react-router": "1.132.47",
"@tanstack/react-router-devtools": "1.132.51",
"@tanstack/react-start": "1.132.51",
"@tanstack/zod-adapter": "1.132.47",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align TanStack router packages to the same patch version.

@tanstack/react-start@1.132.51 and @tanstack/react-router-devtools@1.132.51 will each pull in @tanstack/react-router@1.132.51, while the root dependency is pinned to 1.132.47. PNPM will therefore install two copies of @tanstack/react-router, splitting router singletons (context, store, etc.) and causing runtime breakage. Please bump the root @tanstack/react-router to 1.132.51 (or downgrade the others) so the stack dedupes to one build.

🤖 Prompt for AI Agents
In package.json around lines 63 to 67, the @tanstack/react-router root
dependency is pinned to 1.132.47 while other TanStack packages in the same block
are at 1.132.51, which will cause PNPM to install two copies of
@tanstack/react-router and split router singletons; update the root
"@tanstack/react-router" version to "1.132.51" (or alternatively downgrade the
other two to 1.132.47) so all TanStack router-related packages use the same
patch version and dedupe to a single build.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (6)
src/server/routers/book.ts (4)

98-98: Duplicate: ID validation consistency concern.

As flagged in the previous review, this should potentially use .cuid() if the database enforces CUID format for IDs.


24-24: Duplicate: Cursor validation weakened.

As flagged in the previous review, removing .cuid() validation allows any string format for cursor, which could cause database errors if the ID column enforces CUID format.


33-33: Duplicate: Output cursor validation weakened.

Same cursor validation issue as input cursor.


165-165: Duplicate: ID validation inconsistency in updateById.

As flagged in the previous review, the id field should use .cuid() for consistency with the database ID format.

src/lib/zod/zod-utils.ts (2)

17-24: Fix type mismatch in params default value.

Same issue as required: the default value is a string but should be an object or undefined.

Apply this diff:

-    nullable: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
-    ) =>
+    nullable: (params?: Parameters<typeof z.string>[0]) =>
       z
         .string(params)
         .transform(emptyStringAsNull)
         .nullable()
         .pipe(z.string(params).nullable()),

33-40: Fix type mismatch in params default value.

Same issue as the other methods.

Apply this diff:

-    optional: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
-    ) =>
+    optional: (params?: Parameters<typeof z.string>[0]) =>
       z
         .string(params)
         .transform(emptyStringAsUndefined)
         .optional()
         .pipe(z.string(params).optional()),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8a341c and cbf25ba.

📒 Files selected for processing (5)
  • src/layout/manager/nav-sidebar.tsx (1 hunks)
  • src/lib/zod/zod-utils.ts (1 hunks)
  • src/server/routers/book.ts (7 hunks)
  • src/server/routers/genre.ts (2 hunks)
  • src/server/routers/user.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/routers/user.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T08:29:28.532Z
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/components/form/field-number/docs.stories.tsx:23-23
Timestamp: 2025-10-09T08:29:28.532Z
Learning: In Zod v4, the unified `error` parameter is used for custom error messages. For example, `z.number({ error: 'Required' })` is the correct syntax, replacing the older Zod v3 pattern of `required_error` and `invalid_type_error`.

Applied to files:

  • src/lib/zod/zod-utils.ts
🧬 Code graph analysis (2)
src/server/routers/genre.ts (1)
src/features/genre/schema.ts (1)
  • zGenre (8-15)
src/server/routers/book.ts (1)
src/features/book/schema.ts (2)
  • zBook (10-19)
  • zFormFieldsBook (22-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Playwright E2E Tests
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: 🔬 Tests (22)
🔇 Additional comments (6)
src/server/routers/genre.ts (3)

37-37: LGTM! Log message corrected.

Good catch fixing the log message from "Getting books from database" to "Getting genres from database".


46-59: LGTM! Parallel execution improves performance.

Migrating from sequential/transactional execution to Promise.all parallelization is a solid performance improvement. Both operations are read-only (count and findMany), making parallelization safe with no risk of race conditions or consistency issues.


23-23: Verify cursor validation relaxation is intentional

The cursor in src/server/routers/genre.ts (and similarly in other routers) now uses z.string().optional(), accepting any string. Since your Prisma IDs are defined with @default(cuid()), confirm you intended to drop .cuid() validation or if you should reinstate z.string().cuid().optional().

src/layout/manager/nav-sidebar.tsx (1)

1-1: ‘use no memo’ directive is correct: Verified per React Compiler docs that "use no memo" is the proper syntax to opt out of compiler optimizations in React 19+.

src/server/routers/book.ts (2)

57-71: Approve transaction to Promise.all refactor for read-only queries.

The change from $transaction to Promise.all is appropriate here since both operations are read-only. Promise.all allows concurrent execution and may improve performance without sacrificing correctness.


25-28: Invalid Zod method .prefault() will cause runtime errors.

Zod does not have a .prefault() method. The correct method is .default(). This will cause immediate errors when the schema is evaluated.

Apply this diff to fix all occurrences:

-          limit: z.coerce.number().int().min(1).max(100).prefault(20),
-          searchTerm: z.string().trim().optional().prefault(''),
+          limit: z.coerce.number().int().min(1).max(100).default(20),
+          searchTerm: z.string().trim().optional().default(''),
         })
-        .prefault({})
+        .default({})
⛔ Skipped due to learnings
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/env/server.ts:13-14
Timestamp: 2025-10-09T08:30:25.898Z
Learning: In Zod v4, `prefault()` is a valid method that acts as a "pre-parse default". It differs from `default()` by applying the fallback value before the inner schema runs, whereas `default()` short-circuits parsing. Usage like `z.coerce.number().int().prefault(2592000)` is correct and ensures the default value goes through the validation chain.

Comment on lines +14 to +16
required: (
params: Parameters<typeof z.string>[0] = t('common:errors.required')
) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix type mismatch in params default value.

The default value t('common:errors.required') is a string, but Parameters<typeof z.string>[0] expects an object like { error?: string } in Zod v4. This mismatch will cause type errors.

Based on learnings

Apply this diff to fix the type:

-    required: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
-    ) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
+    required: (params?: Parameters<typeof z.string>[0]) =>
+      z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
required: (
params: Parameters<typeof z.string>[0] = t('common:errors.required')
) => z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
required: (params?: Parameters<typeof z.string>[0]) =>
z.string(params).transform(emptyStringAsNull).pipe(z.string(params)),
🤖 Prompt for AI Agents
In src/lib/zod/zod-utils.ts around lines 14 to 16, the default value for params
is a plain string but Parameters<typeof z.string>[0] expects an object; replace
the string default with an object that matches Zod's string params shape (e.g. {
message: t('common:errors.required') }) so the call becomes z.string(params)
with params typed correctly, and update any imports/types if necessary to ensure
the object shape satisfies Zod v4's expected parameter type.

Comment on lines +25 to +32
nullish: (
params: Parameters<typeof z.string>[0] = t('common:errors.required')
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(params).nullish()),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix type mismatch in params default value.

Same issue as required and nullable.

Apply this diff:

-    nullish: (
-      params: Parameters<typeof z.string>[0] = t('common:errors.required')
-    ) =>
+    nullish: (params?: Parameters<typeof z.string>[0]) =>
       z
         .string(params)
         .transform(emptyStringAsNull)
         .nullish()
         .pipe(z.string(params).nullish()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
nullish: (
params: Parameters<typeof z.string>[0] = t('common:errors.required')
) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(params).nullish()),
nullish: (params?: Parameters<typeof z.string>[0]) =>
z
.string(params)
.transform(emptyStringAsNull)
.nullish()
.pipe(z.string(params).nullish()),
🤖 Prompt for AI Agents
In src/lib/zod/zod-utils.ts around lines 25 to 32, the default value for the
params argument (t('common:errors.required')) has a type mismatch with the
declared type; change the default to match the other helpers (use undefined as
the default) so the params signature aligns with Parameters<typeof z.string>[0]
and pass that through to z.string(params) as the other functions do.

cursor: z.string().cuid().optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
cursor: z.string().optional(),
limit: z.coerce.number().int().min(1).max(100).prefault(20),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify .prefault() is a valid Zod 4 method.

The migration from .default() to .prefault() is consistent across the codebase, but since my knowledge of Zod 4 is limited (it was released after my training cutoff), please confirm that .prefault() is the correct Zod 4 method for setting default values.

Also applies to: 27-27


🌐 Web query:

What is the Zod 4 method for setting default values? Is it .prefault() or something else?

💡 Result:

Use .default(). Zod v4 uses z.string().default(value) (the default short-circuits parsing). The term "prefault" described the old Zod‑3 behavior; to replicate that old behavior in v4 you can use a transform/pipe (e.g. z.transform(v => v ?? default).pipe(T)). [1][2]

Sources:
[1] Zod v4 docs — Defaults.
[2] Zod v4 changelog / GitHub discussion explaining "prefault" and how to replicate it.


Replace .prefault() with .default()
Zod v4 removed .prefault(); use .default(value) for defaults (and add a transform pipeline if you need pre‐parse default behavior).
Applies to src/server/routers/genre.ts lines 24 and 27

🤖 Prompt for AI Agents
In src/server/routers/genre.ts around lines 24 and 27 the schema uses the
removed Zod v4 method `.prefault()`; replace `.prefault(20)` (and the other
`.prefault(...)` on line 27) with `.default(20)` (or the desired default value),
and if you require a default that is applied before parsing use a transform
pipeline or `z.preprocess()` to set the value prior to validation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/server/auth.tsx (1)

94-109: Extract demo-mode guard and improve error message

Both databaseHooks.user.create.before and .update.before share identical demo-mode checks. Consider:

const guardDemoMode = async <T>(data: T): Promise<{ data: T }> => {
  if (envClient.VITE_IS_DEMO) {
    throw new Error('User modifications are disabled in demo mode');
  }
  return { data };
};

databaseHooks: {
  user: {
    create: { before: guardDemoMode },
    update: { before: guardDemoMode },
  },
},

This removes duplication and provides a clearer error message.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f58dad and 902d350.

📒 Files selected for processing (3)
  • .env.example (1 hunks)
  • src/env/server.ts (2 hunks)
  • src/server/auth.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-09T08:30:25.898Z
Learnt from: ivan-dalmet
PR: BearStudio/start-ui-web#620
File: src/env/server.ts:13-14
Timestamp: 2025-10-09T08:30:25.898Z
Learning: In Zod v4, `prefault()` is a valid method that acts as a "pre-parse default". It differs from `default()` by applying the fallback value before the inner schema runs, whereas `default()` short-circuits parsing. Usage like `z.coerce.number().int().prefault(2592000)` is correct and ensures the default value goes through the validation chain.

Applied to files:

  • src/env/server.ts
🧬 Code graph analysis (1)
src/server/auth.tsx (2)
src/env/server.ts (1)
  • envServer (9-40)
src/env/client.ts (1)
  • envClient (24-52)
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 23-23: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🔬 Tests (lts/*)
  • GitHub Check: Playwright E2E Tests
  • GitHub Check: 🔬 Tests (24)
🔇 Additional comments (7)
.env.example (2)

24-26: LGTM! Environment variable renaming is consistent.

The renaming of session-related variables and the change from AUTH_TRUSTED_ORIGIN to AUTH_TRUSTED_ORIGINS (plural) aligns correctly with the updated parsing logic in src/env/server.ts that splits comma-separated values.


23-23: Quote the placeholder value to ensure proper parsing.

The value REPLACE ME contains spaces and should be quoted. Some dotenv parsers may fail or misinterpret unquoted values with spaces.

Apply this diff:

-AUTH_SECRET="REPLACE ME" # You can use `npx @better-auth/cli@latest secret` to a generated secret
+AUTH_SECRET="REPLACE ME" # You can use `npx @better-auth/cli@latest secret` to generate a secret

Note: This issue also applies to lines 29-30 (GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET).

Based on static analysis hints.

Likely an incorrect or invalid review comment.

src/server/auth.tsx (1)

25-28: LGTM! Session and trusted origins configuration updated correctly.

The session configuration now uses the renamed environment variables (AUTH_SESSION_EXPIRATION_IN_SECONDS and AUTH_SESSION_UPDATE_AGE_IN_SECONDS), and trustedOrigins correctly references AUTH_TRUSTED_ORIGINS, which is pre-parsed as an array in src/env/server.ts.

src/env/server.ts (4)

13-21: LGTM! Session and trusted origins configuration is well-designed.

The session configuration with prefault() ensures default values go through the validation chain, and the AUTH_TRUSTED_ORIGINS parsing logic correctly handles comma-separated values with proper trimming.

Based on learnings.


26-26: LGTM! Email server URL validation is consistent.

The EMAIL_SERVER configuration correctly uses the z.url() shorthand, consistent with the DATABASE_URL pattern.


42-55: Confirm correct use of error key in Zod v4 refine. This replaces the deprecated message option; no changes needed.


11-12: Approve environment schema update
Zod v4.1.12 supports the z.url() shorthand and the new AUTH_SECRET: z.string() is valid.

.optional()
.refine(
(value) =>
// Check in prodution if the value is not REPLACE ME
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

The comment has a typo: "prodution" should be "production".

Apply this diff:

-        // Check in prodution if the value is not REPLACE ME
+        // Check in production if the value is not REPLACE ME
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check in prodution if the value is not REPLACE ME
// Check in production if the value is not REPLACE ME
🤖 Prompt for AI Agents
In src/env/server.ts at line 48, fix the typo in the comment: change "prodution"
to "production" so the comment reads "Check in production if the value is not
REPLACE ME". Make only this correction in the comment text.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 902d350 and e327b04.

📒 Files selected for processing (1)
  • src/layout/manager/nav-sidebar.tsx (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: 🔬 Tests (lts/*)
  • GitHub Check: 🔬 Tests (24)

Comment on lines +67 to +76
<Link to="/manager/dashboard">
{({ isActive }) => (
<SidebarMenuButton asChild isActive={isActive}>
<span>
<LayoutDashboardIcon />
<span>{t('layout:nav.dashboard')}</span>
</span>
</SidebarMenuButton>
)}
</Link>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the asChild + <span> pattern for semantics and accessibility.

The current pattern uses asChild on SidebarMenuButton with a <span> child, which causes the button to render as a non-interactive span element. When TanStack Router's Link adds click handlers to this span, it creates an accessibility issue—spans are not keyboard-navigable or recognizable as interactive elements by assistive technologies.

Additionally, this pattern is inconsistent with the logo link (lines 41-47), where SidebarMenuButton asChild directly wraps the Link component.

Consider one of these approaches:

  1. Remove asChild and let SidebarMenuButton render its own interactive element:
 <Link to="/manager/dashboard">
   {({ isActive }) => (
-    <SidebarMenuButton asChild isActive={isActive}>
-      <span>
-        <LayoutDashboardIcon />
-        <span>{t('layout:nav.dashboard')}</span>
-      </span>
+    <SidebarMenuButton isActive={isActive}>
+      <LayoutDashboardIcon />
+      <span>{t('layout:nav.dashboard')}</span>
     </SidebarMenuButton>
   )}
 </Link>
  1. Align with the logo pattern by making Link the child of SidebarMenuButton (if render props aren't essential):
-<Link to="/manager/dashboard">
-  {({ isActive }) => (
-    <SidebarMenuButton asChild isActive={isActive}>
-      <span>
+<SidebarMenuButton asChild>
+  <Link to="/manager/dashboard">
+    <span>
         <LayoutDashboardIcon />
         <span>{t('layout:nav.dashboard')}</span>
-      </span>
-    </SidebarMenuButton>
-  )}
-</Link>
+    </span>
+  </Link>
+</SidebarMenuButton>

Note: Option 2 loses access to the isActive render prop, so you'd need an alternative way to determine active state.

Also applies to: 79-88, 107-116


🏁 Script executed:

#!/bin/bash
# Locate the SidebarMenuButton component definition
rg -n "SidebarMenuButton" -g "src/**/*.tsx"

Length of output: 1526


🏁 Script executed:

#!/bin/bash
# Show SidebarMenuButton implementation
sed -n '480,620p' src/components/ui/sidebar.tsx

Length of output: 5102


🏁 Script executed:

#!/bin/bash
# Show import section of nav-sidebar.tsx
sed -n '1,40p' src/layout/manager/nav-sidebar.tsx

Length of output: 1015


Ensure menu items render interactive elements. Using SidebarMenuButton asChild with a <span> produces a non-focusable element that receives click handlers—breaking keyboard and assistive‐tech navigation. Remove asChild so it renders a native <button>, or slot the <Link> inside SidebarMenuButton so an anchor/button element receives the event handlers.

🤖 Prompt for AI Agents
In src/layout/manager/nav-sidebar.tsx around lines 67 to 76, the current pattern
uses SidebarMenuButton with asChild wrapping a plain <span>, which yields a
non-focusable element and breaks keyboard/assistive navigation; either remove
the asChild prop so SidebarMenuButton renders a native <button> directly, or
move the <Link> inside SidebarMenuButton (so the underlying rendered
anchor/button receives the event handlers and is focusable). Update the JSX
accordingly so the interactive element is an actual button/anchor (not a span)
and retains accessibility and keyboard support.

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e327b04 and 9b622ab.

📒 Files selected for processing (1)
  • .github/workflows/e2e-tests.yml (1 hunks)
🧰 Additional context used
🪛 Gitleaks (8.28.0)
.github/workflows/e2e-tests.yml

[high] 22-22: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: 🔬 Tests (24)
  • GitHub Check: 🔬 Tests (22)
  • GitHub Check: Playwright E2E Tests
  • GitHub Check: 🔬 Tests (lts/*)

DATABASE_URL: postgres://startui:startui@localhost:5432/startui
VITE_IS_DEMO: false
BETTER_AUTH_SECRET: fyXjLxjXJowjicI2BAjxOaUsQd1QafdegZDciJE/xM8=
AUTH_SECRET: fyXjLxjXJowjicI2BAjxOaUsQd1QafdegZDciJE/xM8=
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove hardcoded auth secret from workflow.

A real AUTH_SECRET is committed directly in the workflow. This leaks credentials to anyone cloning the repo or viewing workflow logs. Pull it from secrets (e.g. AUTH_SECRET: ${{ secrets.AUTH_SECRET }}) and revoke/rotate the exposed value immediately.

🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 22-22: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
.github/workflows/e2e-tests.yml around line 22 contains a hardcoded AUTH_SECRET
value; remove the literal secret from the file, replace it with a reference to
the repository secret (e.g. set AUTH_SECRET to use GitHub Actions secrets like
${ { secrets.AUTH_SECRET } }), add the real secret into the repository/org
GitHub Secrets UI, and immediately revoke/rotate the exposed credential so the
leaked value is invalidated.

@ivan-dalmet ivan-dalmet merged commit 55b1568 into v3-main Oct 10, 2025
12 checks passed
@ivan-dalmet ivan-dalmet deleted the v3/update-deps branch October 10, 2025 12:47
@coderabbitai coderabbitai bot mentioned this pull request Nov 6, 2025
7 tasks
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.

2 participants