Skip to content

Conversation

@kingston
Copy link
Collaborator

@kingston kingston commented Nov 28, 2025

Summary by CodeRabbit

  • Chores

    • Upgraded Fastify (5.6.2), fastify-plugin (5.1.0), Zod (v4) and several router/cli deps.
    • Bumped various generator and example dependencies and tightened schema typings.
  • Bug Fixes

    • Improved error handling and logging in server and generator flows.
    • Updated validation and form schemas for more consistent UUID, int and email handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Nov 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
baseplate-project-builder-web Ready Ready Preview Comment Nov 28, 2025 2:08pm

@changeset-bot
Copy link

changeset-bot bot commented Nov 28, 2025

🦋 Changeset detected

Latest commit: f891025

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@baseplate-dev/project-builder-server Patch
@baseplate-dev/fastify-generators Patch
@baseplate-dev/react-generators Patch
@baseplate-dev/core-generators Patch
@baseplate-dev/project-builder-cli Patch
@baseplate-dev/project-builder-common Patch
@baseplate-dev/project-builder-test Patch
@baseplate-dev/project-builder-web Patch
@baseplate-dev/plugin-auth Patch
@baseplate-dev/plugin-queue Patch
@baseplate-dev/plugin-storage Patch
@baseplate-dev/create-project Patch
@baseplate-dev/code-morph Patch
@baseplate-dev/project-builder-lib Patch
@baseplate-dev/sync Patch
@baseplate-dev/tools Patch
@baseplate-dev/ui-components Patch
@baseplate-dev/utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Walkthrough

Broad dependency bumps (Fastify, Zod, TanStack Router, etc.), pervasive Zod API migrations (z.uuid(), z.int(), z.enum(), z.ZodType), schema composition refactors (discriminated unions, looser object schemas), and extensive typing/generic changes across generators, examples, and project-builder server actions. No major runtime feature additions.

Changes

