Skip to content

Conversation

@kingston
Copy link
Collaborator

@kingston kingston commented Nov 19, 2025

Summary by CodeRabbit

  • New Features

    • Declarative, type-safe data operations with composable field definitions and lifecycle hooks.
    • Standardized "Data" input shapes for GraphQL mutations with improved nested relation handling.
    • File-field abstraction for safer file references and automatic metadata handling.
    • New test helpers and custom matchers to improve codegen testing.
  • Bug Fixes

    • Unified file validation/authorization to prevent invalid or reused file references.
  • Tests

    • New and updated integration/unit tests for data services, file handling, and generators.

@changeset-bot
Copy link

changeset-bot bot commented Nov 19, 2025

🦋 Changeset detected

Latest commit: 47feaa1

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/fastify-generators Minor
@baseplate-dev/core-generators Minor
@baseplate-dev/utils Minor
@baseplate-dev/project-builder-server Minor
@baseplate-dev/plugin-auth Major
@baseplate-dev/plugin-queue Major
@baseplate-dev/plugin-storage Major
@baseplate-dev/react-generators Minor
@baseplate-dev/create-project Minor
@baseplate-dev/project-builder-cli Minor
@baseplate-dev/project-builder-lib Minor
@baseplate-dev/project-builder-test Minor
@baseplate-dev/project-builder-web Minor
@baseplate-dev/sync Minor
@baseplate-dev/ui-components Minor
@baseplate-dev/project-builder-common Minor
@baseplate-dev/code-morph Minor
@baseplate-dev/tools Minor

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

@vercel
Copy link

vercel bot commented Nov 19, 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 19, 2025 3:59pm

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Walkthrough

Replaces imperative Prisma CRUD services with a declarative, type-safe data-operations framework: adds operation builders, field-definition primitives, relation helpers, Prisma type adapters, generator updates and example migrations from *.crud.ts to *.data-service.ts; updates GraphQL inputs/resolvers and test configs (Vitest maxWorkers = 1).

Changes

Cohort / File(s) Summary
Data-operations core
examples/*/src/utils/data-operations/*
Adds new modules: define-operations.ts, field-definitions.ts, prisma-types.ts, prisma-utils.ts, relation-helpers.ts, types.ts — typed primitives, field transformers, lifecycle hooks, generic Prisma delegates, and relation helpers.
Service modules (CRUD → data-service)
examples/*/src/modules/**/services/*.crud.ts, examples/*/src/modules/**/services/*.data-service.ts
Removes many *.crud.ts files and adds corresponding *.data-service.ts modules that export field schemas and defineCreate/defineUpdate/defineDelete operations; call signatures updated to use where: { ... } and imports rewired.
GraphQL schema & resolvers
examples/*/schema.graphql, examples/*/src/modules/**/schema/*.mutations.ts
Replaces legacy Create/Update input types with consolidated CreateXData/UpdateXData inputs; mutations updated to use new input types, new service imports, and call operations with where: { id } where applicable.
Storage / file handling
examples/*/src/modules/storage/services/file-field.ts, examples/*/src/modules/storage/types/*, examples/*/src/modules/storage/services/validate-file-input.ts
Adds fileField helper and FileInput type, removes validate-file-input module, and generalizes FileCategory/createFileCategory typings.
Removed legacy utilities
examples/*/src/utils/{crud-service-types,data-pipes,embedded-pipes,prisma-relations,arrays}.ts
Deletes legacy helper modules and types (crud-service-types, data-pipes, embedded-pipes, prisma-relations, arrays/notEmpty).
Examples manifests & templates
examples/*/baseplate/file-id-map.json, examples/*/src/utils/.templates-info.json, examples/*/src/utils/data-operations/.templates-info.json
Updates generator/file-id mappings to prisma-data-service, removes old template entries, and adds data-operations template registrations.
Tests & Vitest config / test-helpers
examples/*/vitest.config.ts, packages/core-generators/src/generators/node/vitest/*, packages/core-generators/src/test-helpers/*
Sets maxWorkers: 1 in vitest configs; adds test-helpers (import-map, matchers, utils, setup, types) and unit tests for fragment utilities.
Generators & codegen
packages/fastify-generators/src/..., packages/core-generators/src/...
Adds relation build-data and operation-callback generators/helpers; refactors Pothos mutation generator (name-based, injected args), supports service-file headers, removes legacy data-method generator pieces, and updates provider schemas/imports.
Fastify & core package metadata
packages/fastify-generators/package.json, packages/core-generators/package.json
Adds test script and exports for core-generators test helpers.
Misc config / tooling
.vscode/tasks.json, knip.config.js, package.json
Adjusts Watch Build problemMatcher applyTo, updates knip paths/entries, and bumps knip devDependency version.

Sequence Diagram(s)

%%{init: {"themeVariables":{"actorBorder":"#334155","signal":"#0ea5a4","noteBackground":"#f8fafc"}}}%%
sequenceDiagram
  participant UI as GraphQL UI
  participant Resolver as Mutation Resolver
  participant DataSvc as Data Service (define-*Operation)
  participant Fields as Field Definitions
  participant TX as Prisma TX
  participant DB as Database

  Note over UI,DB: Declarative create/update/delete flow with field transforms, hooks, and tx-bound logic
  UI->>Resolver: mutation(data)
  Resolver->>DataSvc: op({ data, context, query })
  DataSvc->>Fields: validate & transform fields
  Fields-->>DataSvc: preparedData + hooks (may include tx-scoped functions)
  alt preparedData static
    DataSvc->>TX: tx.Model.create/update/delete(preparedData, query)
  else preparedData contains tx-scoped functions
    DataSvc->>TX: resolve tx-scoped pieces then tx.Model.create/update/delete(...)
  end
  TX->>DB: execute SQL
  DB-->>TX: result
  TX-->>DataSvc: result
  DataSvc->>DataSvc: invoke afterCommit hooks (outside tx)
  DataSvc-->>Resolver: result
  Resolver-->>UI: result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Files/areas to inspect closely:

  • src/utils/data-operations/* — lifecycle hooks, tx vs non-tx resolution, composite-key semantics, type inference and exported API.
  • Large removals: data-pipes, embedded-pipes, prisma-relations, arrays, plus many *.crud.ts deletions — search for residual usages/tests referencing removed APIs.
  • Generator changes (pothos/prisma helpers, service-file headers, generate-relation-build-data, generate-operation-callbacks) — verify emitted fragments, injected-arg wiring, and template mappings.
  • Storage file-field and file-category changes — validate file validation flow and storage adapter metadata integration.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly describes the main architectural change—replacing imperative CRUD patterns with a declarative, type-safe data operations system, which aligns with the changeset and file modifications.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
✨ 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-786-exploration-data-transformer-system-redesign

📜 Recent 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 37708c5 and 47feaa1.

⛔ Files ignored due to path filters (4)
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/prisma-types.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/prisma-types.ts is excluded by !**/generated/**, !**/generated/**
  • tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/prisma-types.ts is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/apps/backend/src/utils/data-operations/prisma-types.ts is excluded by !tests/**
📒 Files selected for processing (2)
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-types.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-types.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). (5)
  • GitHub Check: Verify Sync (blog-with-auth)
  • GitHub Check: Verify Sync (todo-with-auth0)
  • GitHub Check: Test E2E
  • GitHub Check: Lint
  • GitHub Check: test

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 19, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedknip@​5.59.0 ⏵ 5.70.098 +110092 +196 +3100

View full report

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

Caution

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

⚠️ Outside diff range comments (1)
examples/todo-with-auth0/apps/backend/src/modules/todos/schema/todo-list.mutations.ts (1)

15-32: Add ownerId validation to reject mismatches with authenticated user

The mutation accepts ownerId directly from input with only null-checking on createdAt. A client can create todo lists for arbitrary users by supplying a different UUID. Example attack: createTodoList(ownerId: "victim-uuid") creates a list owned by that victim, not the authenticated user.

Add a guard in the createTodoList resolver (examples/todo-with-auth0/apps/backend/src/modules/todos/schema/todo-list.mutations.ts, lines 36-41) to validate data.ownerId === context.auth.userId before calling the data-service. Alternatively, derive ownerId exclusively from context.auth.userId and remove it from the public input type.

♻️ Duplicate comments (10)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts (7)

27-64: Same scalarField implementation as blog-with-auth

This scalarField is identical to the one in the blog-with-auth utilities and the same comments apply regarding validation behavior and schema.optional/nullable usage.


80-114: ParentModelConfig/createParentModelConfig mirror blog-with-auth version

These definitions match the blog-with-auth copy; see that file’s comments for rationale and usage notes.


137-185: NestedOneToOneFieldConfig docs vs API

Same note as in the blog-with-auth file: the JSDoc example still mentions deleteRelation, which is not part of the config interface here.


226-355: nestedOneToOneField behavior identical to blog-with-auth

Implementation is duplicated; any fixes or tweaks applied there (e.g., doc updates) should be mirrored here to keep behavior and documentation in sync.


371-431: NestedOneToManyFieldConfig duplication

Same configuration surface and semantics as in the blog-with-auth module; see that file’s comment about careful getWhereUnique implementations.


447-465: expandWhereUnique helper duplication

Behavior and considerations are identical to the blog-with-auth version.


532-748: nestedOneToManyField duplicated; consider reusing guarded getWhereUnique

This is a straight copy of the blog-with-auth implementation, including the local getWhereUnique wrapper and the direct calls to config.getWhereUnique in processedItems. The same recommendation applies: reuse the guarded helper everywhere inside this function.

examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (3)

228-232: Align TPrepareResult generics with the other example app and simplify to object-only

Same comment as for the todo-with-auth0 version: TPrepareResult is constrained as Record<string, unknown> | undefined in config interfaces but defaults to an object type in the builder generics. Dropping | undefined and defaulting both configs and builders to an object type (e.g., Record<string, never>) would avoid confusing intersections and make call sites clearer.

The diff shown in the sibling file applies here as well.

Also applies to: 343-350, 476-480, 599-606


429-435: Refine query typing to avoid casting {} to a required include shape

As in the todo-with-auth0 version, each operation currently casts (query ?? {}) to { include: NonNullable<TQueryArgs['include']> }. This works at runtime but is slightly misleading to the type system when query is undefined.

Adopting a Pick<TQueryArgs, 'include'>-style type for the query parameter in the config functions and corresponding calls would keep the “include-only” contract while avoiding the cast.

Also applies to: 705-712, 909-915


855-931: Mirror delete hook context and error message fixes from the todo-with-auth0 implementation

This delete operation implementation has the same two issues:

  • Error message says “create operations” instead of “delete operations”.
  • afterExecute and afterCommit hooks receive { ...ctx, new: result } instead of { ...ctx, result }, violating the OperationHooks contract and breaking any hooks that use ctx.result.

Apply the same fix here as suggested for the todo-with-auth0 file:

-    if (query?.select) {
-      throw new Error(
-        'Query select is not supported for create operations. Use include instead.',
-      );
-    }
+    if (query?.select) {
+      throw new Error(
+        'Query select is not supported for delete operations. Use include instead.',
+      );
+    }
@@
-        await invokeHooks(allHooks.afterExecute, {
-          ...txContext,
-          new: result,
-        });
+        await invokeHooks(allHooks.afterExecute, {
+          ...txContext,
+          result,
+        });
@@
-      .then(async (result) => {
+      .then(async (result) => {
         // Run afterCommit hooks (outside transaction)
-        await invokeHooks(allHooks.afterCommit, {
-          ...baseOperationContext,
-          new: result,
-        });
+        await invokeHooks(allHooks.afterCommit, {
+          ...baseOperationContext,
+          result,
+        });
         return result as GetPayload<TModelName, TQueryArgs>;
       });

This keeps both example apps’ delete hooks consistent and prevents runtime errors in cleanup logic.

🧹 Nitpick comments (29)
examples/todo-with-auth0/apps/backend/vitest.config.ts (1)

10-10: Document the single-worker constraint and verify test isolation strategy.

Forcing sequential test execution with maxWorkers: 1 will significantly slow down the test suite. While this is often necessary for integration tests that share database state, it should be:

  1. Documented with a comment explaining why parallel execution causes issues
  2. Verified that this resolves the actual test stability problems
  3. Investigated for proper test isolation (e.g., separate test databases per worker, transaction rollbacks, or better cleanup)

Add a comment explaining the constraint:

       globalSetup: './tests/scripts/global-setup.ts',
+      // TODO: Running tests sequentially due to shared database state.
+      // Investigate proper test isolation to enable parallel execution.
       maxWorkers: 1,
       passWithNoTests: true,

Can you confirm:

  1. Whether this change resolves specific test failures or race conditions?
  2. If there are plans to implement proper test isolation to restore parallel execution?
examples/blog-with-auth/apps/backend/vitest.config.ts (1)

10-10: Document the single-worker constraint.

Same concern as in examples/todo-with-auth0/apps/backend/vitest.config.ts: this constraint should be documented with a comment explaining why parallel execution is disabled, and whether this is a temporary workaround or permanent solution.

       globalSetup: './tests/scripts/global-setup.ts',
+      // TODO: Sequential execution required due to shared test resources.
       maxWorkers: 1,
       passWithNoTests: true,
packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts (1)

469-482: Implementation is correct; consider adding JSDoc documentation.

The hoisted fragments support is properly implemented and follows the existing pattern used in formatFragment. The use of object destructuring with a default value ensures backward compatibility.

However, for consistency with other utility methods in this file (e.g., frag, importBuilder, template), consider adding JSDoc comments to document the parameters and behavior.

Example JSDoc:

+  /**
+   * Create a template function that includes specified imports and hoisted fragments.
+   * @param imports - The imports to include in the template.
+   * @param options - Additional options including hoisted fragments.
+   * @returns A template function that processes template literals with embedded expressions.
+   */
   templateWithImports(
     imports?: TsImportDeclaration[] | TsImportDeclaration,
     { hoistedFragments }: { hoistedFragments?: TsHoistedFragment[] } = {},
packages/fastify-generators/src/generators/pothos/pothos-prisma/pothos-prisma.generator.ts (1)

53-64: Good integration of config service dependency.

The wiring follows the established pattern for adding provider dependencies to the generator.

Optional: Consider adding explicit return type.

Per coding guidelines, top-level functions should include return types. While TypeScript infers the type from createGeneratorTask, adding an explicit return type would improve code clarity and align with the guidelines.

packages/core-generators/src/generators/node/vitest/vitest.generator.ts (1)

18-18: Move external import to the top.

Per coding guidelines, external library imports should be grouped before local imports. Move the @baseplate-dev/utils import to lines 1-6 with other external imports.

Apply this diff:

 import {
   createConfigProviderTask,
   createGenerator,
   createGeneratorTask,
 } from '@baseplate-dev/sync';
 import { z } from 'zod';
+import { stringifyPrettyStable } from '@baseplate-dev/utils';

 import { CORE_PACKAGES } from '#src/constants/index.js';
 import { packageScope } from '#src/providers/scopes.js';
 import { extractPackageVersions } from '#src/utils/extract-packages.js';

 import { eslintConfigProvider } from '../eslint/index.js';
 import { createNodePackagesTask, nodeProvider } from '../node/index.js';
 import { typescriptFileProvider } from '../typescript/index.js';

 const descriptorSchema = z.object({});

-import { stringifyPrettyStable } from '@baseplate-dev/utils';
-
 import type { TsCodeFragment } from '#src/renderers/index.js';
packages/fastify-generators/src/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.ts (1)

41-41: Consider using standalone z.uuid() validator.

Zod 4 introduces standalone format schemas. The current 'string().uuid()' works but you could use the newer 'uuid()' to align with Zod 4's standalone validators.

If you'd like to adopt the newer API:

-  uuid: 'string().uuid()',
+  uuid: 'uuid()',

Based on library documentation (Zod 4)

packages/core-generators/src/test-helpers/import-map-helpers.ts (1)

31-42: Type-cast on imports can be avoided with slightly tighter typing

The implementation is functionally correct, but you can avoid the as Record<keyof T, string> cast by typing imports in terms of keyof T up front:

-export function createTestTsImportMap<
-  T extends Record<string, TsImportMapSchemaEntry>,
->(importSchema: T, name: string): InferTsImportMapFromSchema<T> {
-  // Build the imports object based on the schema
-  const imports: Record<string, string> = {};
-
-  for (const key of Object.keys(importSchema)) {
-    imports[key] = `${name}/${key}`;
-  }
-
-  return createTsImportMap(importSchema, imports as Record<keyof T, string>);
-}
+export function createTestTsImportMap<
+  T extends Record<string, TsImportMapSchemaEntry>,
+>(importSchema: T, name: string): InferTsImportMapFromSchema<T> {
+  const imports = {} as Record<keyof T, string>;
+
+  for (const key of Object.keys(importSchema) as Array<keyof T>) {
+    imports[key] = `${name}/${String(key)}`;
+  }
+
+  return createTsImportMap(importSchema, imports);
+}

This keeps the helper fully type-safe without a final assertion.

packages/core-generators/src/test-helpers/vitest-types.d.ts (1)

1-34: Reuse FragmentMatchers type from matchers to avoid duplication and verify import spec

You already export a FragmentMatchers interface from matchers.ts; redefining it here risks the two drifting apart. You can simplify and keep things in sync by importing the type directly:

-import type {
-  ToIncludeImportOptions,
-  ToMatchTsFragmentOptions,
-} from './matchers.ts';
-
-/**
- * TypeScript module augmentation for custom matchers
- * This provides type checking and autocomplete for the custom matchers
- */
-interface FragmentMatchers<R = unknown> {
-
-}
-
-declare module 'vitest' {
-  // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
-  interface Matchers<T = any> extends FragmentMatchers<T> {}
-}
+import type {
+  FragmentMatchers,
+  ToIncludeImportOptions,
+  ToMatchTsFragmentOptions,
+} from './matchers.ts';
+
+/**
+ * TypeScript module augmentation for custom matchers
+ * This provides type checking and autocomplete for the custom matchers
+ */
+declare module 'vitest' {
+  // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
+  interface Matchers<T = any> extends FragmentMatchers<T> {}
+}

Also, please double-check that using the .ts extension in this type-only import aligns with your TS/Node 16 module resolution setup for published d.ts files; other runtime imports in this package use .js.

packages/core-generators/src/test-helpers/utils.unit.test.ts (1)

31-50: Potential locale sensitivity in named import sorting expectation

normalizeImports delegates to localeCompare for sorting named imports, but this test asserts a specific order for mixed‑case names (z, ZodError, ZodSchema). Depending on the runtime’s locale/ICU version, localeCompare can order mixed-case strings differently, which could make this test brittle across environments.

Two options to make this more robust:

  • Normalize case in the implementation before calling localeCompare, if you truly want case-insensitive ordering; or
  • Loosen the test to assert relative ordering only among same-case names (e.g., ZodError before ZodSchema) rather than the exact full array including z.

Either approach reduces the chance of CI failures when Node/ICU is upgraded or when running on different OS locales.

packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts (1)

63-114: Injected-arg handling and dynamic ID wiring look correct

  • handleInjectedArg’s three branches map cleanly to the expected runtime values:
    • contextKindcontext.
    • prismaQueryKindqueryFromInfo({ context, info, path: ['returnFieldName'] }) with the right import.
    • prismaWhereUniqueInputKind → a Prisma where shape that matches single and composite ID conventions when combined with getPrimaryKeyDefinition(prismaModel).
  • The argRequirements set and inputArgs construction correctly add an id input only when a where-unique injected arg is present, while leaving scalar/nested service args as normal Pothos args.
  • The double call to handleInjectedArg (once to collect requirements, once when building SERVICE_ARGUMENTS) is slightly redundant; if you touch this again, you might consider caching the fragments/requirements per arg instead of recomputing, but it’s not functionally problematic.

Also applies to: 164-181, 236-244

packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.unit.test.ts (1)

19-320: Strong coverage of relation scenarios; consider adding upsert tests

These tests do a good job exercising create/update paths, composite keys, optional relations, all‑FK vs passthrough, and partial‑FK errors. The implementation of generateRelationBuildData also has a dedicated operationType === 'upsert' branch, though that path is not covered here. Adding a couple of focused upsert tests (e.g., one with relations and one passthrough) would lock in that behavior and guard against regressions in the more complex branch.

packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1)