Cohort / File(s) Summary
Changesets
/.changeset/eight-mice-act.md, /.changeset/khaki-animals-see.md, /.changeset/tired-olives-notice.md
Add changeset entries declaring patch-level dependency upgrades (Fastify 5.6.2, fastify-plugin 5.1.0, Zod 4.1.13, TanStack Router 1.139.7, etc.).
Package constants & package.json bumps
packages/fastify-generators/src/constants/fastify-packages.ts, packages/react-generators/src/constants/react-packages.ts, packages/*/package.json, examples/*/package.json
Update dependency version pins across generators, server, CLI, web packages and example apps (fastify, fastify-plugin, zod, @tanstack/react-router, @hookform/resolvers, @trpc/*, fastify-type-provider-zod, etc.).
Zod scalar & enum API migration
**/field-definition*.ts, **/data-operations/**, examples/**/src/modules/**/services/*.ts, packages/fastify-generators/.../generate-scalar-input-field*.ts, packages/project-builder-lib/src/definition/model/model-field-utils.ts
Replace patterns: z.string().uuid()z.uuid(), z.number().int()z.int(), z.string().email()z.email(), and z.nativeEnum()z.enum() in validation schemas and generated code.
Zod error handling
examples/*/src/utils/zod.ts, packages/fastify-generators/.../utils/zod.ts
Use error.issues instead of deprecated error.errors when formatting Zod validation errors.
Error handler improvements
examples/*/src/plugins/error-handler.ts, packages/fastify-generators/.../error-handler.ts
Introduce hasStatusCode() type guard and getErrorDetails() helper; adjust 4xx/500 handling to use safe extraction and conditionally include originalError in development.
Type constraint & generic tightening (ZodSchema/ZodType/ZodTypeAny → ZodType)
packages/project-builder-lib/**, packages/fastify-generators/**/data-operations/**, examples/**/utils/data-operations/**, packages/fastify-generators/.../postmark.ts, packages/project-builder-web/src/routes/data/models/-hooks/use-model-form.ts, packages/*/schema/creator/*
Narrow generic bounds and signatures from broader Zod types to z.ZodType/z.ZodObject, update related helper types and exported type aliases.
Record / object shape refinements & loosening
packages/fastify-generators/**, packages/project-builder-lib/src/schema/**/types.ts, packages/core-generators/**, packages/project-builder-lib/src/schema/models/**, packages/project-builder-lib/src/schema/settings/theme.ts
Replace z.record(z.unknown()) with z.record(z.string(), z.unknown()); change strict z.object() to z.looseObject() in multiple schema bases; make theme color entries optional.
Discriminated-union & generator refactors
packages/fastify-generators/src/generators/prisma/prisma-field/prisma-field.generator.ts, packages/fastify-generators/src/writers/prisma-schema/fields.ts
Replace object+superRefine descriptor with z.discriminatedUnion built from per-type field schemas; refactor scalar field configs to use createConfig wrapper.
Generated scalar mappings & tests
packages/fastify-generators/.../generate-scalar-input-field.ts, ...unit.test.ts
Update SCALAR_TYPE_TO_ZOD_TYPE mapping (int → int(), uuid → uuid(), jsonObject → record(z.string(), z.unknown())) and adjust tests to match new zod constructors.
Data-operations typing & docs
**/define-operations.ts, **/field-definitions.ts, **/types.ts
Swap email schema docs/usages to z.email(), cast validated update data to Partial<InferInput<TFields>>, and change scalarField/FieldDefinition generics to z.ZodType.
Service action generics & utilities
packages/project-builder-server/src/actions/types.ts, .../cli.ts, .../run-in-worker.ts, .../trpc.ts, .../worker-script.ts, .../utils/*
Replace TInputShape/TOutputShape (ZodRawShape) with TInputType/TOutputType (ZodType); update handler/input/output typings, parse/validate in CLI runner, adjust TRPC builder signatures and procedure input/output types.
Action schema migrations (z.object wrappers)
packages/project-builder-server/src/actions/** (diff, snapshot, sync, template-extractor, templates, etc.)
Convert many previously plain object schema declarations into explicit z.object({...}) schemas; add/adjust some input/output fields (snapshot/sync outputs, template inputs).
Admin CRUD / Input / Column refactors
packages/project-builder-lib/src/schema/apps/web/admin/sections/{crud-actions,crud-columns,crud-form}/*.ts
Introduce AdminCrud*Schema types; tighten spec generics to DefinitionSchemaCreator<...>; replace withEnt/parse flows with lookup-based transforms; add exported input/config input types.
Transformer schema & model transformer spec changes
packages/project-builder-lib/src/schema/models/transformers/*
Build transformer schema as discriminated union from registered transformers; change getModelTransformers() to return Record<string,ModelTransformerType> and tighten registration/lookup flow.
Reference serialization/deserialization & refs helpers
packages/project-builder-lib/src/references/*.ts, .../extend-parser-context-with-refs.ts, fix-ref-deletions.ts, parse-schema-with-references.ts
Change return/inference types to use def.InferOutput<T>; introduce ZodTypeWithOptional helper; adjust WithRef/WithEnt/WithRefBuilder types; add runtime object guards and explicit casts.
Parser & plugin implementation updates
packages/project-builder-lib/src/definition/project-definition-container.test-utils.ts, packages/project-builder-lib/src/parser/parser.ts, packages/project-builder-lib/src/schema/plugins/definition.ts
Replace PluginImplementationStore construction with createPluginImplementationStore(...); add config: undefined to injected plugins; simplify plugin config parsing/transform logic and early-exit when no createConfigSchema.
Model validation/coercion removal
packages/project-builder-lib/src/definition/model/model-field-utils.ts
Remove in-place coercion logic for int/float fields; delegate to generic validator map (affects coercion behavior for string numeric inputs).
Theme utilities & CSS generation
packages/project-builder-lib/src/utils/theme.ts
Return partial result cast to ThemeColorsConfig; filter undefined color entries before generating CSS properties.
Dev-server, compiler & logging tweaks
packages/project-builder-server/src/dev-server/*, src/compiler/web/admin/crud/inputs.ts, src/dev-server/mcp/*
Reformat fastify.log calls to payload-first, add getIsNumber() helper for admin CRUD inputs, and cast structuredContent in MCP server responses.
Minor UX/form changes
examples/todo-with-auth0/.../todo-list-edit-form.tsx
Add registerOptions={{ valueAsNumber: true }} to Position input so React Hook Form treats the value as number.
Tests & e2e adjustments
packages/project-builder-cli/e2e/sync.spec.ts, generator unit tests
Adjust small test expectations/identifiers and update generated string expectations to reflect new zod constructors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas needing extra attention:

  • Discriminated-union refactor in Prisma field generator and model transformer schema (coverage of all field types, options handling).
  • Wide-reaching service-action generic/type changes (ensure callers, TRPC adapters, CLI/worker paths compile and preserve runtime behavior).
  • Reference serialization/deserialization and WithRef/WithEnt typing changes (round-trip correctness, runtime guards).
  • Removal of numeric coercion in model validators (behavioral impact where string numeric values were accepted).
  • .prefault({}) / prefault vs default changes in multiple schema defaults (verify intended semantics with Zod utilities).
  • Zod API shifts (issues vs errors, z.email(), z.uuid(), z.int(), z.enum()) compatibility across generated code and runtime.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Upgrade to Zod v4' clearly and concisely describes the main purpose of this pull request, which is to upgrade the Zod dependency from v3 to v4 across the entire codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kingston/eng-925-upgrade-to-zod-4

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.

@socket-security
Copy link

socket-security bot commented Nov 28, 2025

Copy link

@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: 7

Caution

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

⚠️ Outside diff range comments (2)
packages/project-builder-lib/src/definition/model/model-field-utils.ts (1)

98-117: Dead code confirmed: preProcess parameter has no effect on function behavior.

The preProcess parameter is checked on line 113 but both branches return the identical validator value. While the sole caller at packages/project-builder-server/src/compiler/web/admin/crud/inputs.ts:151 does pass true, the function ignores this argument entirely, making it unused dead code.

Either remove the unused parameter (and update the caller at inputs.ts:155) or add a TODO if coercion logic will be re-added:

 function getModelFieldValidation(
   projectDefinition: ProjectDefinition,
   modelId: string,
   fieldId: string,
-  preProcess?: boolean,
 ): string {
   const model = ModelUtils.byIdOrThrow(projectDefinition, modelId);
   const field = model.model.fields.find((f) => f.id === fieldId);
   if (!field) {
     throw new Error(`Field ${fieldId} not found in model ${model.name}`);
   }

   const nullishSuffix = field.isOptional ? '.nullish()' : '';

-  const validator = `z.${getModelValidator(field)}${nullishSuffix}`;
-  if (!preProcess) {
-    return validator;
-  }
-  return validator;
+  return `z.${getModelValidator(field)}${nullishSuffix}`;
 }
examples/todo-with-auth0/apps/backend/src/modules/todos/services/todo-list.data-service.ts (1)

15-25: Fix z.enum usage with Prisma enum object to use z.nativeEnum instead

The backend's use of z.enum($Enums.TodoListStatus) is incorrect. z.enum expects a tuple of string literals (e.g., ['ACTIVE', 'INACTIVE']), while $Enums.TodoListStatus is a Prisma-generated enum object. This pattern will fail type-checking and runtime validation.

The admin app (examples/todo-with-auth0/apps/admin/src/routes/admin/todos/todo-list/-schemas/todo-list-schema.ts:13) correctly uses z.enum(['ACTIVE', 'INACTIVE']). The backend should either match that pattern or use z.nativeEnum($Enums.TodoListStatus) to validate against the Prisma enum directly:

-  status: scalarField(z.enum($Enums.TodoListStatus).nullish()),
+  status: scalarField(z.nativeEnum($Enums.TodoListStatus).nullish()),
♻️ Duplicate comments (2)
examples/blog-with-auth/apps/backend/src/plugins/error-handler.ts (1)

62-68: Consider storing errorDetails once for consistency.

Similar to the 500 error branch (line 70), storing the result of getErrorDetails(error) would be more consistent and slightly more efficient.

    } else if (hasStatusCode(error) && error.statusCode < 500) {
+     const errorDetails = getErrorDetails(error);
      await reply.code(error.statusCode).send({
-       message: getErrorDetails(error).message,
+       message: errorDetails.message,
        code: error.code,
        statusCode: error.statusCode,
        reqId: request.id,
      });
examples/todo-with-auth0/apps/backend/src/plugins/error-handler.ts (1)

62-68: Consider storing errorDetails once for consistency.

Storing the result of getErrorDetails(error) would match the pattern used in the 500 error branch and be slightly more efficient.

    } else if (hasStatusCode(error) && error.statusCode < 500) {
+     const errorDetails = getErrorDetails(error);
      await reply.code(error.statusCode).send({
-       message: getErrorDetails(error).message,
+       message: errorDetails.message,
        code: error.code,
        statusCode: error.statusCode,
        reqId: request.id,
      });
🧹 Nitpick comments (14)
examples/todo-with-auth0/baseplate/project-definition.json (1)

130-130: Zod v4 migration looks good; minor semantic observation on .min(1) constraint.

The email validation update correctly adopts Zod v4's z.email() direct constructor pattern. However, .min(1) is technically redundant here—valid email addresses are inherently longer than 1 character—but it's not incorrect and may be intentional for consistency with other string validators. If this was present in the old z.string().email().min(1) pattern, preserving it maintains backward compatibility.

Consider removing .min(1) for semantic clarity:

-                  "validation": "z.email().min(1)"
+                  "validation": "z.email()"
packages/project-builder-web/src/routes/data/models/-hooks/use-model-form.ts (1)

24-29: Type refinement aligns with Zod v4 migration.

The change from z.ZodTypeAny to z.ZodType is correct and consistent with the Zod v4 upgrade objectives.

Note: The schema property is defined in the interface but never used in the useModelForm implementation (lines 34-38 only destructure onSubmitSuccess, omit, and modelKey). Consider verifying whether this property should be removed or if it's intentionally kept for API extensibility.

packages/project-builder-server/src/actions/templates/delete-template.action.ts (1)

5-23: LGTM! Clean Zod v4 schema migration.

The migration from implicit to explicit z.object() schema definitions is correct. The input and output schemas properly define all fields with appropriate types and descriptions, and the handler implementation correctly aligns with these schemas.

Optional consideration: The schemas could be exported to allow external consumers to validate input or reference types. However, since the action itself is already exported and consumable, this is not essential.

packages/project-builder-server/src/compiler/web/admin/crud/inputs.ts (1)

125-127: Confirm numeric scalar coverage and consider centralizing the check

getIsNumber is simple and clear, but it hard‑codes 'int' | 'float' as the only numeric types. Please confirm that ModelScalarFieldConfig['type'] has no other numeric variants (e.g. decimal, bigint) and that none are planned; otherwise this will drift silently.

If there is already a numeric helper in ModelFieldUtils (or similar), consider delegating to that here to keep the definition of “numeric scalar” in one place.

packages/project-builder-server/src/actions/templates/show-template-metadata.action.ts (1)

5-15: Input schema migration to z.object looks correct; consider encoding the cross-field constraint

The new showTemplateMetadataInputSchema as a z.object with filePath and optional project is a clean upgrade and should work well with createServiceAction. One optional improvement: the description says project is “required for relative paths, optional for absolute”, but this isn’t enforced by the schema. If you want Zod to catch that earlier, you could add a superRefine/refine or a small union to model that constraint explicitly.

packages/project-builder-server/src/actions/templates/list-templates.action.ts (1)

17-19: Consider avoiding any for config and aligning with existing template metadata.

config: z.record(z.string(), z.any()) is functionally fine but widens the type to Record<string, any>, weakening type safety. If feasible, consider tightening this to something like z.record(z.string(), z.unknown()) or reusing a looser-but-typed object shape similar to instanceData in packages/sync/src/templates/metadata/metadata.ts, which uses z.looseObject({}).optional(). This would keep runtime behavior flexible while preserving stricter typing for consumers.

packages/fastify-generators/src/generators/core/error-handler-service/templates/src/plugins/error-handler.ts (1)

63-69: Consider storing errorDetails once for consistency.

Similar to the 500 error branch (line 70), you could store the result of getErrorDetails(error) once rather than calling it inline on line 65. This would be more consistent with the pattern used below and slightly more efficient.

    } else if (hasStatusCode(error) && error.statusCode < 500) {
+     const errorDetails = getErrorDetails(error);
      await reply.code(error.statusCode).send({
-       message: getErrorDetails(error).message,
+       message: errorDetails.message,
        code: error.code,
        statusCode: error.statusCode,
        reqId: request.id,
      });
packages/project-builder-lib/src/utils/theme.ts (1)

50-69: The type annotations are overly defensive but functionally correct.

Since ThemeColorsConfig now allows undefined values (due to the schema change making oklchColor.optional()), the use of Partial<ThemeColorsConfig> at line 50 and the type assertion at line 69 are more defensive than necessary. The function iterates through all entries in THEME_COLORS, so all keys will be present—only the values can be undefined, which is now permitted by ThemeColorsConfig itself.

Consider simplifying to:

-  const result: Partial<ThemeColorsConfig> = {};
+  const result: ThemeColorsConfig = {} as ThemeColorsConfig;

And removing the cast at line 69:

-  return result as ThemeColorsConfig;
+  return result;

However, the current implementation is safe and works correctly, so this is purely a clarity improvement.

packages/project-builder-server/src/actions/sync/sync-project.action.ts (1)

103-106: Error logging now passes an Error instance; consider deduping message construction

Wrapping non-Error values before logging is a solid improvement and matches the shared logger’s [object, string?] signature. You could also avoid repeating String(error) by computing a single errorMessage used by both logger and return value:

-    } catch (error) {
-      logger.error(
-        error instanceof Error ? error : new Error(String(error)),
-        `Failed to sync project ${project.name}: ${String(error)}`,
-      );
-      return {
-        status: 'error' as const,
-        message: `Failed to sync project ${project.name}: ${error instanceof Error ? error.message : String(error)}`,
-      };
+    } catch (error) {
+      const errorMessage =
+        error instanceof Error ? error.message : String(error);
+      logger.error(
+        error instanceof Error ? error : new Error(errorMessage),
+        `Failed to sync project ${project.name}: ${errorMessage}`,
+      );
+      return {
+        status: 'error' as const,
+        message: `Failed to sync project ${project.name}: ${errorMessage}`,
+      };
+    }
packages/project-builder-server/src/actions/sync/sync-all-projects.action.ts (1)

120-124: Improved error logging; optionally reuse a single derived message

Passing an Error (or wrapped Error) into logger.error is consistent with the shared logger API and other actions. You could slightly simplify by computing the raw message once and reusing it for both logging and results:

-      } catch (error) {
-        logger.error(
-          error instanceof Error ? error : new Error(String(error)),
-          `Failed to sync project ${project.name}: ${String(error)}`,
-        );
-
-        const errorMessage = `Failed to sync: ${error instanceof Error ? error.message : String(error)}`;
+      } catch (error) {
+        const rawErrorMessage =
+          error instanceof Error ? error.message : String(error);
+        const logMessage = `Failed to sync project ${project.name}: ${rawErrorMessage}`;
+
+        logger.error(
+          error instanceof Error ? error : new Error(rawErrorMessage),
+          logMessage,
+        );
+
+        const errorMessage = `Failed to sync: ${rawErrorMessage}`;
packages/project-builder-server/src/actions/snapshot/snapshot-remove.action.ts (1)

8-28: Snapshot-remove schemas look good; optionally standardize error logging

The new snapshotRemoveInputSchema/snapshotRemoveOutputSchema correctly describe the fields the handler reads/returns, and optional snapshotDirectory matches the destructured default to .baseplate-snapshot. For consistency with the updated sync actions, you might also have logger.error receive an Error plus message:

-    } catch (error) {
-      logger.error(
-        `Failed to remove files from snapshot for project ${projectId}: ${String(error)}`,
-      );
-      return {
-        success: false,
-        message: `Failed to remove files from snapshot: ${error instanceof Error ? error.message : String(error)}`,
-        filesRemoved: 0,
-      };
-    }
+    } catch (error) {
+      const errorMessage =
+        error instanceof Error ? error.message : String(error);
+      logger.error(
+        error instanceof Error ? error : new Error(errorMessage),
+        `Failed to remove files from snapshot for project ${projectId}: ${errorMessage}`,
+      );
+      return {
+        success: false,
+        message: `Failed to remove files from snapshot: ${errorMessage}`,
+        filesRemoved: 0,
+      };
+    }
packages/project-builder-server/src/actions/snapshot/snapshot-show.action.ts (1)

8-41: Expanded snapshot-show schemas accurately reflect manifest handling

snapshotShowInputSchema and snapshotShowOutputSchema now fully describe the snapshot path and file breakdown (modified/added/deleted plus totalFiles), and the handler’s success/error branches return objects that conform to these Zod shapes. If you want slightly more consistency, you could also include snapshotPath in the “no snapshot found” branch, since snapshotDir is already resolved there:

-      if (!manifest) {
-        return {
-          success: false,
-          message: `No snapshot found for ${project.name}/${app}`,
-          files: { modified: [], added: [], deleted: [] },
-          totalFiles: 0,
-        };
-      }
+      if (!manifest) {
+        return {
+          success: false,
+          message: `No snapshot found for ${project.name}/${app}`,
+          snapshotPath: snapshotDir.path,
+          files: { modified: [], added: [], deleted: [] },
+          totalFiles: 0,
+        };
+      }
packages/project-builder-lib/src/schema/apps/web/admin/sections/crud-columns/admin-column-spec.ts (1)

35-38: Consider documenting the double cast.

The as unknown as AdminCrudColumnType cast is necessary due to generic variance, but a brief comment explaining why would aid maintainability.

       adminCrudColumns.set(
         column.name,
+        // Cast needed because generic T is erased at runtime
         column as unknown as AdminCrudColumnType,
       );
examples/todo-with-auth0/apps/backend/src/modules/storage/utils/validate-file-upload-options.ts (1)

34-49: Perfect. I have verified all the relevant details. Let me now provide the final rewritten review comment:


Zod v4 error param usage is correct; optional message consistency suggestion for .min(1) validation

The move to string-based error params on z.string(), z.number(), and .positive() aligns with Zod v4's error customization API. The validation logic remains sound.

For complete message consistency, consider adding explicit messages to the .min(1) validations on contentType and category (lines 46, 48) to prevent empty-string failures from falling back to generic "minimum length" text:

contentType: z.string('Content type is required').min(1, { message: 'Content type is required' }),
category: z.string('Category is required').min(1, { message: 'Category is required' }),

This ensures all field validation errors use your custom messages rather than Zod defaults. This is optional polish—the current implementation is functionally correct.

'@baseplate-dev/fastify-generators': patch
---

Upgrade Fastify to 5.6.2 and fastify-plugin to 5.1.0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Changeset description should align with PR title and comprehensive scope.

The changeset documents only the Fastify/fastify-plugin upgrades, but the PR is titled "feat: Upgrade to Zod v4" and the AI summary indicates this is a comprehensive dependency upgrade including Zod 4.1.13 as a significant component. The description should be updated to reflect all major dependency changes being introduced.

Consider revising the description to include Zod upgrade:

-Upgrade Fastify to 5.6.2 and fastify-plugin to 5.1.0
+Upgrade dependencies: Fastify to 5.6.2, fastify-plugin to 5.1.0, Zod to 4.x, and TanStack Router to 1.139.7

Alternatively, if separate changesets are being used per package, ensure all relevant packages have corresponding changesets in the .changeset/ directory.

📝 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
Upgrade Fastify to 5.6.2 and fastify-plugin to 5.1.0
---
'@baseplate-dev/project-builder-server': patch
'@baseplate-dev/fastify-generators': patch
---
Upgrade dependencies: Fastify to 5.6.2, fastify-plugin to 5.1.0, Zod to 4.x, and TanStack Router to 1.139.7
🤖 Prompt for AI Agents
.changeset/eight-mice-act.md around line 6: the changeset summary only mentions
Fastify and fastify-plugin upgrades but the PR title and branch include a Zod v4
upgrade (and possibly other dependency bumps); update this changeset to list all
major dependency changes introduced by the PR (e.g., Zod v4.1.13 and any other
bumped packages) or create separate changeset files for each package if you
intend per-package releases; ensure the description aligns with the PR title and
comprehensively enumerates the scope of the upgrade(s).

Comment on lines +721 to +723
const validatedData = (
skipValidation ? inputData : dataSchema.parse(inputData)
) as Partial<InferInput<TFields>>;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Investigate the need for explicit type casting.

The validation result now requires explicit type casting to Partial<InferInput<TFields>>. This suggests a potential type inference issue with the Zod v4 upgrade. Type assertions can hide type errors and should be avoided when possible.

Please verify:

  1. Why is this type assertion necessary with Zod v4?
  2. Whether dataSchema.parse() no longer returns the correct type
  3. If there's a way to fix the underlying type inference issue instead of using as

Run the following script to check if similar type assertions were needed elsewhere:

#!/bin/bash
# Search for similar type assertion patterns in validation code
rg -nP --type=ts 'dataSchema\.parse\([^)]+\)\s*as\s+(Partial<)?InferInput' -C3
🤖 Prompt for AI Agents
In
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
around lines 721-723, the code casts the result of dataSchema.parse(...) to
Partial<InferInput<TFields>> which hides a type-inference mismatch introduced by
the Zod v4 upgrade; fix by ensuring the Zod schema and input types line up
instead of asserting: declare dataSchema with the correct generic return type
(e.g. ZodType<Partial<InferInput<TFields>>> or
ZodSchema<Partial<InferInput<TFields>>> depending on imports), ensure inputData
is typed as Partial<InferInput<TFields>> so the skipValidation branch matches
the parse branch, or use dataSchema.safeParse and narrow the type from its
success value; update the schema/input types accordingly, remove the cast, and
run the provided ripgrep script to find and fix any other occurrences.

/* TPL_SCHEMA_OBJECT:START */ {
customer: embeddedCustomerFormSchema.nullish(),
email: z.string().email().min(1),
email: z.email().min(1),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the actual file to see the current state
cat -n examples/todo-with-auth0/apps/admin/src/routes/admin/accounts/users/user/-schemas/user-schema.ts | head -40

Repository: halfdomelabs/baseplate

Length of output: 1456


🌐 Web query:

Zod v4 z.email() API documentation methods available

💡 Result:

Short answer — z.email() in Zod v4:

  • Constructor: z.email([options]) — creates a string schema that validates an email. The options object supports at least { pattern: RegExp } to supply a custom email regex (Zod also exports several built-in regexes like z.regexes.email, z.regexes.html5Email, z.regexes.rfc5322Email, z.regexes.unicodeEmail). [1][2]

  • Because z.email() returns a Zod string schema, you can use the usual string/schema methods on it (chainable): .min(), .max(), .regex(), .refine(), .transform(), .optional(), .nullable(), .default(), .describe(), .brand(), .superRefine(), parse()/safeParse(), etc. (i.e., all standard Zod schema operators). [1]

Examples:

  • z.email()
  • z.email({ pattern: z.regexes.html5Email })
  • z.email().optional().min(3)

Sources: Zod v4 docs and API (format validators / email docs). [1][2]

References
[1] Zod v4 documentation (format validators / API).
[2] Zod v4 API (email docs / regex examples).


The .min(1) constraint on z.email() is valid but redundant.

In Zod v4, z.email() returns a string schema that supports chainable methods including .min(), .max(), and other string validators. The code z.email().min(1) is syntactically correct and will function properly. However, the .min(1) constraint is redundant—z.email() already validates that the input is a non-empty email string, making an explicit minimum length check unnecessary.

Consider removing .min(1) to simplify the schema:

email: z.email(),
🤖 Prompt for AI Agents
In
examples/todo-with-auth0/apps/admin/src/routes/admin/accounts/users/user/-schemas/user-schema.ts
around line 27, the schema uses z.email().min(1) which is redundant because
z.email() already enforces a valid non-empty email; remove the unnecessary
.min(1) call so the schema becomes simply z.email(), keeping validation correct
and simplifying the code.

Comment on lines 55 to 72
case 'strip': {
// Use transform to remove values matching defaults after validation
return optionalSchema.transform((value) => {
if (isEmpty(value)) {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- it's typed to a generic
return value;
});
return schema
.transform((value) => {
if (value === defaultValue) return undefined;
if (isEmpty(value)) {
return undefined;
}

return value;
})
.optional();
}
case 'preserve': {
// Return schema with .optional() added
// eslint-disable-next-line @typescript-eslint/no-unsafe-return -- it's typed to a generic
return optionalSchema.transform((x) => x);

return schema.transform((x) => x).optional();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

I'll help you verify the concern about reference equality in the strip mode. Let me investigate the codebase to understand the context better.
<function_calls>

find . -type f -name "extend-parser-context-with-defaults.ts" | head -20

</function_calls>

cat -n packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts | head -100

</function_calls>
<function_calls>

wc -l packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>
<function_calls>

cat packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Now let me search for where this function is being called and what kinds of defaultValue are passed:
<function_calls>

rg -n "extendParserContextWithDefaults|expandDefaultsIntoParserContext" --type=ts -B2 -A2

</function_calls>

Let me check for test files related to this function:
<function_calls>

find . -type f -name "extend-parser-contexttest*" -o -name "extend-parser-context.spec.ts" 2>/dev/null

</function_calls>

Let me search more broadly for tests:
<function_calls>

rg "extendParserContextWithDefaults|expandDefaultsIntoParserContext" --type=ts | grep -i test | head -20

</function_calls>

Let me check how isEmpty is used to understand what types are expected:
<function_calls>

rg -n "isEmpty" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts -B3 -A3

</function_calls>

Let me find where isEmpty is imported from:
<function_calls>

head -30 packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me search for how this schema builder is used in the codebase to understand what defaults are passed:
<function_calls>

rg "stripDefaults|preserveDefaults|strip.*:" packages/project-builder-lib/src --type=ts -B2 -A2 | head -60

</function_calls>

Let me check the actual usage of defaults in the system:
<function_calls>

rg "defaultValue" packages/project-builder-lib/src/schema/creator -n --type=ts -B1 -A1

</function_calls>

Let me look at the schema definition to understand what types are accepted:
<function_calls>

rg -n "z.(object|array|string|number|boolean)" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me search for test files in the project builder lib:
<function_calls>

find packages/project-builder-lib -type f -name ".test.ts" -o -name ".spec.ts" | head -20

</function_calls>

Now let me check if there are any tests for the extend-parser-context function:
<function_calls>

rg "extend.*context.*default|expandDefault" packages/project-builder-lib --type=ts -l

</function_calls>

Let me check for any schema examples or test data:
<function_calls>

rg "defaultValue\s*:" --type=ts packages/project-builder-lib -B3 -A3 | head -50

</function_calls>

Let me look at the broader context of how schemas are used in the codebase:
<function_calls>

find packages/project-builder-lib/src/schema -type f -name "*.ts" | head -20

</function_calls>

Let me check the most relevant files:
<function_calls>

ls -la packages/project-builder-lib/src/schema/creator/

</function_calls>

Let me examine how the function works more carefully by looking at line 40-75:
<function_calls>

sed -n '40,75p' packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Now let me search for how this function is called with specific defaultValue examples:
<function_calls>

rg -n "defaultsMode.*strip|defaultsMode.*preserve" packages/project-builder-lib/src --type=ts -B5 -A5 | head -80

</function_calls>

Let me check if there are any files that show schema creation with defaults:
<function_calls>

rg "z.default(" packages/project-builder-lib/src --type=ts -B2 -A2 | head -60

</function_calls>

Let me examine the type signature more carefully:
<function_calls>

sed -n '1,40p' packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me check what the isEmpty utility actually does:
<function_calls>

rg -n "export.*isEmpty" packages/project-builder-lib/src --type=ts | head -10

</function_calls>

<function_calls>

rg -n "function isEmpty|const isEmpty" --type=ts -A10

</function_calls>

Let me search more broadly:
<function_calls>

find . -type f -name "isEmpty" 2>/dev/null

</function_calls>

Let me check the utils directory:
<function_calls>

rg "isEmpty" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts -B20 | head -30

</function_calls>

Now let me understand the actual use case by looking at specific examples in the codebase:
<function_calls>

rg "expandDefaultsIntoParserContext" packages/project-builder-lib/src --type=ts -A10 | head -80

</function_calls>

Let me check if there are object or array defaults being used in practice:
<function_calls>

rg "z.(object|array)" packages/project-builder-lib/src/schema --type=ts | grep -A1 "default" | head -30

</function_calls>

Let me look at practical examples in schema files:
<function_calls>

ls packages/project-builder-lib/src/schema/*.ts | head -10

</function_calls>

<function_calls>

head -100 packages/project-builder-lib/src/schema/plugin.schema.ts 2>/dev/null || echo "File not found"

</function_calls>

Let me check what's in the schema directory:
<function_calls>

find packages/project-builder-lib/src/schema -name "*.schema.ts" -type f | head -5

</function_calls>

<function_calls>

rg ".default(" packages/project-builder-lib/src/schema --type=ts | head -20

</function_calls>

Let me focus on understanding what types of values can be used as defaults by checking the Zod patterns:
<function_calls>

python3 << 'EOF'

Let me analyze the transform logic

The concern is about reference equality for objects/arrays

In the strip mode:

Line 59: if (value === defaultValue) return undefined;

Line 60-62: if (isEmpty(value)) return undefined;

The question is: can defaultValue be an object or array?

Looking at Zod patterns:

z.object({...}).default({...}) - object default

z.array(...).default([...]) - array default

z.string().default("x") - string default

z.number().default(5) - number default

z.boolean().default(true) - boolean default

For primitives: === works fine

For objects/arrays: === checks reference equality, not value equality

So the concern is valid IF object/array defaults are used

print("Analyzing the concern:")
print("1. Strict equality (===) works fine for primitives")
print("2. Strict equality fails for objects/arrays (different references)")
print("3. The isEmpty() check would catch empty arrays/objects")
print("4. But would NOT catch non-empty objects/arrays matching the default")
print("\nKey question: Are object/array defaults actually used in this codebase?")
EOF


</function_calls>

Let me search specifically for object and array schema defaults:
<function_calls>

rg "z.(object|array|record|map)(" packages/project-builder-lib/src --type=ts | wc -l

</function_calls>

<function_calls>

rg -n "z.(object|array)" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me check the function signature again to see what schema types it handles:
<function_calls>

sed -n '1,50p' packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts | cat -n

</function_calls>

Let me see the actual full file content again carefully:
<function_calls>

cat -n packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts | tail -n +35 | head -45

</function_calls>

Now let me understand what ZodSchema can be by checking type definitions:
<function_calls>

rg "export.*type.*ZodSchema|interface.*ZodSchema" packages/project-builder-lib --type=ts | head -5

</function_calls>

Let me check if there are practical examples with object defaults in test files:
<function_calls>

find packages/project-builder-lib -name ".test.ts" -o -name ".spec.ts" | xargs grep -l "expandDefaultsIntoParserContext|strip|preserve" 2>/dev/null | head -5

</function_calls>

Let me check if there are example files or documentation:
<function_calls>

find packages/project-builder-lib -name "README*" -o -name "*.md" | head -10

</function_calls>

Let me try a different approach - look for actual schema usage patterns:
<function_calls>

python3 << 'EOF'
import re
import subprocess

Get all usages of expandDefaultsIntoParserContext

result = subprocess.run(
["rg", "-n", "expandDefaultsIntoParserContext", "--type=ts", "-B3", "-A10"],
capture_output=True,
text=True,
cwd="."
)

Print relevant sections showing how it's called

print("Usages of expandDefaultsIntoParserContext:")
print(result.stdout[:2000])
EOF


</function_calls>

Let me check the file structure and understand if defaults mode is used only for certain field types:
<function_calls>

rg "case.*strip|case.*preserve" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts -B10 -A15

</function_calls>

Now let me look at the actual implementation logic more carefully:
<function_calls>

sed -n '40,80p' packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts | cat -n

</function_calls>

Let me check what types of values isEmpty can handle:
<function_calls>

rg "import.*isEmpty|from.*isEmpty" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me check the imports at the top of the file:
<function_calls>

head -20 packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Now let me check the isEmpty implementation from lodash:
<function_calls>

python3 << 'EOF'

isEmpty from lodash documentation:

Checks if value is an empty object, collection, map, or set

Works with: objects, arrays, maps, sets, strings, etc.

So isEmpty(value) returns true for:

- {} (empty object)

- [] (empty array)

- "" (empty string)

- null, undefined would return true

- Other falsy values

The strip mode logic is:

1. If value === defaultValue, return undefined (EXACT equality)

2. If isEmpty(value), return undefined (handles empty values)

3. Otherwise return value

For object/array scenario:

defaultValue = { a: 1 }

value = { a: 1 }

value === defaultValue => FALSE (different references)

isEmpty(value) => FALSE (not empty)

Result: returns { a: 1 } instead of undefined

This IS a bug if object/array defaults are used!

print("Analysis: The concern appears VALID")
print("")
print("Problematic scenario:")
print(" defaultValue = { a: 1 }")
print(" value = { a: 1 }")
print(" value === defaultValue => false (reference mismatch)")
print(" isEmpty(value) => false (not empty)")
print(" Result: value is NOT stripped (BUG)")
EOF


</function_calls>

Now let me verify if object/array defaults are actually used in the codebase:
<function_calls>

rg "z.(object|array|record)([^)]*).default(" packages/project-builder-lib/src --type=ts -B2 -A2 | head -60

</function_calls>

Let me try a broader search:
<function_calls>

rg ".default({" packages/project-builder-lib/src --type=ts | head -10

</function_calls>

<function_calls>

rg ".default([" packages/project-builder-lib/src --type=ts | head -10

</function_calls>

Let me check the actual scenarios in which this function is used by looking at git history or examining all usages more carefully:
<function_calls>

rg "expandDefaultsIntoParserContext|extendParserContextWithDefaults" packages/project-builder-lib/src --type=ts -l

</function_calls>

<function_calls>

cat packages/project-builder-lib/src/schema/creator/parser-context.schema.ts 2>/dev/null | head -150

</function_calls>

Let me check if that file exists:
<function_calls>

find packages/project-builder-lib/src -name "parser-context" -type f

</function_calls>

<function_calls>

ls packages/project-builder-lib/src/schema/creator/

</function_calls>

Let me check what calls the function:
<function_calls>

find packages/project-builder-lib/src/schema/creator -name "*.ts" -type f | xargs grep -l "expandDefaults|extendParserContext"

</function_calls>

<function_calls>

head -200 packages/project-builder-lib/src/schema/creator/schema-field-for-plugin.schema.ts 2>/dev/null | tail -100

</function_calls>

Let me get the full file:
<function_calls>

wc -l packages/project-builder-lib/src/schema/creator/schema-field-for-plugin.schema.ts

</function_calls>

<function_calls>

grep -n "expandDefaults|extendParserContext" packages/project-builder-lib/src/schema/creator/schema-field-for-plugin.schema.ts -B5 -A5

</function_calls>

Let me check what the actual schema definition looks like for the mode:
<function_calls>

grep -n "strip|preserve" packages/project-builder-lib/src/schema/creator/schema-field-for-plugin.schema.ts -B2 -A2 | head -40

</function_calls>

Let me search for where the function is actually called:
<function_calls>

rg "expandDefaultsIntoParserContext" packages/project-builder-lib/src --type=ts -B5 -A10 | head -100

</function_calls>

Let me check the full extend-parser-context file one more time to understand the complete logic:
<function_calls>

cat -n packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Perfect! Now let me understand the context. Let me check what types of schemas are being passed to this function:
<function_calls>

grep -n "expandDefaultsIntoParserContext" packages/project-builder-lib/src --type=ts -r | head -20

</function_calls>

<function_calls>

grep -rn "expandDefaultsIntoParserContext" packages/project-builder-lib/src/

</function_calls>

It seems the function might not be used, or it's exported but not used internally. Let me check the exports:
<function_calls>

grep -n "export" packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts

</function_calls>

Let me check the index file:
<function_calls>

find packages/project-builder-lib/src/schema/creator -name "index.ts" -exec cat {} ;

</function_calls>

<function_calls>

find packages/project-builder-lib/src/schema -name "index.ts" -exec cat {} ;

</function_calls>

<function_calls>

find packages/project-builder-lib/src -name "index.ts" | head -5

</function_calls>

<function_calls>

cat packages/project-builder-lib/src/index.ts 2>/dev/null | head -50

</function_calls>

Now let me verify the logic one final time with a concrete analysis:
<function_calls>

python3 << 'EOF'
import json

Let me trace through the logic carefully for the strip case:

Line 55-72 of the file shows:

case 'strip': {

return schema

.transform((value) => {

if (value === defaultValue) return undefined;

if (isEmpty(value)) {

return undefined;

}

return value;

})

.optional();

}

Scenario 1: Primitive default (string, number, etc.)

print("=== SCENARIO 1: Primitive Default ===")
print("defaultValue = 'hello'")
print("value = 'hello'")
print("value === defaultValue => TRUE")
print("Result: Returns undefined ✓ CORRECT")
print()

Scenario 2: Object default with same value

print("=== SCENARIO 2: Object Default ===")
print("defaultValue = { a: 1, b: 2 }")
print("value = { a: 1, b: 2 }")
print("value === defaultValue => FALSE (different references)")
print("isEmpty(value) => FALSE")
print("Result: Returns { a: 1, b: 2 } ✗ SHOULD RETURN undefined")
print()

Scenario 3: Array default with same value

print("=== SCENARIO 3: Array Default ===")
print("defaultValue = [1, 2, 3]")
print("value = [1, 2, 3]")
print("value === defaultValue => FALSE (different references)")
print("isEmpty(value) => FALSE")
print("Result: Returns [1, 2, 3] ✗ SHOULD RETURN undefined")
print()

Scenario 4: Empty array

print("=== SCENARIO 4: Empty Array ===")
print("defaultValue = []")
print("value = []")
print("value === defaultValue => FALSE")
print("isEmpty(value) => TRUE")
print("Result: Returns undefined ✓ (empty handled by isEmpty)")
print()

print("CONCLUSION:")
print("The concern is VALID for non-empty objects and arrays.")
print("The logic correctly handles empty collections via isEmpty().")
print("The logic correctly handles primitives via ===.")
print("The logic FAILS for non-empty objects/arrays matching the default.")
EOF


</function_calls>

Reference equality issue in strip mode correctly identified—affects object and array defaults.

The concern is valid. Line 59's strict equality check (value === defaultValue) works correctly for primitives but fails for objects and arrays due to reference comparison.

For non-empty object/array defaults:

  • { a: 1 } === { a: 1 } returns false (different references)
  • isEmpty({ a: 1 }) returns false (not empty)
  • Result: matching default values won't be stripped, violating the intended behavior

The isEmpty() fallback only catches empty collections ({}, []), not non-empty matching defaults.

Suggested fix remains valid, though JSON.stringify() comparison has edge cases (e.g., property order, circular references, functions). For robust handling, consider using a deep equality library like lodash.isEqual() or fast-deep-equal.

🤖 Prompt for AI Agents
In
packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts
around lines 55 to 72, the strip-mode uses strict equality (value ===
defaultValue) which fails for objects/arrays; replace that check with a
deep-equality comparison (e.g., import and use lodash.isEqual or
fast-deep-equal) so non-empty object/array defaults are recognized and stripped,
keep the existing isEmpty() check, and add the necessary import at the top of
the file.

Comment on lines 18 to +25
export async function runActionInWorker<
TInputShape extends z.ZodRawShape,
TOutputShape extends z.ZodRawShape,
TInputType extends z.ZodType,
TOutputType extends z.ZodType,
>(
serviceAction: ServiceAction<TInputShape, TOutputShape>,
input: z.objectOutputType<TInputShape, z.ZodTypeAny, 'strip'>,
serviceAction: ServiceAction<TInputType, TOutputType>,
input: z.output<TInputType>,
context: ServiceActionContext,
): Promise<z.objectInputType<TOutputShape, z.ZodTypeAny, 'strip'>> {
): Promise<z.input<TOutputType>> {
Copy link

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

🏁 Script executed:

#!/bin/bash
# Search for runActionInWorker call sites to understand how callers use the result
rg -n "runActionInWorker\(" --type=ts -A 5 -B 2

Repository: halfdomelabs/baseplate

Length of output: 2242


🏁 Script executed:

#!/bin/bash
# Check worker-script.ts validation at line 83 and surrounding context
cat -n packages/project-builder-server/src/actions/utils/worker-script.ts | sed -n '75,95p'

Repository: halfdomelabs/baseplate

Length of output: 785


🏁 Script executed:

#!/bin/bash
# Check run-in-worker.ts to see how it handles the worker response
wc -l packages/project-builder-server/src/actions/utils/run-in-worker.ts

Repository: halfdomelabs/baseplate

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Read the full run-in-worker.ts file to understand result handling
cat -n packages/project-builder-server/src/actions/utils/run-in-worker.ts

Repository: halfdomelabs/baseplate

Length of output: 4037


Return type should be z.output<TOutputType> to match validated output from worker.

The worker validates output at line 83 before sending it back (validatedResult = z.object(action.outputSchema).parse(result)), but the return type declares Promise<z.input<TOutputType>> (unparsed). Line 72 incorrectly casts the validated result as z.input when it should be z.output. This inconsistency forces callers to use type escapes (as never) to work around the mismatch.

Fix: Change line 25 return type to Promise<z.output<TOutputType>> and line 72 cast to as z.output<TOutputType>.

🤖 Prompt for AI Agents
In packages/project-builder-server/src/actions/utils/run-in-worker.ts around
lines 18 to 25 (and referencing lines 72 and 83), the function return type and a
cast are using z.input<TOutputType> despite the worker validating and returning
the parsed output; update the function signature return type at line 25 to
Promise<z.output<TOutputType>> and change the cast at line 72 from as
z.input<TOutputType> to as z.output<TOutputType> so the declared types match the
validated result parsed at line 83.

Comment on lines +18 to 24
query: <TInputType extends z.ZodType, TOutputType extends z.ZodType>(
action: ServiceAction<TInputType, TOutputType>,
) => TRPCQueryProcedure<{
input: z.objectOutputType<TInputShape, z.ZodTypeAny, 'strip'>;
output: z.objectOutputType<TOutputShape, z.ZodTypeAny, 'strip'>;
meta: unknown;
input: z.input<TInputType>;
output: z.input<TOutputType>;
}>;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent output type in query signature.

The query output type uses z.input<TOutputType> (lines 23, 68), but mutation uses z.output<TOutputType> (lines 15, 49). Both call runActionInWorker and should have the same output type.

   query: <TInputType extends z.ZodType, TOutputType extends z.ZodType>(
     action: ServiceAction<TInputType, TOutputType>,
   ) => TRPCQueryProcedure<{
     meta: unknown;
     input: z.input<TInputType>;
-    output: z.input<TOutputType>;
+    output: z.output<TOutputType>;
   }>;

Also update line 68:

       }) as TRPCQueryProcedure<{
       meta: unknown;
       input: z.input<TInputType>;
-      output: z.input<TOutputType>;
+      output: z.output<TOutputType>;
     }>,
🤖 Prompt for AI Agents
In packages/project-builder-server/src/actions/utils/trpc.ts around lines 18–24
(and also update line 68), the query signature mistakenly uses
z.input<TOutputType> for the output type while mutation uses
z.output<TOutputType>; make them consistent by changing the query output type to
z.output<TOutputType> (and update the other occurrence at line 68 the same way)
so both query and mutation use z.output for outputs when calling
runActionInWorker.

Comment on lines +82 to 84
const result = await action.handler(input, contextWithLogger);
const validatedResult = z.object(action.outputSchema).parse(result);
sendMessage({ type: 'success', result: validatedResult });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: z.object() wrapping is incompatible with new ZodType-based schemas.

With the migration to z.ZodType-based generics, action.outputSchema is already a complete Zod schema (e.g., z.object({...})), not a raw shape. Wrapping it in z.object() will cause a runtime error.

-  const result = await action.handler(input, contextWithLogger);
-  const validatedResult = z.object(action.outputSchema).parse(result);
+  const parsedInput = action.inputSchema.parse(input);
+  const result = await action.handler(parsedInput, contextWithLogger);
+  const validatedResult = action.outputSchema.parse(result);

Also consider parsing the input before passing to the handler for consistency with cli.ts.

📝 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
const result = await action.handler(input, contextWithLogger);
const validatedResult = z.object(action.outputSchema).parse(result);
sendMessage({ type: 'success', result: validatedResult });
const parsedInput = action.inputSchema.parse(input);
const result = await action.handler(parsedInput, contextWithLogger);
const validatedResult = action.outputSchema.parse(result);
🤖 Prompt for AI Agents
In packages/project-builder-server/src/actions/utils/worker-script.ts around
lines 82 to 84, the code wraps action.outputSchema in z.object(...) which is
wrong because action.outputSchema is already a full Zod schema (ZodType) and
this causes a runtime error; change the code to parse the handler result with
action.outputSchema.parse(result) instead of
z.object(action.outputSchema).parse(result), and additionally validate/parse the
incoming input with action.inputSchema.parse(input) before calling
action.handler so input is consistent with cli.ts.

Copy link

@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

🧹 Nitpick comments (2)
examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx (1)

45-48: Zod v4 z.email() migration here is correct and preserves behavior

Switching the email field to z.email().transform((value) => value.toLowerCase()) is the idiomatic Zod 4 pattern and keeps the type as string with the same post‑validation lowercasing semantics. No downstream changes should be needed since FormData['email'] remains a string and the resolver still surfaces field errors the same way.

If you want to reduce duplication between auth screens, you could optionally extract a shared const emailSchema = z.email().transform((value) => value.toLowerCase()); and reuse it in both login and register form schemas.

examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx (1)

45-48: Consistent Zod v4 z.email() usage and semantics for login form

Using z.email().transform((value) => value.toLowerCase()) here matches the register form and is the intended Zod 4 email schema. Types and validation behavior remain compatible with the previous z.string().email() chain, and the lowercase normalization is still applied only after successful validation.

As with the register form, consider an optional shared emailSchema constant reused across both auth form schemas to keep them in sync.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 03c2d8a and f891025.

⛔ Files ignored due to path filters (13)
  • examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/login.tsx is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/admin/baseplate/generated/src/routes/auth_/register.tsx is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/modules/accounts/password/services/user-password.service.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/local-auth/core/generators/react-session/generated/ts-import-providers.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/local-auth/core/generators/react-session/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.ts is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/types.ts is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/apps/backend/schema.graphql is excluded by !tests/**
  • tests/simple/apps/backend/src/utils/data-operations/define-operations.ts is excluded by !tests/**
  • tests/simple/apps/backend/src/utils/data-operations/field-definitions.ts is excluded by !tests/**
  • tests/simple/apps/backend/src/utils/data-operations/types.ts is excluded by !tests/**
  • tests/simple/apps/web/src/generated/graphql.tsx is excluded by !**/generated/**, !tests/**, !**/generated/**
📒 Files selected for processing (8)
  • examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx (1 hunks)
  • examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx (1 hunks)
  • examples/blog-with-auth/apps/backend/src/modules/accounts/password/services/user-password.service.ts (0 hunks)
  • packages/project-builder-cli/e2e/sync.spec.ts (2 hunks)
  • plugins/plugin-auth/src/local-auth/core/generators/auth-email-password/templates/module/services/user-password.service.ts (0 hunks)
  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/login.tsx (1 hunks)
  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/register.tsx (1 hunks)
  • plugins/plugin-auth/src/local-auth/core/generators/react-session/extractor.json (1 hunks)
💤 Files with no reviewable changes (2)
  • plugins/plugin-auth/src/local-auth/core/generators/auth-email-password/templates/module/services/user-password.service.ts
  • examples/blog-with-auth/apps/backend/src/modules/accounts/password/services/user-password.service.ts
✅ Files skipped from review due to trivial changes (1)
  • plugins/plugin-auth/src/local-auth/core/generators/react-session/extractor.json
🧰 Additional context used
📓 Path-based instructions (5)
plugins/**/plugin-*/**/*.{jsx,tsx}

📄 CodeRabbit inference engine (plugins/CLAUDE.md)

All CSS classes used in className attributes within plugin components MUST be prefixed with the plugin name (e.g., auth:, storage:) to avoid style conflicts between plugins and the main application

Files:

  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/login.tsx
  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/register.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-style.mdc)

**/*.{ts,tsx}: Use TypeScript with strict type checking enabled
Always include return types on top-level functions including React components (React.ReactElement)
Include absolute paths in import statements via tsconfig paths (@src/ is the alias for src/)
If a particular interface or type is not exported, change the file so it is exported
If caught on a typing loop where forcing the any type is necessary, do not iterate too much - leave the typing as broken and let the user fix it

If target code is not easily testable, refactor it to be more testable (e.g., export types or functions)

**/*.{ts,tsx}: Import components from '@baseplate-dev/ui-components' package for UI development (e.g., Button, Input, Card, Dialog, etc.)
Use form components with React Hook Form controller variants (InputField, TextareaField, SelectField, CheckboxField, SwitchField, ComboboxField, MultiComboboxField, ColorPickerField, DatePickerField, DateTimePickerField)
Use SidebarLayout, Card, Breadcrumb, NavigationMenu, and NavigationTabs components for consistent layout structure from @baseplate-dev/ui-components
Use Dialog, ConfirmDialog, and useConfirmDialog from @baseplate-dev/ui-components for modal dialogs and confirmation interactions
Always use compareStrings from @baseplate-dev/utils instead of String.prototype.localeCompare() for code generation, file sorting, and internal data structures
If a particular interface or type is not exported, modify the file to export it
Use TsCodeFragment for composable code pieces and TsCodeUtils for manipulating fragments when generating TypeScript code
Create generators using createGenerator with configuration via descriptor schema (Zod), organizing into one or more tasks created with createGeneratorTask
Tasks should have run (initialization) and build (code generation) phases, export and consume providers, and may be organized into phases for ordered execution
Use provider scopes to control visibility and prevent collisions be...

Files:

  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx
  • packages/project-builder-cli/e2e/sync.spec.ts
  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/register.tsx
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.cursor/rules/code-style.mdc)

**/*.{ts,tsx,js}: Node 16 module resolution - include file extensions in imports (.js)
Sort imports by group: external libs first, then local imports
Use camelCase for variables/functions, PascalCase for types/classes
Order functions such that functions are placed below the variables/functions they use
Prefer using nullish coalescing operator (??) instead of logical or (||), enforced via ESLint rule
Prefer barrel exports e.g. export * from './foo.js' instead of individual named exports
Use console.info/warn/error instead of console.log

Files:

  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx
  • packages/project-builder-cli/e2e/sync.spec.ts
  • plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/register.tsx
examples/blog-with-auth/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (examples/blog-with-auth/CLAUDE.md)

examples/blog-with-auth/**/*.{ts,tsx,js,jsx}: Always use .js extensions in import statements, even for TypeScript files (e.g., import { getSystemInfo } from '@src/system-info.js';)
Follow ESM module resolution with TypeScript's NodeNext setting
Add JSDocs to all exported functions, interfaces, and classes with documentation of the function, its parameters, return value, and all fields

Files:

  • examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx
examples/blog-with-auth/**/*.{ts,tsx}

📄 CodeRabbit inference engine (examples/blog-with-auth/CLAUDE.md)

examples/blog-with-auth/**/*.{ts,tsx}: Use import type for type-only imports in TypeScript
Always specify explicit return types for functions in TypeScript

Files:

  • examples/blog-with-auth/apps/admin/src/routes/auth_/login.tsx
  • examples/blog-with-auth/apps/admin/src/routes/auth_/register.tsx
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T22:46:20.505Z
Learning: Applies to **/*.{ts,tsx} : Create generators using `createGenerator` with configuration via descriptor schema (Zod), organizing into one or more tasks created with `createGeneratorTask`
📚 Learning: 2025-11-24T19:44:46.506Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:44:46.506Z
Learning: Applies to examples/todo-with-auth0/**/*.{unit,int}.test.ts : Import test functions explicitly from 'vitest' instead of using globals. Example: `import { describe, expect, it } from 'vitest';`

Applied to files:

  • packages/project-builder-cli/e2e/sync.spec.ts
📚 Learning: 2025-11-24T19:44:33.994Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/blog-with-auth/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:44:33.994Z
Learning: Applies to examples/blog-with-auth/**/*.test.ts : Import test functions explicitly from 'vitest' instead of relying on globals (e.g., `import { describe, expect, it } from 'vitest';`)

Applied to files:

  • packages/project-builder-cli/e2e/sync.spec.ts
📚 Learning: 2025-04-21T06:32:22.476Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 505
File: packages/create-project/tsconfig.json:6-6
Timestamp: 2025-04-21T06:32:22.476Z
Learning: Since TypeScript 4.1, baseUrl is not required for paths mapping when using explicit relative paths (with "./"). Removing baseUrl from tsconfig.json while updating paths to use relative paths (e.g., changing "src/*": ["src/*"] to "src/*": ["./src/*"]) prevents bare path imports from node_modules while maintaining path alias functionality.

Applied to files:

  • packages/project-builder-cli/e2e/sync.spec.ts
📚 Learning: 2025-04-21T06:32:22.476Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 505
File: packages/create-project/tsconfig.json:6-6
Timestamp: 2025-04-21T06:32:22.476Z
Learning: Since TypeScript 4.1, baseUrl is not required for paths mapping in tsconfig.json. Removing baseUrl and using explicit relative paths with "./" prefix (e.g., changing "src/*": ["src/*"] to "src/*": ["./src/*"]) prevents bare path imports from node_modules while maintaining path alias functionality.

Applied to files:

  • packages/project-builder-cli/e2e/sync.spec.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: Lint Examples (todo-with-auth0)
  • GitHub Check: Lint Examples (blog-with-auth)
  • GitHub Check: Test E2E
  • GitHub Check: Lint
🔇 Additional comments (3)
plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/register.tsx (1)

46-46: Correct migration to Zod v4's standalone email validator.

The change from z.string().email() to z.email() correctly implements Zod v4's new standalone string format schema API. The lowercase transform is preserved.

Please verify that the email validation behavior remains consistent with the previous implementation. You can test this by ensuring:

  • Email format validation works as expected
  • Invalid emails are rejected with appropriate error messages
  • The lowercase transformation still applies correctly
  • Form validation integrates properly with react-hook-form via zodResolver

Based on library documentation from Zod v4.

plugins/plugin-auth/src/local-auth/core/generators/auth-routes/templates/routes/auth_/login.tsx (1)

46-46: Correct migration to Zod v4's standalone email validator.

The change from z.string().email() to z.email() correctly implements Zod v4's new standalone string format schema API, consistent with the same update in register.tsx. The lowercase transform is preserved.

Based on library documentation from Zod v4.

packages/project-builder-cli/e2e/sync.spec.ts (1)

20-20: I'll help you verify the changes in this review comment. Let me investigate the codebase to confirm whether all usages have been properly updated.
<function_calls>


#!/bin/bash

First, let's check for any remaining makeUrl usages in e2e tests

echo "=== Searching for remaining makeUrl usages in e2e tests ==="
rg -n 'makeUrl' packages/project-builder-cli/e2e/ 2>/dev/null || echo "No makeUrl usages found"

echo -e "\n=== Searching for all addProject usages in e2e tests ==="
rg -n -A3 'await addProject(' packages/project-builder-cli/e2e/ 2>/dev/null || echo "No addProject usages found"

echo -e "\n=== Checking for startUrl usages in e2e tests ==="
rg -n 'startUrl' packages/project-builder-cli/e2e/ 2>/dev/null || echo "No startUrl usages found"


</function_calls>

<function_calls>


#!/bin/bash

Find the test helper file that defines addProject

echo "=== Locating test helper files ==="
find packages/project-builder-cli -name 'fixture' -o -name 'helper' -o -name 'setup' | grep -E '.(ts|tsx|js)$'

echo -e "\n=== Searching for addProject function definition ==="
rg -n 'export.*addProject|function addProject|const addProject' packages/project-builder-cli/e2e/ --type ts


</function_calls>

<function_calls>


#!/bin/bash

Let's list all e2e test files to get a complete picture

echo "=== All e2e test files ==="
find packages/project-builder-cli/e2e -name '.spec.ts' -o -name '.test.ts'

echo -e "\n=== Checking file structure ==="
ls -la packages/project-builder-cli/e2e/ 2>/dev/null | head -20


</function_calls>

@kingston kingston merged commit e8576b9 into main Nov 28, 2025
16 checks passed
@kingston kingston deleted the kingston/eng-925-upgrade-to-zod-4 branch November 28, 2025 14:18
@github-actions github-actions bot mentioned this pull request Nov 26, 2025
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