176-236: Verify use of Array.prototype.toSorted is compatible with your runtime

generateBuildDataBody uses foreignKeyFields.toSorted() and relationFragments.toSorted(). These toSorted methods are only available in newer JS runtimes; if your code generators run on an older Node version without appropriate lib/target or polyfills, this will throw at runtime.

If you target environments that might not support toSorted natively, consider replacing with the more widely supported pattern or adding a compatibility layer, for example:

const sortedForeignKeyFields = [...foreignKeyFields].sort();
const sortedRelationFragments = [...relationFragments]; // or sort by relationName if you want stable ordering

Please double‑check your configured JS target / Node runtime for these generators and adjust if needed.

examples/todo-with-auth0/apps/backend/src/modules/storage/utils/create-file-category.ts (1)

1-28: Add JSDoc for exported createFileCategory helper

The generic extension over Prisma.FileCountOutputType looks correct, but this exported helper should have JSDoc describing purpose, parameters, and return value to match the examples/todo-with-auth0 documentation standard.

A possible JSDoc:

-export function createFileCategory<
+/**
+ * Validates and returns a strongly typed file category configuration.
+ *
+ * Ensures the name is CONSTANT_CASE and maxFileSize is positive before use.
+ *
+ * @param config File category definition, including its Prisma `referencedByRelation`.
+ * @returns The validated file category configuration.
+ */
+export function createFileCategory<
   TName extends string,
   TReferencedByRelation extends
     keyof Prisma.FileCountOutputType = keyof Prisma.FileCountOutputType,
 >(
   config: FileCategory<TName, TReferencedByRelation>,
 ): FileCategory<TName, TReferencedByRelation> {

Based on learnings.

examples/todo-with-auth0/apps/backend/src/modules/todos/services/todo-list.data-service.ts (1)

15-56: Consider not exposing createdAt as a mutable input field

Allowing clients to supply createdAt can make timeline data untrustworthy and is usually unnecessary when the DB manages this column; consider dropping createdAt from todoListInputFields (and from GraphQL inputs) or restricting it to system/internal flows only.

A minimal change in this file would be:

 export const todoListInputFields = {
   ownerId: scalarField(z.string().uuid()),
   position: scalarField(z.number().int()),
   name: scalarField(z.string()),
-  createdAt: scalarField(z.date().optional()),
   status: scalarField(z.nativeEnum($Enums.TodoListStatus).nullish()),
   coverPhoto: fileField({

(You can still set createdAt internally inside the operation if needed.)

examples/blog-with-auth/apps/backend/src/modules/accounts/services/user.data-service.ts (1)

1-44: Logic is sound; add JSDoc and explicit return types per examples/blog-with-auth guidelines

The userInputFields and createUser/updateUser/deleteUser wiring into define*Operation look correct and consistent with the shared data-operations framework. To align with the blog backend rules for this path:

  • Add JSDoc on the exported data-service functions describing purpose, parameters (data, where, query, context) and return type.
  • Consider making the return types explicit for the exported operations (e.g. via explicit generics on defineCreateOperation / defineUpdateOperation / defineDeleteOperation or a declared function type alias) instead of relying solely on inference.
examples/todo-with-auth0/apps/backend/src/modules/todos/services/todo-item-attachment.data-service.ts (1)

1-33: Nested tag configuration looks correct; consider annotating callback return types

The todoItemAttachmentInputFields and tags nestedOneToManyField config (including the composite todoItemAttachmentId_tag whereUnique) are consistent with the data-operations helpers and should behave as expected for upsert/delete of tags. To better match the “explicit return types” rule for this app, you may want to annotate small callbacks, e.g.:

  • createParentModelConfig('todoItemAttachment', (value): { id: string } => ({ id: value.id }))
  • buildData: (data): typeof data => data
  • getWhereUnique: (input, parentModel): { todoItemAttachmentId_tag: { tag: string; todoItemAttachmentId: string } } | undefined => ...
examples/todo-with-auth0/apps/backend/src/modules/todos/services/todo-list-share.data-service.ts (1)

1-54: TodoListShare wiring looks good; add JSDoc and explicit return types

The use of relationHelpers.connectCreate/connectUpdate for todoList and user plus the shared todoListShareInputFields is a clean fit with the declarative data-operations layer. To align with the backend example guidelines for this tree:

  • Add brief JSDoc on todoListShareInputFields, createTodoListShare, updateTodoListShare, and deleteTodoListShare describing their roles and parameters.
  • Consider adding explicit return types to the exported operations (e.g. via a shared TodoListShareOperation type alias or explicit generics on define*Operation) and, optionally, to the small config callbacks.
examples/todo-with-auth0/apps/backend/src/modules/accounts/users/services/user.data-service.ts (1)

68-96: User data-service operations match the framework; add JSDoc and explicit return types

createUser, updateUser, and deleteUser are wired correctly into defineCreateOperation / defineUpdateOperation / defineDeleteOperation and align with the new nested field setup (and the accompanying integration tests). To match the examples/todo-with-auth0 backend rules:

  • Add JSDoc comments documenting each exported operation (inputs – data, where, query, context – and what they return).
  • Consider making the return types explicit on these exported functions, e.g. by introducing shared type aliases for the operation signatures or by supplying explicit generics to the define*Operation helpers, rather than relying purely on inference.
examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (3)

21-39: Consider exporting FileFieldConfig to match TS type export guidelines

FileFieldConfig is used in the signature of exported fileField but is itself not exported. Given the “export interfaces/types” convention for this codebase, it would be more consistent (and helpful for consumers) to export this config type.

You could change the declaration to:

-interface FileFieldConfig<
+export interface FileFieldConfig<
   TFileCategory extends FileCategory,
   TOptional extends boolean = false,
 > {

Based on learnings.


76-89: Type-level optionality vs. runtime behavior is slightly asymmetric

The TOptional generic influences the FieldDefinition input/output types, but the runtime logic in processInput always accepts FileInput | null | undefined regardless of config.optional. This is probably intentional (the upstream input type controls what is actually allowed), but it’s worth a brief comment in the JSDoc clarifying that optional only affects types, not runtime validation of “field must be present”.


134-147: Uploader scoping via uploaderId is good, but relies on Prisma handling undefined as “omit”

The findUnique call scopes by { id, uploaderId } when not a system user, and uses uploaderId: undefined for system users (effectively falling back to “by id only”). This is a reasonable pattern but depends on Prisma treating undefined properties as omitted from the where clause.

If that behavior ever changes, system users might end up with an invalid query. It may be safer to branch the where object explicitly:

const where = isSystemUser ? { id } : { id, uploaderId };
const file = await prisma.file.findUnique({ where });
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-utils.ts (1)

22-47: Consider exporting GenericPrismaDelegate for reuse and clarity

GenericPrismaDelegate underpins the public return type of makeGenericPrismaDelegate but is not itself exported. Exporting it would better match the project’s “export interfaces/types” guideline and make it easier to reference in other modules (e.g., for mocking or further extensions).

A minimal change:

-interface GenericPrismaDelegate<TModelName extends ModelPropName> {
+export interface GenericPrismaDelegate<TModelName extends ModelPropName> {

Based on learnings.

examples/todo-with-auth0/apps/backend/src/modules/todos/schema/todo-item.mutations.ts (1)

13-41: Confirm GraphQL ID scalar choice for nested attachment IDs

The new nested input types for attachments/tags look consistent, but note that id is now defined via t.id() instead of the previous 'Uuid' scalar. This changes the GraphQL type from a UUID-specific scalar to the generic ID scalar.

If the underlying Prisma column is still a UUID and you want validation to stay strict, consider keeping 'Uuid' here; otherwise, this change is fine but relaxes validation at the GraphQL boundary.

examples/todo-with-auth0/apps/backend/src/modules/accounts/users/schema/user.mutations.ts (1)

21-59: Nested user inputs look good; double-check ID scalar vs Uuid for child entities

The new nested input types for customer, images, roles, and userProfile are coherent and match the expected shapes for the corresponding nested relations. Two small points:

  • id for images and userProfile is defined as t.id() (GraphQL ID) rather than 'Uuid' like fileInputInputType. If these IDs are still UUID-backed in Prisma, consider sticking with the Uuid scalar for stricter validation.
  • restrictObjectNulls on images/userProfile only rejects explicit null while allowing fields to be omitted (undefined), which matches the optional field semantics.

If the looser ID scalar is intentional, this block is otherwise solid.

examples/todo-with-auth0/apps/backend/src/utils/data-operations/types.ts (2)

1-188: Well-structured operation and field typing layer

The overall type design (operation contexts, hooks, field transforms, and inference utilities) is coherent, strongly typed, and matches the declarative data-operations goals. import type usage and JSDoc coverage for exported interfaces/functions are in line with the example app guidelines.


241-255: Consider exporting helper utility types to align with repo-wide TypeScript guidelines

RequiredKeys, OptionalForUndefinedKeys, and Identity are defined as internal helpers but not exported. The repo guideline says that interfaces/types in TS files should be exported; exporting these would also make them reusable in nearby modules (e.g., other data-operations helpers).

You could minimally adjust:

-/** Extracts keys from T where the value type does not include undefined */
-type RequiredKeys<T> = {
+/** Extracts keys from T where the value type does not include undefined */
+export type RequiredKeys<T> = {
   [P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never;
 }[keyof T];

-/** Makes properties with undefined values optional, keeps others required */
-type OptionalForUndefinedKeys<T> = Partial<T> & Pick<T, RequiredKeys<T>>;
+/** Makes properties with undefined values optional, keeps others required */
+export type OptionalForUndefinedKeys<T> = Partial<T> & Pick<T, RequiredKeys<T>>;

-/** Identity type that expands type aliases for better IDE tooltips */
-type Identity<T> = T extends object
+/** Identity type that expands type aliases for better IDE tooltips */
+export type Identity<T> = T extends object
   ? {} & { [P in keyof T]: T[P] }
   : T;

(Based on learnings)

examples/blog-with-auth/apps/backend/src/utils/data-operations/types.ts (1)

241-255: Export internal helper types and consider centralizing shared typings

As in the corresponding todo-with-auth0 file, RequiredKeys, OptionalForUndefinedKeys, and Identity are internal-only, which conflicts slightly with the “export interfaces/types” rule and makes reuse harder across modules.

Same change as suggested there would help:

-/** Extracts keys from T where the value type does not include undefined */
-type RequiredKeys<T> = {
+/** Extracts keys from T where the value type does not include undefined */
+export type RequiredKeys<T> = {
   [P in keyof T]: T[P] extends Exclude<T[P], undefined> ? P : never;
 }[keyof T];

-/** Makes properties with undefined values optional, keeps others required */
-type OptionalForUndefinedKeys<T> = Partial<T> & Pick<T, RequiredKeys<T>>;
+/** Makes properties with undefined values optional, keeps others required */
+export type OptionalForUndefinedKeys<T> = Partial<T> & Pick<T, RequiredKeys<T>>;

-/** Identity type that expands type aliases for better IDE tooltips */
-type Identity<T> = T extends object
+/** Identity type that expands type aliases for better IDE tooltips */
+export type Identity<T> = T extends object
   ? {} & { [P in keyof T]: T[P] }
   : T;

Given this file is effectively duplicated with the todo-with-auth0 version, you might also consider extracting these shared types to a common module to avoid divergence over time.

examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (2)

228-232: Unify TPrepareResult generics to avoid undefined intersections and simplify typing

For both create and update configs/builders, TPrepareResult is declared as:

  • In CreateOperationConfig / UpdateOperationConfig:
TPrepareResult extends Record<string, unknown> | undefined = undefined;
  • In defineCreateOperation / defineUpdateOperation generics:
TPrepareResult extends Record<string, unknown> | undefined = Record<string, never>;

Mixing | undefined with an object default leads to slightly confusing types (InferFieldsX & TPrepareResult can conceptually intersect with undefined). At runtime you always spread preparedData into an object, and you already fall back to Promise.resolve(undefined as TPrepareResult).

It would be clearer and safer to treat TPrepareResult as always-object (possibly empty) and drop | undefined entirely, e.g.:

-export interface CreateOperationConfig<
-  TModelName extends ModelPropName,
-  TFields extends Record<string, AnyFieldDefinition>,
-  TPrepareResult extends Record<string, unknown> | undefined = undefined,
-> {
+export interface CreateOperationConfig<
+  TModelName extends ModelPropName,
+  TFields extends Record<string, AnyFieldDefinition>,
+  TPrepareResult extends Record<string, unknown> = Record<string, never>,
+> {
   // ...
 }

and mirror that pattern in the defineCreateOperation and update counterparts. Callers that don’t need computed fields can just accept the empty record.

Also applies to: 343-350, 476-480, 599-606


429-435: Type-cast of query to required include object is slightly misleading

In all three operations you type the delegate call input as requiring query: { include: NonNullable<TQueryArgs['include']> }, then satisfy it via:

query: (query ?? {}) as {
  include: NonNullable<TQueryArgs['include']>;
};

At runtime {} is valid (spreads to nothing), but this cast effectively lies to the type system when query is undefined, and enforces “include-only” via convention rather than type structure.

If you want to keep “no select allowed” but allow query to be omitted, consider something like:

-    query: { include: NonNullable<TQueryArgs['include']> };
+    query: Pick<TQueryArgs, 'include'>;

and let the inner create/update/delete simply spread ...query (which might be {} or { include: ... }). The explicit if (query?.select) guard already prevents mis-use.

Also applies to: 705-712, 909-915

Comment on lines +1 to +5
---
'@baseplate-dev/utils': patch
---

Add case utils to utils package
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

Changeset description may be incomplete or mismatch PR scope.

The changeset format is correct (YAML frontmatter with patch type and description body), but the description "Add case utils to utils package" appears narrow compared to the PR's stated objective of introducing an extensive data-operations architecture redesign with new utilities, templates, GraphQL updates, and service layer changes.

Additionally, if this PR modifies multiple packages, each package should have its own changeset entry (or multiple entries in this file).

Run the script below to identify all packages modified in this PR:


🏁 Script executed:

#!/bin/bash
# Description: Identify which packages are modified in this PR to determine if additional changesets are needed

# Find all package.json files and check if they're modified
git diff --name-only | grep -E 'package\.json$' | head -20

# Show summary of file changes by directory depth
echo "=== Modified files by top-level directory ==="
git diff --name-only | cut -d'/' -f1 | sort | uniq -c | sort -rn

Length of output: 45


Changeset format is correct; scope concern is partially misguided.

Changesets allow adding more packages or changing bump types by editing the file, and the YAML frontmatter can include multiple package entries. The current file follows proper format with valid YAML frontmatter and description.

However, the description "Add case utils to utils package" may still be too narrow. If this PR modifies packages beyond @baseplate-dev/utils (such as core utilities or other dependencies), either add those package entries to this changeset's frontmatter or create separate changesets for them. Verify by checking which packages have actual code changes in the PR.

🤖 Prompt for AI Agents
In .changeset/red-cycles-wave.md around lines 1 to 5, the changeset frontmatter
currently lists only '@baseplate-dev/utils' and the description "Add case utils
to utils package" may be too narrow; inspect the PR to determine all packages
actually changed and either add additional package entries (with appropriate
bump types) to this file's YAML frontmatter or create separate changeset files
for other packages, and update the description to clearly summarize all changes
included in this PR; ensure the final YAML frontmatter accurately reflects every
package affected and that the description is broad enough to cover all
modifications.

Comment on lines +137 to +185
export interface NestedOneToOneFieldConfig<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
> {
/**
* Prisma model name of parent model
*/
parentModel: ParentModelConfig<TParentModelName>;

/**
* Prisma model name of the child model
*/
model: TModelName;

/**
* Relation name of the parent model from the child model
*/
relationName: TRelationName;

/**
* Field definitions for the nested entity
*/
fields: TFields;

/**
* Extract where unique from parent model
*/
getWhereUnique: (
parentModel: GetPayload<TParentModelName>,
) => WhereUniqueInput<TModelName>;

/**
* Transform validated field data into final Prisma structure
*/
buildData: (
data: {
create: InferFieldsCreateOutput<TFields> &
Record<TRelationName, { connect: WhereUniqueInput<TParentModelName> }>;
update: InferFieldsUpdateOutput<TFields>;
},
parentModel: GetPayload<TParentModelName>,
ctx: TransactionalOperationContext<
GetPayload<TModelName>,
{ hasResult: false }
>,
) => PrismaFieldData<TModelName> | Promise<PrismaFieldData<TModelName>>;
}
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

NestedOneToOneFieldConfig docs slightly out of sync with current API

The config interface for one-to-one fields looks solid, but the JSDoc example below still refers to a deleteRelation property that no longer exists on NestedOneToOneFieldConfig.

To avoid confusion for consumers, either:

  • Reintroduce deleteRelation into the config (if still desired), or
  • Update the example to match the current shape (no deleteRelation) and explain that relation deletion is handled via { delete: true } in the nested update data.
🤖 Prompt for AI Agents
In
examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
around lines 137 to 185, the JSDoc/example for NestedOneToOneFieldConfig still
references a removed deleteRelation property; update the example to reflect the
current interface (no deleteRelation) and explicitly show how to delete the
relation using nested update syntax (e.g., include a brief example line showing
{ delete: true } in the nested update payload), or if deleteRelation behavior is
intended to remain part of the API, reintroduce and type that property
consistently in the interface instead — pick one approach and make the code and
docs consistent.

Comment on lines +532 to +748
export function nestedOneToManyField<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
>(
config: NestedOneToManyFieldConfig<
TParentModelName,
TModelName,
TRelationName,
TFields
>,
): FieldDefinition<
InferInput<TFields>[] | undefined,
undefined,
undefined | { deleteMany: Record<never, never> }
> {
const getWhereUnique = (
input: InferInput<TFields>,
originalModel: GetPayload<TParentModelName>,
): WhereUniqueInput<TModelName> | undefined => {
const whereUnique = config.getWhereUnique(input, originalModel);
if (whereUnique && Object.values(whereUnique).includes(undefined)) {
throw new Error(
'getWhereUnique cannot return any undefined values in the object',
);
}
return whereUnique;
};

return {
processInput: async (value, processCtx) => {
const { serviceContext, loadExisting } = processCtx;

if (value === undefined) {
return { data: { create: undefined, update: undefined } };
}

const existingModel = (await loadExisting()) as
| GetPayload<TParentModelName>
| undefined;

// Filter objects that relate to parent model only
const whereFromOriginalModel = existingModel && {
[config.relationName]: expandWhereUnique(
config.parentModel.getWhereUnique(existingModel),
),
};
// Handle list of items
const delegate = makeGenericPrismaDelegate(prisma, config.model);

const cachedLoadExisting = value.map((itemInput) => {
let cachedExisting: GetPayload<TModelName> | undefined;
const whereUnique =
existingModel && getWhereUnique(itemInput, existingModel);

return async (): Promise<GetPayload<TModelName> | undefined> => {
if (cachedExisting) return cachedExisting;
if (!whereUnique) return undefined;
cachedExisting =
(await delegate.findUnique({
where: { ...whereUnique, ...whereFromOriginalModel },
})) ?? undefined;
return cachedExisting;
};
});

const processedItems = await Promise.all(
value.map(async (itemInput, idx) => {
const whereUnique =
existingModel && config.getWhereUnique(itemInput, existingModel);

const { data, hooks } = await transformFields(
config.fields,
itemInput,
{
serviceContext,
operation: 'upsert',
allowOptionalFields: false,
loadExisting: cachedLoadExisting[idx] as () => Promise<
object | undefined
>,
},
);

return { whereUnique, data, hooks };
}),
);

const beforeExecuteHook = async (
ctx: TransactionalOperationContext<
GetPayload<TParentModelName>,
{ hasResult: false }
>,
): Promise<void> => {
await Promise.all(
processedItems.map((item, idx) =>
invokeHooks(item.hooks.beforeExecute, {
...ctx,
loadExisting: cachedLoadExisting[idx],
}),
),
);
};

const results: (GetPayload<TModelName> | undefined)[] = Array.from(
{ length: value.length },
() => undefined,
);
const afterExecuteHook = async (
ctx: TransactionalOperationContext<
GetPayload<TParentModelName>,
{ hasResult: true }
>,
): Promise<void> => {
const prismaDelegate = makeGenericPrismaDelegate(ctx.tx, config.model);

// Delete items not in the input
if (whereFromOriginalModel) {
const keepFilters = processedItems
.map((item) => expandWhereUnique(item.whereUnique))
.filter(
(where): where is WhereInput<TModelName> => where !== undefined,
)
.map((where) => ({ NOT: where }));

const deleteWhere =
keepFilters.length === 0
? whereFromOriginalModel
: ({
AND: [whereFromOriginalModel, ...keepFilters],
} as WhereInput<TModelName>);

await prismaDelegate.deleteMany({ where: deleteWhere });
}

// Upsert items
await Promise.all(
processedItems.map(async (item, idx) => {
const awaitedData =
typeof item.data === 'function'
? await item.data(ctx.tx)
: item.data;

const parentWhereUnique = config.parentModel.getWhereUnique(
ctx.result,
);

const builtData = await config.buildData(
{
create: {
...awaitedData.create,
...({
[config.relationName]: { connect: parentWhereUnique },
} as Record<
TRelationName,
{ connect: WhereUniqueInput<TParentModelName> }
>),
},
update: awaitedData.update,
},
ctx.result,
{
...ctx,
operation: item.whereUnique ? 'update' : 'create',
loadExisting: cachedLoadExisting[idx],
result: undefined,
},
);

results[idx] = item.whereUnique
? await prismaDelegate.upsert({
where: item.whereUnique,
create: builtData.create,
update: builtData.update,
})
: await prismaDelegate.create({
data: builtData.create,
});

await invokeHooks(item.hooks.afterExecute, {
...ctx,
result: results[idx],
loadExisting: cachedLoadExisting[idx],
});
}),
);
};

const afterCommitHook = async (
ctx: OperationContext<
GetPayload<TParentModelName>,
{ hasResult: true }
>,
): Promise<void> => {
await Promise.all(
processedItems.map((item, idx) =>
invokeHooks(item.hooks.afterCommit, {
...ctx,
result: results[idx],
loadExisting: cachedLoadExisting[idx],
}),
),
);
};

return {
data: {},
hooks: {
beforeExecute: [beforeExecuteHook],
afterExecute: [afterExecuteHook],
afterCommit: [afterCommitHook],
},
};
},
};
}
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

Reuse guarded getWhereUnique in nestedOneToManyField to enforce invariants consistently

The overall flow in nestedOneToManyField (loading existing children, processing each item via transformFields, deleting missing items, then upserting/creating as needed and invoking hooks) is well thought out.

One small robustness issue:

  • You define a local getWhereUnique wrapper that throws if config.getWhereUnique returns an object containing any undefined values.
  • However, in processedItems you still call config.getWhereUnique directly:
    • This whereUnique is later used in upsert({ where: item.whereUnique, ... }), so a mis-implemented getWhereUnique (e.g. { id } where id might be undefined) can still slip through and reach Prisma.

To fully enforce the invariant, consider:

  • Using the local getWhereUnique helper everywhere inside this function (both in cachedLoadExisting and in processedItems), so any undefined values are consistently rejected up front.

The rest of the algorithm (building deleteWhere, wiring parent connects, and hook propagation) looks sound.


I'll verify the review comment by examining the actual code and confirming whether the inconsistency exists.


🏁 Script executed:

#!/bin/bash
# Extract the nestedOneToManyField function to analyze getWhereUnique usage patterns
fd 'field-definitions.ts' --type f | head -1 | xargs cat | grep -n "getWhereUnique"

Length of output: 1300


🏁 Script executed:

#!/bin/bash
# More detailed inspection: show context around getWhereUnique calls
fd 'field-definitions.ts' --type f | head -1 | xargs cat | grep -n -B2 -A2 "getWhereUnique"

Length of output: 4596


🏁 Script executed:

#!/bin/bash
# Use ast-grep to find the function definition and analyze its structure
fd 'field-definitions.ts' --type f | head -1 | xargs ast-grep --pattern 'export function nestedOneToManyField<$_>($_): $_ {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Inspect the afterExecuteHook where processedItems[].whereUnique is used
fd 'field-definitions.ts' --type f | head -1 | xargs sed -n '630,720p'

Length of output: 2868


Use the local getWhereUnique wrapper consistently in processedItems to prevent undefined values from reaching Prisma

The review comment accurately identifies a real inconsistency. At line 549–558, a getWhereUnique wrapper is defined to validate that config.getWhereUnique output contains no undefined values. This wrapper is applied when building cachedLoadExisting (line 586), but bypassed in processedItems (line 602), where config.getWhereUnique is called directly.

The unvalidated whereUnique from line 602 is then stored and passed directly to the Prisma upsert call at line 710. A misconfigured getWhereUnique returning an object with undefined values (e.g., { id: undefined }) would be caught by the wrapper in one path but slip through in the other, creating an inconsistency that could lead to runtime Prisma errors.

Fix: Replace line 602 to use the wrapper:

const whereUnique = existingModel && getWhereUnique(itemInput, existingModel);
🤖 Prompt for AI Agents
In
examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
around lines 532 to 748, the processedItems mapping at ~line 602 calls
config.getWhereUnique(...) directly which bypasses the local getWhereUnique
wrapper that validates no undefined values; replace that direct call with the
wrapper (i.e., use getWhereUnique(itemInput, existingModel)) so the same
validation is applied and only validated whereUnique objects are stored and
later passed to Prisma.

Comment on lines +22 to +47
interface GenericPrismaDelegate<TModelName extends ModelPropName> {
findUnique: (args: {
where: WhereUniqueInput<TModelName>;
}) => Promise<GetPayload<TModelName> | null>;
findMany: (args: {
where: WhereInput<TModelName>;
}) => Promise<GetPayload<TModelName>[]>;
create: (args: {
data: CreateInput<TModelName>;
}) => Promise<GetPayload<TModelName>>;
update: (args: {
where: WhereUniqueInput<TModelName>;
data: UpdateInput<TModelName>;
}) => Promise<GetPayload<TModelName>>;
upsert: (args: {
where: WhereUniqueInput<TModelName>;
create: CreateInput<TModelName>;
update: UpdateInput<TModelName>;
}) => Promise<GetPayload<TModelName>>;
delete: (args: {
where: WhereUniqueInput<TModelName>;
}) => Promise<GetPayload<TModelName>>;
deleteMany: (args: {
where: WhereInput<TModelName>;
}) => Promise<{ count: number }>;
}
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

Export GenericPrismaDelegate so the public signature compiles

makeGenericPrismaDelegate is exported and its return type references the local GenericPrismaDelegate, which is not exported. Under isolatedModules this yields TS2742: exported function ... has or is using name 'GenericPrismaDelegate' but cannot be named, blocking compilation. Please export the interface (or inline its shape) so the public signature is valid.

-interface GenericPrismaDelegate<TModelName extends ModelPropName> {
+export interface GenericPrismaDelegate<TModelName extends ModelPropName> {
🤖 Prompt for AI Agents
In
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-utils.ts
around lines 22 to 47, the GenericPrismaDelegate interface is used in the
exported makeGenericPrismaDelegate signature but is not exported, causing TS2742
under isolatedModules; export the interface (add "export" before the interface
declaration) or replace the returned type with an exported inline
type/explicitly exported alias so the public function signature references only
exported types and the module compiles.

Comment on lines +141 to +143
const orderedHeaders = headersContainer
.getValue()
.sort((a, b) => a.name.localeCompare(b.name));
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

Consider using locale-independent sorting for determinism.

Using localeCompare() without an explicit locale parameter can produce different ordering across different system locales, potentially affecting build reproducibility.

Apply this diff for deterministic sorting:

-            const orderedHeaders = headersContainer
-              .getValue()
-              .sort((a, b) => a.name.localeCompare(b.name));
+            const orderedHeaders = headersContainer
+              .getValue()
+              .sort((a, b) => a.name.localeCompare(b.name, 'en-US'));

Alternatively, for maximum performance and simplicity:

-            const orderedHeaders = headersContainer
-              .getValue()
-              .sort((a, b) => a.name.localeCompare(b.name));
+            const orderedHeaders = headersContainer
+              .getValue()
+              .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
📝 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 orderedHeaders = headersContainer
.getValue()
.sort((a, b) => a.name.localeCompare(b.name));
const orderedHeaders = headersContainer
.getValue()
.sort((a, b) => a.name.localeCompare(b.name, 'en-US'));
Suggested change
const orderedHeaders = headersContainer
.getValue()
.sort((a, b) => a.name.localeCompare(b.name));
const orderedHeaders = headersContainer
.getValue()
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
🤖 Prompt for AI Agents
In
packages/fastify-generators/src/generators/core/service-file/service-file.generator.ts
around lines 141 to 143, the code uses a.name.localeCompare(b.name) which can
vary with system locale; change the sort to be locale-independent and
deterministic by either calling localeCompare with an explicit locale and
options (e.g., 'en' or 'en-US' and { sensitivity: 'base' }) or replace it with a
plain Unicode code-point comparison like (a.name === b.name ? 0 : a.name <
b.name ? -1 : 1) so ordering is stable across environments.

hasPrimaryKeyInputType: z.boolean(),
});

type InjectedArgRequirements = 'context' | 'info' | 'id';
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export InjectedArgRequirements to follow repo-wide type export convention

Project guidelines require interfaces/types in *.ts files to be exported. InjectedArgRequirements is currently local:

type InjectedArgRequirements = 'context' | 'info' | 'id';

Recommend exporting it to stay consistent and enable reuse:

-type InjectedArgRequirements = 'context' | 'info' | 'id';
+export type InjectedArgRequirements = 'context' | 'info' | 'id';
🤖 Prompt for AI Agents
In
packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
around line 63, the local type alias "InjectedArgRequirements" is not exported
which violates the repo convention for exporting types; change the declaration
to export the type (e.g., add the export keyword before the type definition) so
it becomes part of the module's public types, and update any imports/usages
elsewhere if necessary to import it from this file.

Comment on lines 31 to 43
const SCALAR_TYPE_TO_ZOD_TYPE: Record<ScalarFieldType, string> = {
string: 'string()',
int: 'number().int()',
float: 'number()',
decimal: 'number()',
boolean: 'boolean()',
date: 'date()',
dateTime: 'date()',
json: 'unknown()',
jsonObject: 'record(unknown())',
uuid: 'string().uuid()',
enum: '',
};
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

Fix invalid Zod record schema for jsonObject type.

The jsonObject mapping on line 40 generates invalid Zod code that will fail at runtime:

  1. unknown() is not a valid Zod schema—it needs the z. prefix
  2. According to Zod 4 documentation: "z.record() now requires both key and value schemas"

The generated code z.record(unknown()) is invalid on both counts.

Apply this diff to fix the record schema:

-  jsonObject: 'record(unknown())',
+  jsonObject: 'record(z.string(), z.unknown())',

This generates z.record(z.string(), z.unknown()) which correctly validates a record with string keys and unknown values.

Based on library documentation (Zod 4)

🤖 Prompt for AI Agents
In
packages/fastify-generators/src/generators/prisma/_shared/field-definition-generators/generate-scalar-input-field.ts
around lines 31 to 43, the jsonObject mapping currently emits an invalid Zod
schema; replace the value for the jsonObject key from "record(unknown())" to
"record(string(), unknown())" so the generated code produces
z.record(z.string(), z.unknown()) at runtime (matching the existing pattern of
leaving off the "z." prefix in the map entries).

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

♻️ Duplicate comments (2)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1)

856-860: Fix copy-paste error and critical hook context bug in delete operation.

This delete operation has two issues that were flagged in previous reviews:

  1. Error message copy-paste issue (lines 857-858): References "create operations" instead of "delete operations"

  2. Critical bug in hook context (lines 918, 927): Both afterExecute and afterCommit hooks receive new: result instead of result: result. The OperationHooks interface expects ctx.result, and the JSDoc example (line 832) shows hooks accessing ctx.result.id. With the current code, ctx.result is undefined, breaking any delete hook that tries to access the deleted record.

Apply this diff to fix both issues:

       if (query?.select) {
         throw new Error(
-          'Query select is not supported for create operations. Use include instead.',
+          'Query select is not supported for delete operations. Use include instead.',
         );
       }
         // Run afterExecute hooks
         await invokeHooks(allHooks.afterExecute, {
           ...txContext,
-          new: result,
+          result,
         });

         return result;
       })
       .then(async (result) => {
         // Run afterCommit hooks (outside transaction)
         await invokeHooks(allHooks.afterCommit, {
           ...baseOperationContext,
-          new: result,
+          result,
         });
         return result as GetPayload<TModelName, TQueryArgs>;
       });

Also applies to: 916-919, 924-928

packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1)

294-310: Upsert update path still uses create semantics for relation helpers

In the upsert branch, the updateDataBody is still built with operationType: 'create', so the update half of an upsert will incorrectly use connectCreate semantics instead of connectUpdate:

const updateDataBody = generateBuildDataBody(
  foreignKeyFieldNames,
  'create', // <- should be 'update'
  dataUtilsImports,
  relevantRelations,
  inputFieldNames,
  'updateData',
);

This will generate wrong relation helper calls for the update part of an upsert.

Recommend changing the second argument to 'update':

-    const updateDataBody = generateBuildDataBody(
-      foreignKeyFieldNames,
-      'create',
+    const updateDataBody = generateBuildDataBody(
+      foreignKeyFieldNames,
+      'update',
       dataUtilsImports,
       relevantRelations,
       inputFieldNames,
       'updateData',
     );

and ensuring upsert-specific tests assert that create uses connectCreate while update uses connectUpdate.

📜 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 c360904 and e224dec.

⛔ Files ignored due to path filters (5)
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !tests/**, !**/generated/**
  • tests/simple/apps/backend/src/utils/data-operations/define-operations.ts is excluded by !tests/**
📒 Files selected for processing (7)
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • knip.config.js (2 hunks)
  • package.json (1 hunks)
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts (1 hunks)
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1 hunks)
  • packages/fastify-generators/src/generators/prisma/_shared/crud-method/primary-key-input.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/fastify-generators/src/generators/prisma/_shared/crud-method/primary-key-input.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*

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

Use kebab-case for file names

Files:

  • package.json
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts
  • knip.config.js
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{ts,tsx}

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

**/*.{ts,tsx}: TypeScript with strict type checking
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 a particular interface or type is not exported, update the TypeScript file so it is exported

Files:

  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{js,ts,tsx}

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

Node 16 module resolution - include file extensions in imports (.js)

Files:

  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts
  • knip.config.js
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{ts,tsx,js}

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

**/*.{ts,tsx,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
We use the prefer using nullish coalescing operator (??) ESLint rule instead of a logical or (||), as it is a safer operator
Use console.info/warn/error instead of console.log

Files:

  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts
  • knip.config.js
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}

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

examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}: Always use .js file extensions in import statements, even when importing from .ts files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes, documenting purpose, parameters, return values, and all fields

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}: Always use .js extensions in import statements, even when importing from TypeScript files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

Use kebab-case for all file and directory names

Files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
🧠 Learnings (19)
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx} : Include absolute paths in import statements via tsconfig paths (`src/` is the alias for `src/`)

Applied to files:

  • knip.config.js
📚 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:

  • knip.config.js
📚 Learning: 2025-08-17T01:32:58.983Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 633
File: packages/project-builder-web/src/routes/admin-sections.$appKey/-components/columns/column-configs.ts:1-2
Timestamp: 2025-08-17T01:32:58.983Z
Learning: The project-builder-web package doesn't use ESM (ECMAScript modules), so .js extensions are not required in import statements, unlike other packages in the codebase that do use Node 16 ESM resolution.

Applied to files:

  • knip.config.js
📚 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:

  • knip.config.js
📚 Learning: 2025-07-22T09:11:29.223Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/ui-rules.mdc:0-0
Timestamp: 2025-07-22T09:11:29.223Z
Learning: Applies to {packages/project-builder-web/**,packages/ui-components/**}/*.tsx : In plugins, prefix all Tailwind classes with the plugin name (e.g., `auth-`, `storage-`)

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{js,ts,tsx} : Node 16 module resolution - include file extensions in imports (`.js`)

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-30T13:27:20.000Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 623
File: plugins/plugin-auth/src/local-auth/core/generators/react-session/templates/src/app/user-session-provider.tsx:6-12
Timestamp: 2025-07-30T13:27:20.000Z
Learning: Ignore ESLint import ordering rules for files in templates/** directories as these are code generation templates, not direct source code.

Applied to files:

  • knip.config.js
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{unit,int}.test.ts : Collocate tests with source files (tests live under the src tree)

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-22T09:11:29.223Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/ui-rules.mdc:0-0
Timestamp: 2025-07-22T09:11:29.223Z
Learning: Applies to {packages/project-builder-web/**,packages/ui-components/**}/*.tsx : Use ShadCN-based components from `baseplate-dev/ui-components` and always prefer these components over creating custom ones

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-14T12:02:36.595Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 609
File: packages/ui-components/src/components/badge/badge-with-icon.stories.tsx:3-3
Timestamp: 2025-07-14T12:02:36.595Z
Learning: For TypeScript/TSX files: `#src/` is the new path alias standard for `src/` directory imports, replacing the previous `src/` convention.

Applied to files:

  • knip.config.js
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Use `import type` for type-only imports

Applied to files:

  • knip.config.js
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:00:29.010Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/blog-with-auth/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:00:29.010Z
Learning: Applies to examples/blog-with-auth/apps/**/src/**/*.{ts,tsx} : Use `import type` for type-only imports

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx} : TypeScript with strict type checking

Applied to files:

  • knip.config.js
📚 Learning: 2025-06-03T09:11:29.651Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 562
File: plugins/plugin-auth/package.json:32-36
Timestamp: 2025-06-03T09:11:29.651Z
Learning: With TypeScript project references, TypeScript compilation is watched from the root level using "watch:tsc:root" script, so individual packages no longer need to include their "tsc:watch" scripts in their local "watch" commands. The local "tsc:watch" scripts are renamed from "watch:tsc" but are not meant to be run as part of the package's watch command since TypeScript watching is handled centrally at the workspace root.

Applied to files:

  • knip.config.js
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx,js} : Sort imports by group: external libs first, then local imports

Applied to files:

  • knip.config.js
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Applied to files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Always specify explicit return types for all functions

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{tsconfig*.json,apps/{admin,backend}/tsconfig*.json} : Use TypeScript NodeNext module resolution and module ("moduleResolution": "NodeNext", "module": "NodeNext")

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:00:29.010Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/blog-with-auth/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:00:29.010Z
Learning: Applies to examples/blog-with-auth/apps/**/src/**/*.{ts,tsx} : Add JSDoc to all exported functions, interfaces, and classes, documenting purpose, parameters, return values, and all fields

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
🧬 Code graph analysis (4)
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts (4)
packages/fastify-generators/src/types/prisma-output.ts (1)
  • PrismaOutputModel (32-36)
packages/core-generators/src/renderers/typescript/fragments/types.ts (1)
  • TsCodeFragment (55-72)
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1)
  • generateRelationBuildData (281-347)
packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts (1)
  • tsTemplate (587-587)
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (4)
packages/fastify-generators/src/types/prisma-output.ts (2)
  • PrismaOutputModel (32-36)
  • PrismaOutputRelationField (20-26)
packages/fastify-generators/src/generators/prisma/data-utils/index.ts (1)
  • DataUtilsImportsProvider (2-2)
packages/core-generators/src/renderers/typescript/fragments/types.ts (1)
  • TsCodeFragment (55-72)
packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts (2)
  • TsCodeUtils (85-584)
  • tsTemplate (587-587)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (5)
examples/blog-with-auth/apps/backend/src/utils/data-operations/types.ts (12)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • InferFieldsOutput (335-342)
  • AnyOperationHooks (125-125)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/types.ts (11)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • AnyOperationHooks (125-125)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
⏰ 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). (1)
  • GitHub Check: Lint
🔇 Additional comments (6)
knip.config.js (3)

38-40: LGTM!

The paths mapping addition is consistent with the pattern used across other packages in the workspace.


131-134: LGTM!

The ignoreDependencies addition is well-documented and follows the same pattern used elsewhere in the configuration (e.g., packages/project-builder-cli).


126-126: Entry point file verified to exist.

The file packages/create-project/src/create-baseplate-project.ts exists at the location referenced in the knip.config.js entry point configuration. The change is valid.

package.json (1)

64-64: Knip 5.70.0 is the latest version and safe to upgrade.

Version 5.70.0 was released 21 hours ago and is available on npm. Version 5 includes many enhancements with no breaking changes, and Knip has been largely stable since version 3. The bump from 5.59.0 to 5.70.0 involves only minor version increments within the stable v5 range, so no breaking changes or configuration updates are required.

packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts (1)

57-92: Callback generation logic reads clean and consistent with relation-builder API

The create/update/delete callback generators correctly:

  • Use generateRelationBuildData for create/update, respecting the passthrough flag.
  • Destructure data with the relation argument fragment and pass the transformed returnFragment into Prisma calls.
  • Keep delete as a simple passthrough without unnecessary relation handling.

No functional concerns from this file as long as generateRelationBuildData is correct.

Also applies to: 141-178, 209-223

packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1)

192-193: Review comment is incorrect — Node.js 22 fully supports Array.prototype.toSorted

The codebase targets Node 22 ("engines": "node": "^22.0.0"), which has native support for Array.prototype.toSorted since Node 20. The code at lines 192-193 and 213-214 is already compatible with the required runtime and requires no changes.

Likely an incorrect or invalid review comment.

Comment on lines +14 to +31
interface GenerateCreateCallbackConfig {
/** Prisma model to analyze for relations */
prismaModel: PrismaOutputModel;
/** Field names that are included in the input */
inputFieldNames: string[];
/** Data utils imports provider for accessing relationHelpers fragments */
dataUtilsImports: DataUtilsImportsProvider;
/** Prisma model variable name in camelCase (e.g., 'todoItem', 'user') */
modelVariableName: string;
}

/**
* Result of generating create operation callback
*/
interface GenerateCreateCallbackResult {
/** Complete create callback fragment: ({ tx, data, query }) => tx.model.create({...}) */
createCallbackFragment: TsCodeFragment;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export callback config/result interfaces to align with project guidelines

The config/result interfaces for create/update/delete are currently internal (interface without export). Project guidelines say interfaces/types should be exported; these also form a useful public API surface for the generators.

Suggest exporting them:

-interface GenerateCreateCallbackConfig {
+export interface GenerateCreateCallbackConfig {
@@
-interface GenerateCreateCallbackResult {
+export interface GenerateCreateCallbackResult {
@@
-interface GenerateUpdateCallbackConfig {
+export interface GenerateUpdateCallbackConfig {
@@
-interface GenerateUpdateCallbackResult {
+export interface GenerateUpdateCallbackResult {
@@
-interface GenerateDeleteCallbackConfig {
+export interface GenerateDeleteCallbackConfig {
@@
-interface GenerateDeleteCallbackResult {
+export interface GenerateDeleteCallbackResult {

Also applies to: 94-114, 183-194

🤖 Prompt for AI Agents
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.ts
lines 14-31 (and likewise for 94-114, 183-194): the callback config/result
interfaces are declared non-exported; change each relevant declaration (e.g.,
GenerateCreateCallbackConfig, GenerateCreateCallbackResult and the equivalent
update/delete interfaces found at the other ranges) from "interface ..." to
"export interface ..." so they become part of the module's public API and align
with project guidelines.

Comment on lines +15 to +38
interface GenerateRelationBuildDataConfig {
/** Prisma model to analyze for relations */
prismaModel: PrismaOutputModel;
/** Field names that are included in the input (to determine which relations to include) */
inputFieldNames: string[];
/** Operation type - determines whether to use connectCreate or connectUpdate */
operationType: 'create' | 'update' | 'upsert';
/** Data utils imports provider for accessing relationHelpers fragments */
dataUtilsImports: DataUtilsImportsProvider;
}

/**
* Result of generating relation buildData function
*/
interface GenerateRelationBuildDataResult {
/** Argument pattern for the function (e.g., "{ ownerId, ...data }" or "data") */
argumentFragment: TsCodeFragment;
/** Return value with relation transformations (e.g., "{ ...data, owner: relationHelpers.connectCreate(...) }") */
returnFragment: TsCodeFragment;
/** Whether this is a simple passthrough (no relations to transform) */
passthrough: boolean;
/** Complete buildData function fragment: ({ fk1, fk2, ...data }) => ({ ...data, relation1: ..., relation2: ... }) */
buildDataFunctionFragment: TsCodeFragment;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export relation buildData config/result interfaces

GenerateRelationBuildDataConfig and GenerateRelationBuildDataResult are internal, but guidelines require interfaces/types to be exported, and these are likely useful to consumers/tests of this module.

Recommended change:

-interface GenerateRelationBuildDataConfig {
+export interface GenerateRelationBuildDataConfig {
@@
-interface GenerateRelationBuildDataResult {
+export interface GenerateRelationBuildDataResult {
📝 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
interface GenerateRelationBuildDataConfig {
/** Prisma model to analyze for relations */
prismaModel: PrismaOutputModel;
/** Field names that are included in the input (to determine which relations to include) */
inputFieldNames: string[];
/** Operation type - determines whether to use connectCreate or connectUpdate */
operationType: 'create' | 'update' | 'upsert';
/** Data utils imports provider for accessing relationHelpers fragments */
dataUtilsImports: DataUtilsImportsProvider;
}
/**
* Result of generating relation buildData function
*/
interface GenerateRelationBuildDataResult {
/** Argument pattern for the function (e.g., "{ ownerId, ...data }" or "data") */
argumentFragment: TsCodeFragment;
/** Return value with relation transformations (e.g., "{ ...data, owner: relationHelpers.connectCreate(...) }") */
returnFragment: TsCodeFragment;
/** Whether this is a simple passthrough (no relations to transform) */
passthrough: boolean;
/** Complete buildData function fragment: ({ fk1, fk2, ...data }) => ({ ...data, relation1: ..., relation2: ... }) */
buildDataFunctionFragment: TsCodeFragment;
}
export interface GenerateRelationBuildDataConfig {
/** Prisma model to analyze for relations */
prismaModel: PrismaOutputModel;
/** Field names that are included in the input (to determine which relations to include) */
inputFieldNames: string[];
/** Operation type - determines whether to use connectCreate or connectUpdate */
operationType: 'create' | 'update' | 'upsert';
/** Data utils imports provider for accessing relationHelpers fragments */
dataUtilsImports: DataUtilsImportsProvider;
}
/**
* Result of generating relation buildData function
*/
export interface GenerateRelationBuildDataResult {
/** Argument pattern for the function (e.g., "{ ownerId, ...data }" or "data") */
argumentFragment: TsCodeFragment;
/** Return value with relation transformations (e.g., "{ ...data, owner: relationHelpers.connectCreate(...) }") */
returnFragment: TsCodeFragment;
/** Whether this is a simple passthrough (no relations to transform) */
passthrough: boolean;
/** Complete buildData function fragment: ({ fk1, fk2, ...data }) => ({ ...data, relation1: ..., relation2: ... }) */
buildDataFunctionFragment: TsCodeFragment;
}
🤖 Prompt for AI Agents
In
packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
around lines 15 to 38, the interfaces GenerateRelationBuildDataConfig and
GenerateRelationBuildDataResult are declared but not exported; export both
interfaces (add export before each interface declaration) so they are available
to consumers and tests, and update any local references/imports if necessary to
use the exported names.

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

♻️ Duplicate comments (3)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1)

916-919: CRITICAL: Fix hook context property name from new to result.

Both afterExecute (lines 916-919) and afterCommit (lines 924-928) hooks receive a context with new: result instead of result: result. According to the OperationHooks interface (types.ts lines 109-122) and the JSDoc example on line 832 which shows ctx.result.id, hooks expect to access ctx.result.

With the current implementation, ctx.result will be undefined, causing runtime errors in any delete hook that attempts to access the deleted record.

Apply this diff to fix both hook invocations:

         // Run afterExecute hooks
         await invokeHooks(allHooks.afterExecute, {
           ...txContext,
-          new: result,
+          result,
         });

         return result;
       })
       .then(async (result) => {
         // Run afterCommit hooks (outside transaction)
         await invokeHooks(allHooks.afterCommit, {
           ...baseOperationContext,
-          new: result,
+          result,
         });
         return result as GetPayload<TModelName, TQueryArgs>;
       });

Also applies to: 924-928

packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts (1)

63-63: Export InjectedArgRequirements per coding guidelines.

This type is still not exported, which violates the repository convention that requires all types and interfaces in TypeScript files to be exported. A previous review already flagged this issue.

Apply this diff:

-type InjectedArgRequirements = 'context' | 'info' | 'id';
+export type InjectedArgRequirements = 'context' | 'info' | 'id';

As per coding guidelines

examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1)

916-919: Fix delete hook context to use result instead of new.

Lines 918 and 927 pass new: result to the hook invocations, but the OperationContext and TransactionalOperationContext types define a result property, not new. This will cause runtime errors when hooks attempt to access ctx.result as documented.

Apply this diff to fix both hook invocations:

         // Run afterExecute hooks
         await invokeHooks(allHooks.afterExecute, {
           ...txContext,
-          new: result,
+          result,
         });
 
         return result;
       })
       .then(async (result) => {
         // Run afterCommit hooks (outside transaction)
         await invokeHooks(allHooks.afterCommit, {
           ...baseOperationContext,
-          new: result,
+          result,
         });
         return result as GetPayload<TModelName, TQueryArgs>;
       });

Also applies to: 925-928

🧹 Nitpick comments (6)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1)

173-176: Consider more explicit undefined check for update property.

The current check if (value.update) could fail for valid falsy values like 0, false, or empty string. Consider using if (value.update !== undefined) for more explicit checking.

Apply this diff:

-      if (value.update) {
+      if (value.update !== undefined) {
         update[key as keyof TFields] =
           value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
       }
packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts (2)

214-225: Bug fix confirmed: resolver args construction is now correct.

The previous critical issue where an empty string caused invalid function signatures has been resolved by using undefined on line 222. The filter now correctly removes all undefined values, producing valid parameter lists.

Optionally, consider the cleaner step-by-step construction approach suggested in the previous review for improved readability:

const argNames = inputArgs.map((arg) => arg.name);

const resolveFunctionArgsParts = [
  'root',
  `{ input: { ${argNames.join(', ')} } }`,
];

if (argRequirements.has('context') || argRequirements.has('info')) {
  resolveFunctionArgsParts.push('context');
}

if (argRequirements.has('info')) {
  resolveFunctionArgsParts.push('info');
}

const resolveFunctionArgs = resolveFunctionArgsParts.join(', ');

165-167: Optimize by reusing computed injected arg fragments.

handleInjectedArg is called twice for each injected argument: once at lines 165–167 and again at lines 238–242. The fragments computed during the first pass could be cached and reused.

Consider storing both fragment and requirements in the first pass:

-        const injectedArgs = serviceArgs
-          .filter((arg) => arg.type === 'injected')
-          .map((arg) => handleInjectedArg(arg, { returnFieldName }));
+        const injectedArgsMap = new Map(
+          serviceArgs
+            .filter((arg) => arg.type === 'injected')
+            .map((arg) => [arg.name, handleInjectedArg(arg, { returnFieldName })])
+        );
         const argRequirements = new Set(
-          injectedArgs.flatMap((arg) => arg.requirements),
+          Array.from(injectedArgsMap.values()).flatMap((arg) => arg.requirements),
         );

Then at line 238–242:

                 SERVICE_ARGUMENTS: TsCodeUtils.mergeFragmentsAsObject(
                   Object.fromEntries(
                     serviceArgs.map((arg) => [
                       arg.name,
                       arg.type === 'injected'
-                        ? handleInjectedArg(arg, { returnFieldName }).fragment
+                        ? injectedArgsMap.get(arg.name)!.fragment
                         : writeValueFromPothosArg(arg, tsUtilsImports),
                     ]),
                   ),

Also applies to: 236-243

examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (2)

11-16: Consider adding field-level JSDoc for consistency.

While the interface has a description, the id field lacks its own documentation. The FileFieldConfig interface below documents each field individually. Adding a brief comment for the id field would improve consistency and help document the expected format.

Apply this diff:

 /**
  * File input type - accepts a file ID string
  */
 export interface FileInput {
+  /**
+   * The ID of the file to reference
+   */
   id: string;
 }

186-192: Consider adding error handling for race condition scenarios.

The where: { id, referencedAt: null } clause provides optimistic locking—if another process references the file between the check on Line 150 and this hook, Prisma's update() will throw a RecordNotFound error and roll back the transaction. While this correctly maintains data integrity, the resulting error message may not be user-friendly.

Verify whether the framework supports error handling within afterExecute hooks. If so, consider catching the Prisma error and throwing a clearer BadRequestError:

async ({ tx }) => {
  try {
    await tx.file.update({
      where: { id, referencedAt: null },
      data: {
        referencedAt: new Date(),
        size: fileMetadata.size,
      },
    });
  } catch (error) {
    if (error.code === 'P2025') { // Prisma RecordNotFound
      throw new BadRequestError(
        `File "${id}" was already referenced by another process. Please try again.`,
      );
    }
    throw error;
  }
}

This would provide users with a clearer explanation when concurrent operations attempt to reference the same file.

examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1)

8-27: Consolidate type imports into a single statement.

Lines 8-27 import multiple types from ./types.js but use separate import statements. For better readability and consistency, consolidate these into a single import type statement.

Apply this diff:

-import type {
-  AnyFieldDefinition,
-  AnyOperationHooks,
-  DataOperationType,
-  InferFieldOutput,
-  InferFieldsCreateOutput,
-  InferFieldsOutput,
-  InferFieldsUpdateOutput,
-  InferInput,
-  OperationContext,
-  OperationHooks,
-  PrismaTransaction,
-  TransactionalOperationContext,
-} from './types.js';
+import type {
+  AnyFieldDefinition,
+  AnyOperationHooks,
+  DataOperationType,
+  InferFieldOutput,
+  InferFieldsCreateOutput,
+  InferFieldsOutput,
+  InferFieldsUpdateOutput,
+  InferInput,
+  OperationContext,
+  OperationHooks,
+  PrismaTransaction,
+  TransactionalOperationContext,
+} from './types.js';

Based on coding guidelines.

📜 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 e224dec and 0eabfb0.

⛔ Files ignored due to path filters (5)
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/modules/storage/services/file-field.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.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/src/utils/data-operations/define-operations.ts is excluded by !tests/**
📒 Files selected for processing (6)
  • .changeset/eleven-monkeys-marry.md (1 hunks)
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts (8 hunks)
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/eleven-monkeys-marry.md
  • packages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: TypeScript with strict type checking
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 a particular interface or type is not exported, update the TypeScript file so it is exported

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{js,ts,tsx}

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

Node 16 module resolution - include file extensions in imports (.js)

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{ts,tsx,js}

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

**/*.{ts,tsx,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
We use the prefer using nullish coalescing operator (??) ESLint rule instead of a logical or (||), as it is a safer operator
Use console.info/warn/error instead of console.log

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
**/*

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

Use kebab-case for file names

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}: Always use .js extensions in import statements, even when importing from TypeScript files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

Use kebab-case for all file and directory names

Files:

  • examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}

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

examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}: Always use .js file extensions in import statements, even when importing from .ts files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes, documenting purpose, parameters, return values, and all fields

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
🧠 Learnings (12)
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx} : If a particular interface or type is not exported, change the file so it is exported

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-10-14T08:07:58.492Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-14T08:07:58.492Z
Learning: Applies to **/*.{ts,tsx} : If a particular interface or type is not exported, update the TypeScript file so it is exported

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-07-22T09:11:16.930Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-07-22T09:11:16.930Z
Learning: Refactor code to be more easily testable, such as exporting types or functions

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-05-05T06:36:50.687Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 521
File: packages/react-generators/src/generators/admin/admin-crud-edit/admin-crud-edit.generator.ts:90-94
Timestamp: 2025-05-05T06:36:50.687Z
Learning: In this codebase, import paths can include `.ts` extensions, and the `resolveModuleSpecifier` function will handle them appropriately. There's no need to strip file extensions before passing paths to functions like `TsCodeUtils.importFragment`.

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Use `import type` for type-only imports

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:00:29.010Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/blog-with-auth/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:00:29.010Z
Learning: Applies to examples/blog-with-auth/apps/**/src/**/*.{ts,tsx} : Use `import type` for type-only imports

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-07-07T18:24:17.522Z
Learnt from: kingston
Repo: halfdomelabs/baseplate PR: 592
File: plugins/plugin-auth/src/auth0/generators/react/auth0-hooks/templates/src/hooks/use-required-user-id.ts:1-2
Timestamp: 2025-07-07T18:24:17.522Z
Learning: Files under templates/** directories can use `// ts-nocheck` because they are templates meant for code generation, not direct type checking.

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx} : TypeScript with strict type checking

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-07-22T09:10:31.413Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/code-style.mdc:0-0
Timestamp: 2025-07-22T09:10:31.413Z
Learning: Applies to **/*.{ts,tsx} : Include absolute paths in import statements via tsconfig paths (`src/` is the alias for `src/`)

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-07-22T09:11:29.223Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: .cursor/rules/ui-rules.mdc:0-0
Timestamp: 2025-07-22T09:11:29.223Z
Learning: Applies to {packages/project-builder-web/**,packages/ui-components/**}/*.tsx : Use ShadCN-based components from `baseplate-dev/ui-components` and always prefer these components over creating custom ones

Applied to files:

  • packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{tsconfig*.json,apps/{admin,backend}/tsconfig*.json} : Use TypeScript NodeNext module resolution and module ("moduleResolution": "NodeNext", "module": "NodeNext")

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
🧬 Code graph analysis (4)
examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (4)
plugins/plugin-storage/src/generators/fastify/storage-module/templates/module/services/file-field.ts (2)
  • FileInput (14-16)
  • fileField (76-199)
packages/fastify-generators/src/generators/core/error-handler-service/templates/src/utils/http-errors.ts (1)
  • BadRequestError (14-22)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
plugins/plugin-storage/src/generators/fastify/storage-module/templates/module/config/adapters.config.ts (1)
  • STORAGE_ADAPTERS (3-3)
packages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts (13)
packages/fastify-generators/src/types/service-output.ts (1)
  • ServiceOutputDtoInjectedArg (72-79)
packages/core-generators/src/renderers/typescript/fragments/types.ts (1)
  • TsCodeFragment (55-72)
packages/fastify-generators/src/types/service-dto-kinds.ts (3)
  • contextKind (69-69)
  • prismaQueryKind (77-77)
  • prismaWhereUniqueInputKind (84-86)
packages/core-generators/src/renderers/typescript/fragments/creators.ts (1)
  • tsCodeFragment (73-83)
packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts (1)
  • TsCodeUtils (85-584)
packages/utils/src/string/quot.ts (1)
  • quot (6-8)
packages/core-generators/src/renderers/typescript/imports/builder.ts (1)
  • tsImportBuilder (74-82)
packages/fastify-generators/src/generators/prisma/prisma/prisma.generator.ts (1)
  • prismaOutputProvider (73-74)
packages/fastify-generators/src/generators/pothos/pothos-prisma-primary-key/pothos-prisma-primary-key.generator.ts (1)
  • getPothosPrismaPrimaryKeyTypeOutputName (30-34)
packages/react-generators/src/utils/case.ts (1)
  • lowerCaseFirst (3-8)
packages/fastify-generators/src/generators/prisma/_shared/crud-method/primary-key-input.ts (1)
  • getPrimaryKeyDefinition (4-52)
packages/fastify-generators/src/writers/pothos/input-types.ts (1)
  • writePothosInputFieldsFromDtoFields (39-54)
packages/fastify-generators/src/writers/pothos/resolvers.ts (1)
  • writeValueFromPothosArg (105-117)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/types.ts (12)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • InferFieldsOutput (335-342)
  • AnyOperationHooks (125-125)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (5)
examples/blog-with-auth/apps/backend/src/utils/data-operations/types.ts (10)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
⏰ 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). (6)
  • GitHub Check: Verify Sync (blog-with-auth)
  • GitHub Check: Verify Sync (todo-with-auth0)
  • GitHub Check: Lint Examples (todo-with-auth0)
  • GitHub Check: Lint
  • GitHub Check: test
  • GitHub Check: Test E2E
🔇 Additional comments (2)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1)

856-860: The error message is now correct.

The past review comment about this error message being incorrect has been addressed. The error message now correctly references "delete operations".

examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (1)

163-176: Past review issue resolved: Adapter guard is properly implemented.

The missing storage adapter guard flagged in the previous review has been addressed. The current implementation (lines 164-168) checks for adapter existence before accessing it, which is even safer than the originally suggested fix. After this guard, the adapter access on lines 170-171 is guaranteed to be safe.

Comment on lines +21 to +39
interface FileFieldConfig<
TFileCategory extends FileCategory,
TOptional extends boolean = false,
> {
/**
* The category of files this field accepts
*/
category: TFileCategory;
/**
* The field name of the file ID in the existing model
*/
fileIdFieldName: keyof Prisma.$FilePayload['objects'][TFileCategory['referencedByRelation']][number]['scalars'] &
string;

/**
* Whether the file is optional
*/
optional?: TOptional;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Export FileFieldConfig per coding guidelines.

The coding guidelines require exporting all interfaces and types. While consumers can use inline object literals, exporting FileFieldConfig improves API discoverability and allows developers to reference the type explicitly when storing configuration objects.

As per coding guidelines.

Apply this diff:

 /**
  * Configuration for file field handler
  */
-interface FileFieldConfig<
+export interface FileFieldConfig<
   TFileCategory extends FileCategory,
   TOptional extends boolean = false,
 > {
📝 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
interface FileFieldConfig<
TFileCategory extends FileCategory,
TOptional extends boolean = false,
> {
/**
* The category of files this field accepts
*/
category: TFileCategory;
/**
* The field name of the file ID in the existing model
*/
fileIdFieldName: keyof Prisma.$FilePayload['objects'][TFileCategory['referencedByRelation']][number]['scalars'] &
string;
/**
* Whether the file is optional
*/
optional?: TOptional;
}
export interface FileFieldConfig<
TFileCategory extends FileCategory,
TOptional extends boolean = false,
> {
/**
* The category of files this field accepts
*/
category: TFileCategory;
/**
* The field name of the file ID in the existing model
*/
fileIdFieldName: keyof Prisma.$FilePayload['objects'][TFileCategory['referencedByRelation']][number]['scalars'] &
string;
/**
* Whether the file is optional
*/
optional?: TOptional;
}
🤖 Prompt for AI Agents
In
examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts
around lines 21 to 39, the FileFieldConfig interface is not exported; update the
declaration to export the interface (i.e., add the export keyword before
interface FileFieldConfig) so it is publicly available to consumers and follows
the project's coding guidelines for exporting interfaces.

Comment on lines +173 to +176
if (value.update) {
update[key as keyof TFields] =
value.update as InferFieldsUpdateOutput<TFields>[keyof 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 | 🟡 Minor

Use explicit undefined check for consistency and correctness.

Line 173 uses a truthy check if (value.update) while line 169 correctly uses if (value.create !== undefined). This inconsistency could cause bugs if an update value is legitimately falsy (e.g., setting a field to 0, false, "", or null).

Apply this diff for consistency:

-      if (value.update) {
+      if (value.update !== undefined) {
         update[key as keyof TFields] =
           value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
       }
📝 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
if (value.update) {
update[key as keyof TFields] =
value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}
if (value.update !== undefined) {
update[key as keyof TFields] =
value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}
🤖 Prompt for AI Agents
In
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
around lines 173 to 176, the code uses a truthy check `if (value.update)` which
is inconsistent with the earlier `if (value.create !== undefined)` and will
incorrectly skip legitimate falsy update values; change the condition to an
explicit undefined check `if (value.update !== undefined)` and keep the
assignment to `update[key as keyof TFields] = value.update as
InferFieldsUpdateOutput<TFields>[keyof TFields];` so falsy but valid values (0,
false, "", null) are handled correctly.

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

♻️ Duplicate comments (3)
examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts (2)

125-225: Nested one-to-one docs/example still reference old API (deleteRelation, input-based getWhereUnique)

The NestedOneToOneFieldConfig / nestedOneToOneField JSDoc is still out of sync with the current config:

  • Example shows getWhereUnique(input) returning WhereUniqueInput | undefined, but the actual signature is getWhereUnique(parentModel) returning WhereUniqueInput<TModelName> and is non-optional.
  • Example still includes deleteRelation, which does not exist on the config; deletion is now driven by passing null and emitting { delete: true } on the parent’s nested update.
  • The behavior description talks about returning nested create/update payloads from the field, but the implementation now does that work via hooks and uses the field data only for the delete case.

Please update the JSDoc/example to match the current API and semantics (parent-based getWhereUnique, no deleteRelation, delete via null{ delete: true }).


532-619: Guarded getWhereUnique wrapper is still bypassed in processedItems

nestedOneToManyField defines a local getWhereUnique wrapper that throws if config.getWhereUnique returns an object containing undefined values, but processedItems still calls config.getWhereUnique directly and passes that unvalidated result into Prisma upsert:

const whereUnique =
  existingModel && config.getWhereUnique(itemInput, existingModel);
...
results[idx] = item.whereUnique
  ? await prismaDelegate.upsert({ where: item.whereUnique, ... })
  : await prismaDelegate.create({ ... });

To consistently enforce the invariant and avoid passing { id: undefined } (or similar) to Prisma, reuse the local wrapper here:

-          const whereUnique =
-            existingModel && config.getWhereUnique(itemInput, existingModel);
+          const whereUnique =
+            existingModel && getWhereUnique(itemInput, existingModel);
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1)

158-179: Ensure splitCreateUpdateData does not drop falsy update values

The splitCreateUpdateData helper in transformFields still uses a truthy check:

if (value.create !== undefined) {
  create[key as keyof TFields] =
    value.create as InferFieldsCreateOutput<TFields>[keyof TFields];
}
if (value.update) {
  update[key as keyof TFields] =
    value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}

This means updates that intentionally set a field to false, 0, '', or null are silently ignored, since those values are falsy.

Align the condition with the create path:

-      if (value.update) {
+      if (value.update !== undefined) {
         update[key as keyof TFields] =
           value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
       }
📜 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 0eabfb0 and 37708c5.

⛔ Files ignored due to path filters (8)
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !**/generated/**
  • examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.ts is excluded by !**/generated/**, !**/generated/**
  • examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.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/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/**
📒 Files selected for processing (4)
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (1 hunks)
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

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

**/*.{ts,tsx}: TypeScript with strict type checking
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 a particular interface or type is not exported, update the TypeScript file so it is exported

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{js,ts,tsx}

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

Node 16 module resolution - include file extensions in imports (.js)

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*.{ts,tsx,js}

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

**/*.{ts,tsx,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
We use the prefer using nullish coalescing operator (??) ESLint rule instead of a logical or (||), as it is a safer operator
Use console.info/warn/error instead of console.log

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
**/*

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

Use kebab-case for file names

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}

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

examples/blog-with-auth/apps/**/src/**/*.{ts,tsx}: Always use .js file extensions in import statements, even when importing from .ts files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes, documenting purpose, parameters, return values, and all fields

Files:

  • examples/blog-with-auth/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx}: Always use .js extensions in import statements, even when importing from TypeScript files
Use import type for type-only imports
Always specify explicit return types for all functions
Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*

📄 CodeRabbit inference engine (examples/todo-with-auth0/CLAUDE.md)

Use kebab-case for all file and directory names

Files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
🧠 Learnings (1)
📚 Learning: 2025-10-23T21:01:15.331Z
Learnt from: CR
Repo: halfdomelabs/baseplate PR: 0
File: examples/todo-with-auth0/CLAUDE.md:0-0
Timestamp: 2025-10-23T21:01:15.331Z
Learning: Applies to examples/todo-with-auth0/{apps/admin/src,apps/backend/src}/**/*.{ts,tsx} : Add JSDoc to all exported functions, interfaces, and classes documenting purpose, parameters, returns, and fields

Applied to files:

  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
  • examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
  • examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts
🧬 Code graph analysis (3)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts (5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/types.ts (7)
  • FieldDefinition (216-230)
  • AnyFieldDefinition (233-233)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • TransactionalOperationContext (67-73)
  • InferInput (277-288)
  • OperationContext (42-56)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts (6)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • WhereUniqueInput (107-110)
  • CreateInput (132-135)
  • UpdateInput (157-160)
  • WhereInput (80-83)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (2)
  • transformFields (100-206)
  • invokeHooks (52-58)
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts (4)
examples/blog-with-auth/apps/backend/src/utils/data-operations/types.ts (12)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • InferFieldsOutput (335-342)
  • AnyOperationHooks (125-125)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
examples/blog-with-auth/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.ts (5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/types.ts (11)
  • AnyFieldDefinition (233-233)
  • InferFieldOutput (295-301)
  • PrismaTransaction (13-13)
  • InferInput (277-288)
  • DataOperationType (23-23)
  • InferFieldsOutput (335-342)
  • InferFieldsCreateOutput (310-314)
  • InferFieldsUpdateOutput (324-328)
  • OperationContext (42-56)
  • OperationHooks (110-123)
  • TransactionalOperationContext (67-73)
packages/fastify-generators/src/generators/core/service-context/templates/src/utils/service-context.ts (1)
  • ServiceContext (3-5)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-types.ts (4)
  • ModelPropName (12-12)
  • GetPayload (35-38)
  • ModelQuery (59-62)
  • WhereUniqueInput (107-110)
packages/fastify-generators/src/generators/prisma/prisma/templates/src/services/prisma.ts (1)
  • prisma (8-8)
examples/todo-with-auth0/apps/backend/src/utils/data-operations/prisma-utils.ts (1)
  • makeGenericPrismaDelegate (73-78)
⏰ 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: Test E2E
  • GitHub Check: Lint

Comment on lines +158 to +179
function splitCreateUpdateData(data: {
[K in keyof TFields]: InferFieldOutput<TFields[K]>;
}): {
create: InferFieldsCreateOutput<TFields>;
update: InferFieldsUpdateOutput<TFields>;
} {
const create = {} as InferFieldsCreateOutput<TFields>;
const update = {} as InferFieldsUpdateOutput<TFields>;
for (const [key, value] of Object.entries<
InferFieldOutput<TFields[keyof TFields]>
>(data)) {
if (value.create !== undefined) {
create[key as keyof TFields] =
value.create as InferFieldsCreateOutput<TFields>[keyof TFields];
}
if (value.update) {
update[key as keyof TFields] =
value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}
}
return { create, update };
}
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

🧩 Analysis chain

Fix splitCreateUpdateData to handle falsy update values correctly

In transformFields, splitCreateUpdateData currently does:

if (value.create !== undefined) {
  create[key as keyof TFields] =
    value.create as InferFieldsCreateOutput<TFields>[keyof TFields];
}
if (value.update) {
  update[key as keyof TFields] =
    value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}

The truthy check on value.update will skip valid falsy updates (e.g. setting a field to false, 0, '', or null), so those updates will never be written.

Use an explicit undefined check like you already do for create:

-      if (value.update) {
+      if (value.update !== undefined) {
         update[key as keyof TFields] =
           value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
       }

This preserves the ability to set fields to falsy values while still omitting keys whose update is actually undefined.



---

🏁 Script executed:

```shell
#!/bin/bash
# Read the actual file content at the specified lines
cat examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts | sed -n '158,179p'

Length of output: 780


🏁 Script executed:

#!/bin/bash
# Get more context: lines 140-190 to understand the function signature and surrounding code
cat examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts | sed -n '140,190p'

Length of output: 1642


🏁 Script executed:

#!/bin/bash
# Search for type definitions related to InferFieldOutput
rg -n "InferFieldOutput|InferFieldsCreateOutput|InferFieldsUpdateOutput" examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts -A2 -B2

Length of output: 2286


🏁 Script executed:

#!/bin/bash
# Check if there are any other uses of value.update in the file to understand the pattern
rg -n "value\.update" examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts -C3

Length of output: 342



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where these types are defined
fd -e ts -e tsx | xargs rg -l "export.*InferFieldOutput|type InferFieldOutput" | head -5

Length of output: 250


🏁 Script executed:

#!/bin/bash
# Search for the type definitions across the codebase
rg -n "type InferFieldOutput|export.*InferFieldOutput" --type ts --type tsx -A5

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Look for tests or usage patterns for splitCreateUpdateData
rg -n "splitCreateUpdateData" examples/blog-with-auth/ -B3 -A3 --type ts --type tsx

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Read the types file to understand InferFieldOutput
cat examples/blog-with-auth/apps/backend/src/utils/data-operations/types.ts

Length of output: 10785


🏁 Script executed:

#!/bin/bash
# Search for usage patterns without file type filter
rg -n "splitCreateUpdateData" examples/blog-with-auth/ -B3 -A3

Length of output: 1830


🏁 Script executed:

#!/bin/bash
# Look for tests related to field definitions and create/update
rg -n "value\.create|value\.update" examples/blog-with-auth/apps/backend/ -B2 -A2 | head -40

Length of output: 1345


Fix splitCreateUpdateData to handle falsy update values correctly

In transformFields, splitCreateUpdateData currently does:

if (value.create !== undefined) {
  create[key as keyof TFields] =
    value.create as InferFieldsCreateOutput<TFields>[keyof TFields];
}
if (value.update) {
  update[key as keyof TFields] =
    value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
}

The truthy check on value.update will skip valid falsy updates (e.g. setting a field to false, 0, '', or null), so those updates will never be written.

Use an explicit undefined check like you already do for create:

-      if (value.update) {
+      if (value.update !== undefined) {
         update[key as keyof TFields] =
           value.update as InferFieldsUpdateOutput<TFields>[keyof TFields];
       }

This preserves the ability to set fields to falsy values while still omitting keys whose update is actually undefined.

🤖 Prompt for AI Agents
In
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
around lines 158 to 179, splitCreateUpdateData incorrectly checks update with a
truthy test which skips valid falsy updates; change the check to an explicit
undefined comparison (e.g., value.update !== undefined) so falsy values like
false, 0, '', or null are preserved while still omitting keys whose update is
actually undefined, keeping the create check as-is.

Comment on lines +125 to +225
/**
* Configuration for defining a nested one-to-one relationship field.
*
* One-to-one fields represent a single related entity that can be created,
* updated, or deleted along with the parent entity. The field handler manages
* the lifecycle of the nested entity automatically.
*
* @template TParentModelName - Parent model name
* @template TModelName - Child model name
* @template TRelationName - Relation field name on the child model
* @template TFields - Field definitions for the nested entity
*/
export interface NestedOneToOneFieldConfig<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
> {
/**
* Prisma model name of parent model
*/
parentModel: ParentModelConfig<TParentModelName>;

/**
* Prisma model name of the child model
*/
model: TModelName;

/**
* Relation name of the parent model from the child model
*/
relationName: TRelationName;

/**
* Field definitions for the nested entity
*/
fields: TFields;

/**
* Extract where unique from parent model
*/
getWhereUnique: (
parentModel: GetPayload<TParentModelName>,
) => WhereUniqueInput<TModelName>;

/**
* Transform validated field data into final Prisma structure
*/
buildData: (
data: {
create: InferFieldsCreateOutput<TFields> &
Record<TRelationName, { connect: WhereUniqueInput<TParentModelName> }>;
update: InferFieldsUpdateOutput<TFields>;
},
parentModel: GetPayload<TParentModelName>,
ctx: TransactionalOperationContext<
GetPayload<TModelName>,
{ hasResult: false }
>,
) => PrismaFieldData<TModelName> | Promise<PrismaFieldData<TModelName>>;
}

/**
* Create a nested one-to-one relationship field handler
*
* This helper creates a field definition for managing one-to-one nested relationships.
* It handles nested field validation, transformation, and supports both create and update operations.
*
* For create operations:
* - Returns nested create data if input is provided
* - Returns undefined if input is not provided
*
* For update operations:
* - Returns upsert if input has a unique identifier (via getWhereUnique)
* - Returns create if input doesn't have a unique identifier
* - Deletes the relation if input is null (requires deleteRelation)
* - Returns undefined if input is not provided (no change)
*
* @param config - Configuration object
* @returns Field definition
*
* @example
* ```typescript
* const fields = {
* userProfile: nestedOneToOneField({
* fields: {
* bio: scalarField(z.string()),
* avatar: fileField(avatarFileCategory),
* },
* buildData: (data) => ({
* bio: data.bio,
* avatar: data.avatar ? { connect: { id: data.avatar } } : undefined,
* }),
* getWhereUnique: (input) => input.id ? { id: input.id } : undefined,
* deleteRelation: async () => {
* await prisma.userProfile.deleteMany({ where: { userId: parentId } });
* },
* }),
* };
* ```
*/
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

Nested one-to-one JSDoc and example are out of sync with the actual config/API

The JSDoc around NestedOneToOneFieldConfig / nestedOneToOneField still describes an API that no longer matches the types/implementation:

  • The example uses getWhereUnique: (input) => ... and a return type of WhereUniqueInput | undefined, but the actual config expects getWhereUnique(parentModel: GetPayload<TParentModelName>): WhereUniqueInput<TModelName> and is always non-undefined.
  • The example still references a deleteRelation property, which does not exist on NestedOneToOneFieldConfig; deletion is now driven by passing null (emitting { delete: true } on the parent update) and not by a separate callback.
  • The bullet list under “For create/update operations” describes returning nested create/update payloads from the field, but the current implementation pushes all non-null work into hooks and only emits { delete: true } for the null case.

This is likely to confuse consumers trying to implement one-to-one fields.

Consider updating the docs/example to:

  • Show getWhereUnique receiving the parent model and always returning a concrete WhereUniqueInput<TModelName>.
  • Remove the deleteRelation property from the example and show deletion via data.userProfile = null{ userProfile: { delete: true } } on the parent.
  • Clarify that non-null inputs are handled via hooks/upsert on the child model, while the field’s data contribution is empty except for the delete case.



🏁 Script executed:

#!/bin/bash
# View the complete field-definitions.ts file to verify JSDoc/implementation mismatches
cat examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts

Length of output: 23124


🏁 Script executed:

#!/bin/bash
# Search for deleteRelation references in the file
rg -n "deleteRelation" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Search for nestedOneToOneField function implementation
rg -n "export.*nestedOneToOneField" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -A 50

Length of output: 1753


🏁 Script executed:

#!/bin/bash
# Search for nestedOneToManyField and getWhereUnique wrapper pattern
rg -n "nestedOneToManyField|getWhereUnique" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -B 2 -A 2

Length of output: 5099


🏁 Script executed:

#!/bin/bash
# Find all instances of { delete: true } in the file to confirm deletion mechanism
rg -n "delete.*true" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -B 3 -A 3

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Check the complete NestedOneToOneFieldConfig interface definition
rg -n "interface NestedOneToOneFieldConfig" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -A 100 | head -150

Length of output: 3331


🏁 Script executed:

#!/bin/bash
# Look for any remaining deleteRelation in config usage/type definitions
rg -n "deleteRelation\|NestedOneToOneFieldConfig\|processedItems" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -C 4

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the actual nestedOneToOneField implementation logic for null handling and data building
ast-grep --pattern 'export function nestedOneToOneField($_) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Examine export statements and function signatures
rg -n "^export (function|const|interface)" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Check null handling in field data building
rg -n "null|undefined|\{ delete" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -C 3

Length of output: 7950


🏁 Script executed:

#!/bin/bash
# Verify getWhereUnique wrapper pattern in nestedOneToManyField
rg -n "nestedOneToManyField|const.*getWhereUnique|config\.getWhereUnique" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -B 3 -A 3

Length of output: 2825


🏁 Script executed:

#!/bin/bash
# Get the exact line numbers and structure of the complete interface and function
sed -n '125,225p' examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts

Length of output: 3110


🏁 Script executed:

#!/bin/bash
# Check for processedItems and where config.getWhereUnique is called directly
rg -n "processedItems|config\.getWhereUnique" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -B 5 -A 5

Length of output: 3639


🏁 Script executed:

#!/bin/bash
# Find the complete nestedOneToOneField and nestedOneToManyField function bodies
rg -n "export (function|const) nested(OneToOne|OneToMany)Field" examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts -A 200

Length of output: 14372


🏁 Script executed:

#!/bin/bash
# Quick check of file length and structure
wc -l examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts && echo "---" && head -20 examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts

Length of output: 516


🏁 Script executed:

#!/bin/bash
# Final comprehensive dump of entire file for manual inspection
cat examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts | head -500

Length of output: 15722


Update JSDoc and example for nestedOneToOneField; fix validation wrapper bypass in nestedOneToManyField

The JSDoc and example for nestedOneToOneField are out of sync with the actual API and implementation. The interface does not define a deleteRelation property; deletion is triggered by passing null (emitting { delete: true } on parent update). Additionally, getWhereUnique receives the parent model, not input data—the example incorrectly shows (input) => ... when it should be (parentModel) => .... Update lines 187–224 to reflect the actual API.

Additionally, on line 602 of nestedOneToManyField, the code calls config.getWhereUnique(itemInput, existingModel) directly, bypassing the local getWhereUnique wrapper defined on lines 549–560. This wrapper validates that returned values contain no undefined entries (lines 554–557). Using the local wrapper ensures consistent validation across all items.

Comment on lines +532 to +619
export function nestedOneToManyField<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
>(
config: NestedOneToManyFieldConfig<
TParentModelName,
TModelName,
TRelationName,
TFields
>,
): FieldDefinition<
InferInput<TFields>[] | undefined,
undefined,
undefined | { deleteMany: Record<never, never> }
> {
const getWhereUnique = (
input: InferInput<TFields>,
originalModel: GetPayload<TParentModelName>,
): WhereUniqueInput<TModelName> | undefined => {
const whereUnique = config.getWhereUnique(input, originalModel);
if (whereUnique && Object.values(whereUnique).includes(undefined)) {
throw new Error(
'getWhereUnique cannot return any undefined values in the object',
);
}
return whereUnique;
};

return {
processInput: async (value, processCtx) => {
const { serviceContext, loadExisting } = processCtx;

if (value === undefined) {
return { data: { create: undefined, update: undefined } };
}

const existingModel = (await loadExisting()) as
| GetPayload<TParentModelName>
| undefined;

// Filter objects that relate to parent model only
const whereFromOriginalModel = existingModel && {
[config.relationName]: expandWhereUnique(
config.parentModel.getWhereUnique(existingModel),
),
};
// Handle list of items
const delegate = makeGenericPrismaDelegate(prisma, config.model);

const cachedLoadExisting = value.map((itemInput) => {
let cachedExisting: GetPayload<TModelName> | undefined;
const whereUnique =
existingModel && getWhereUnique(itemInput, existingModel);

return async (): Promise<GetPayload<TModelName> | undefined> => {
if (cachedExisting) return cachedExisting;
if (!whereUnique) return undefined;
cachedExisting =
(await delegate.findUnique({
where: { ...whereUnique, ...whereFromOriginalModel },
})) ?? undefined;
return cachedExisting;
};
});

const processedItems = await Promise.all(
value.map(async (itemInput, idx) => {
const whereUnique =
existingModel && config.getWhereUnique(itemInput, existingModel);

const { data, hooks } = await transformFields(
config.fields,
itemInput,
{
serviceContext,
operation: 'upsert',
allowOptionalFields: false,
loadExisting: cachedLoadExisting[idx] as () => Promise<
object | undefined
>,
},
);

return { whereUnique, data, hooks };
}),
);
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

Use the guarded getWhereUnique wrapper consistently in nestedOneToManyField

Inside nestedOneToManyField, you define a local getWhereUnique wrapper (lines 549–560) that throws if config.getWhereUnique returns an object containing any undefined values. That wrapper is used when building cachedLoadExisting, but processedItems still calls config.getWhereUnique directly:

const whereUnique =
  existingModel && config.getWhereUnique(itemInput, existingModel);

This bypasses the invariant check, so a mis-implemented getWhereUnique returning { id: undefined } would:

  • Be rejected in the cachedLoadExisting path, but
  • Still be stored as item.whereUnique and passed to prismaDelegate.upsert({ where: item.whereUnique, ... }), risking runtime Prisma errors.

Use the local wrapper everywhere to enforce the same validation:

-          const whereUnique =
-            existingModel && config.getWhereUnique(itemInput, existingModel);
+          const whereUnique =
+            existingModel && getWhereUnique(itemInput, existingModel);

This keeps behavior consistent and fails fast on bad getWhereUnique implementations.


🏁 Script executed:

#!/bin/bash
# Verify the review comment by examining the actual file content
FILE="examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts"

# Check if file exists and extract the relevant section (lines 532-619)
if [ -f "$FILE" ]; then
  echo "=== File exists, extracting lines 532-619 ==="
  sed -n '532,619p' "$FILE"
else
  echo "File not found at: $FILE"
  # Try to find it
  find . -name "field-definitions.ts" -type f 2>/dev/null | head -5
fi

Length of output: 2905



🏁 Script executed:

#!/bin/bash
# Search for all uses of getWhereUnique in the nestedOneToManyField function
FILE="examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts"

# Extract the full nestedOneToManyField function and find all getWhereUnique references
# First, get line numbers to find the function boundaries
grep -n "export function nestedOneToManyField" "$FILE" | head -1
grep -n "^}" "$FILE" | awk -F: 'NR==1 {if ($1 > 532) print}' | head -1

# Now show all lines containing getWhereUnique with context
echo "=== All references to getWhereUnique ==="
grep -n "getWhereUnique" "$FILE" | head -20

Length of output: 1306


Replace direct config.getWhereUnique call with the local wrapper in processedItems (line 602)

The review comment accurately identifies an inconsistency: the local getWhereUnique wrapper (lines 549–560) enforces validation that undefined values cannot be present in the returned object. This wrapper is correctly used when building cachedLoadExisting (line 586), but bypassed in the processedItems loop (line 602), where config.getWhereUnique is called directly instead.

This inconsistency allows a mis-implemented getWhereUnique to return invalid data (e.g., { id: undefined }) through the processedItems path while the same implementation would be caught and rejected in the cachedLoadExisting path. The fix is straightforward:

          const whereUnique =
-           existingModel && config.getWhereUnique(itemInput, existingModel);
+           existingModel && getWhereUnique(itemInput, existingModel);

This ensures consistent validation enforcement and prevents subtle runtime errors from mal-formed whereUnique objects being passed to Prisma operations.

📝 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 function nestedOneToManyField<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
>(
config: NestedOneToManyFieldConfig<
TParentModelName,
TModelName,
TRelationName,
TFields
>,
): FieldDefinition<
InferInput<TFields>[] | undefined,
undefined,
undefined | { deleteMany: Record<never, never> }
> {
const getWhereUnique = (
input: InferInput<TFields>,
originalModel: GetPayload<TParentModelName>,
): WhereUniqueInput<TModelName> | undefined => {
const whereUnique = config.getWhereUnique(input, originalModel);
if (whereUnique && Object.values(whereUnique).includes(undefined)) {
throw new Error(
'getWhereUnique cannot return any undefined values in the object',
);
}
return whereUnique;
};
return {
processInput: async (value, processCtx) => {
const { serviceContext, loadExisting } = processCtx;
if (value === undefined) {
return { data: { create: undefined, update: undefined } };
}
const existingModel = (await loadExisting()) as
| GetPayload<TParentModelName>
| undefined;
// Filter objects that relate to parent model only
const whereFromOriginalModel = existingModel && {
[config.relationName]: expandWhereUnique(
config.parentModel.getWhereUnique(existingModel),
),
};
// Handle list of items
const delegate = makeGenericPrismaDelegate(prisma, config.model);
const cachedLoadExisting = value.map((itemInput) => {
let cachedExisting: GetPayload<TModelName> | undefined;
const whereUnique =
existingModel && getWhereUnique(itemInput, existingModel);
return async (): Promise<GetPayload<TModelName> | undefined> => {
if (cachedExisting) return cachedExisting;
if (!whereUnique) return undefined;
cachedExisting =
(await delegate.findUnique({
where: { ...whereUnique, ...whereFromOriginalModel },
})) ?? undefined;
return cachedExisting;
};
});
const processedItems = await Promise.all(
value.map(async (itemInput, idx) => {
const whereUnique =
existingModel && config.getWhereUnique(itemInput, existingModel);
const { data, hooks } = await transformFields(
config.fields,
itemInput,
{
serviceContext,
operation: 'upsert',
allowOptionalFields: false,
loadExisting: cachedLoadExisting[idx] as () => Promise<
object | undefined
>,
},
);
return { whereUnique, data, hooks };
}),
);
export function nestedOneToManyField<
TParentModelName extends ModelPropName,
TModelName extends ModelPropName,
TRelationName extends RelationName<TModelName>,
TFields extends Record<string, AnyFieldDefinition>,
>(
config: NestedOneToManyFieldConfig<
TParentModelName,
TModelName,
TRelationName,
TFields
>,
): FieldDefinition<
InferInput<TFields>[] | undefined,
undefined,
undefined | { deleteMany: Record<never, never> }
> {
const getWhereUnique = (
input: InferInput<TFields>,
originalModel: GetPayload<TParentModelName>,
): WhereUniqueInput<TModelName> | undefined => {
const whereUnique = config.getWhereUnique(input, originalModel);
if (whereUnique && Object.values(whereUnique).includes(undefined)) {
throw new Error(
'getWhereUnique cannot return any undefined values in the object',
);
}
return whereUnique;
};
return {
processInput: async (value, processCtx) => {
const { serviceContext, loadExisting } = processCtx;
if (value === undefined) {
return { data: { create: undefined, update: undefined } };
}
const existingModel = (await loadExisting()) as
| GetPayload<TParentModelName>
| undefined;
// Filter objects that relate to parent model only
const whereFromOriginalModel = existingModel && {
[config.relationName]: expandWhereUnique(
config.parentModel.getWhereUnique(existingModel),
),
};
// Handle list of items
const delegate = makeGenericPrismaDelegate(prisma, config.model);
const cachedLoadExisting = value.map((itemInput) => {
let cachedExisting: GetPayload<TModelName> | undefined;
const whereUnique =
existingModel && getWhereUnique(itemInput, existingModel);
return async (): Promise<GetPayload<TModelName> | undefined> => {
if (cachedExisting) return cachedExisting;
if (!whereUnique) return undefined;
cachedExisting =
(await delegate.findUnique({
where: { ...whereUnique, ...whereFromOriginalModel },
})) ?? undefined;
return cachedExisting;
};
});
const processedItems = await Promise.all(
value.map(async (itemInput, idx) => {
const whereUnique =
existingModel && getWhereUnique(itemInput, existingModel);
const { data, hooks } = await transformFields(
config.fields,
itemInput,
{
serviceContext,
operation: 'upsert',
allowOptionalFields: false,
loadExisting: cachedLoadExisting[idx] as () => Promise<
object | undefined
>,
},
);
return { whereUnique, data, hooks };
}),
);
🤖 Prompt for AI Agents
In
examples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.ts
around lines 532 to 619, the processedItems loop calls config.getWhereUnique
directly (line ~602) which bypasses the local getWhereUnique wrapper that
validates no undefined values; replace the direct call with the local wrapper
(call getWhereUnique(itemInput, existingModel) instead of
config.getWhereUnique(...)) so the same undefined-value validation is applied
consistently, ensuring invalid where-unique objects are rejected before being
used in downstream operations.

@kingston kingston merged commit c3c2a00 into main Nov 19, 2025
16 checks passed
@kingston kingston deleted the kingston/eng-786-exploration-data-transformer-system-redesign branch November 19, 2025 16:45
@github-actions github-actions bot mentioned this pull request Nov 19, 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