-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Replace imperative CRUD service pattern with declarative, type-safe data operations architecture #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Replace imperative CRUD service pattern with declarative, type-safe data operations architecture #692
Conversation
…r-system-redesign
…r-system-redesign
🦋 Changeset detectedLatest commit: 47feaa1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReplaces 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 Changes
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Files/areas to inspect closely:
Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Disabled knowledge base sources:
⛔ Files ignored due to path filters (4)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ 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)
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. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this 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 userThe mutation accepts
ownerIddirectly from input with only null-checking oncreatedAt. 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
createTodoListresolver (examples/todo-with-auth0/apps/backend/src/modules/todos/schema/todo-list.mutations.ts, lines 36-41) to validatedata.ownerId === context.auth.userIdbefore calling the data-service. Alternatively, deriveownerIdexclusively fromcontext.auth.userIdand 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-authThis
scalarFieldis 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 versionThese definitions match the blog-with-auth copy; see that file’s comments for rationale and usage notes.
137-185: NestedOneToOneFieldConfig docs vs APISame 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-authImplementation 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 duplicationSame configuration surface and semantics as in the blog-with-auth module; see that file’s comment about careful
getWhereUniqueimplementations.
447-465: expandWhereUnique helper duplicationBehavior and considerations are identical to the blog-with-auth version.
532-748: nestedOneToManyField duplicated; consider reusing guarded getWhereUniqueThis is a straight copy of the blog-with-auth implementation, including the local
getWhereUniquewrapper and the direct calls toconfig.getWhereUniqueinprocessedItems. 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: AlignTPrepareResultgenerics with the other example app and simplify to object-onlySame comment as for the todo-with-auth0 version:
TPrepareResultis constrained asRecord<string, unknown> | undefinedin config interfaces but defaults to an object type in the builder generics. Dropping| undefinedand 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: Refinequerytyping to avoid casting{}to a requiredincludeshapeAs 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 whenqueryisundefined.Adopting a
Pick<TQueryArgs, 'include'>-style type for thequeryparameter 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 implementationThis delete operation implementation has the same two issues:
- Error message says “create operations” instead of “delete operations”.
afterExecuteandafterCommithooks receive{ ...ctx, new: result }instead of{ ...ctx, result }, violating theOperationHookscontract and breaking any hooks that usectx.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: 1will significantly slow down the test suite. While this is often necessary for integration tests that share database state, it should be:
- Documented with a comment explaining why parallel execution causes issues
- Verified that this resolves the actual test stability problems
- 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:
- Whether this change resolves specific test failures or race conditions?
- 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/utilsimport 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 standalonez.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 onimportscan be avoided with slightly tighter typingThe implementation is functionally correct, but you can avoid the
as Record<keyof T, string>cast by typingimportsin terms ofkeyof Tup 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: ReuseFragmentMatcherstype frommatchersto avoid duplication and verify import specYou already export a
FragmentMatchersinterface frommatchers.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
.tsextension 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
normalizeImportsdelegates tolocaleComparefor 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,localeComparecan 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.,
ZodErrorbeforeZodSchema) rather than the exact full array includingz.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:
contextKind→context.prismaQueryKind→queryFromInfo({ context, info, path: ['returnFieldName'] })with the right import.prismaWhereUniqueInputKind→ a Prismawhereshape that matches single and composite ID conventions when combined withgetPrimaryKeyDefinition(prismaModel).- The
argRequirementsset andinputArgsconstruction correctly add anidinput 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 buildingSERVICE_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 testsThese tests do a good job exercising create/update paths, composite keys, optional relations, all‑FK vs passthrough, and partial‑FK errors. The implementation of
generateRelationBuildDataalso has a dedicatedoperationType === '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 ofArray.prototype.toSortedis compatible with your runtime
generateBuildDataBodyusesforeignKeyFields.toSorted()andrelationFragments.toSorted(). ThesetoSortedmethods 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
toSortednatively, 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 orderingPlease 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 exportedcreateFileCategoryhelperThe generic extension over
Prisma.FileCountOutputTypelooks 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 exposingcreatedAtas a mutable input fieldAllowing clients to supply
createdAtcan make timeline data untrustworthy and is usually unnecessary when the DB manages this column; consider droppingcreatedAtfromtodoListInputFields(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
createdAtinternally 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 guidelinesThe
userInputFieldsandcreateUser/updateUser/deleteUserwiring intodefine*Operationlook 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/defineDeleteOperationor 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 typesThe
todoItemAttachmentInputFieldsandtagsnestedOneToManyFieldconfig (including the compositetodoItemAttachmentId_tagwhereUnique) 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 => datagetWhereUnique: (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 typesThe use of
relationHelpers.connectCreate/connectUpdatefortodoListanduserplus the sharedtodoListShareInputFieldsis 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, anddeleteTodoListSharedescribing their roles and parameters.- Consider adding explicit return types to the exported operations (e.g. via a shared
TodoListShareOperationtype alias or explicit generics ondefine*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, anddeleteUserare wired correctly intodefineCreateOperation/defineUpdateOperation/defineDeleteOperationand 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*Operationhelpers, rather than relying purely on inference.examples/todo-with-auth0/apps/backend/src/modules/storage/services/file-field.ts (3)
21-39: Consider exportingFileFieldConfigto match TS type export guidelines
FileFieldConfigis used in the signature of exportedfileFieldbut 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 asymmetricThe
TOptionalgeneric influences theFieldDefinitioninput/output types, but the runtime logic inprocessInputalways acceptsFileInput | null | undefinedregardless ofconfig.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 thatoptionalonly affects types, not runtime validation of “field must be present”.
134-147: Uploader scoping viauploaderIdis good, but relies on Prisma handlingundefinedas “omit”The
findUniquecall scopes by{ id, uploaderId }when not a system user, and usesuploaderId: undefinedfor system users (effectively falling back to “by id only”). This is a reasonable pattern but depends on Prisma treatingundefinedproperties as omitted from thewhereclause.If that behavior ever changes, system users might end up with an invalid query. It may be safer to branch the
whereobject 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 exportingGenericPrismaDelegatefor reuse and clarity
GenericPrismaDelegateunderpins the public return type ofmakeGenericPrismaDelegatebut 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 IDsThe new nested input types for attachments/tags look consistent, but note that
idis now defined viat.id()instead of the previous'Uuid'scalar. This changes the GraphQL type from a UUID-specific scalar to the genericIDscalar.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 entitiesThe new nested input types for
customer,images,roles, anduserProfileare coherent and match the expected shapes for the corresponding nested relations. Two small points:
idforimagesanduserProfileis defined ast.id()(GraphQLID) rather than'Uuid'likefileInputInputType. If these IDs are still UUID-backed in Prisma, consider sticking with theUuidscalar for stricter validation.restrictObjectNullsonimages/userProfileonly rejects explicitnullwhile allowing fields to be omitted (undefined), which matches the optional field semantics.If the looser
IDscalar 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 layerThe overall type design (operation contexts, hooks, field transforms, and inference utilities) is coherent, strongly typed, and matches the declarative data-operations goals.
import typeusage 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, andIdentityare 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 typingsAs in the corresponding todo-with-auth0 file,
RequiredKeys,OptionalForUndefinedKeys, andIdentityare 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: UnifyTPrepareResultgenerics to avoidundefinedintersections and simplify typingFor both create and update configs/builders,
TPrepareResultis declared as:
- In
CreateOperationConfig/UpdateOperationConfig:TPrepareResult extends Record<string, unknown> | undefined = undefined;
- In
defineCreateOperation/defineUpdateOperationgenerics:TPrepareResult extends Record<string, unknown> | undefined = Record<string, never>;Mixing
| undefinedwith an object default leads to slightly confusing types (InferFieldsX & TPrepareResultcan conceptually intersect withundefined). At runtime you always spreadpreparedDatainto an object, and you already fall back toPromise.resolve(undefined as TPrepareResult).It would be clearer and safer to treat
TPrepareResultas always-object (possibly empty) and drop| undefinedentirely, 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
defineCreateOperationand 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 ofqueryto requiredincludeobject is slightly misleadingIn 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 whenqueryisundefined, and enforces “include-only” via convention rather than type structure.If you want to keep “no
selectallowed” but allowqueryto be omitted, consider something like:- query: { include: NonNullable<TQueryArgs['include']> }; + query: Pick<TQueryArgs, 'include'>;and let the inner
create/update/deletesimply spread...query(which might be{}or{ include: ... }). The explicitif (query?.select)guard already prevents mis-use.Also applies to: 705-712, 909-915
| --- | ||
| '@baseplate-dev/utils': patch | ||
| --- | ||
|
|
||
| Add case utils to utils package |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -rnLength 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.
| 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>>; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
deleteRelationinto 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.
| 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], | ||
| }, | ||
| }; | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
getWhereUniquewrapper that throws ifconfig.getWhereUniquereturns an object containing anyundefinedvalues. - However, in
processedItemsyou still callconfig.getWhereUniquedirectly:- This
whereUniqueis later used inupsert({ where: item.whereUnique, ... }), so a mis-implementedgetWhereUnique(e.g.{ id }whereidmight beundefined) can still slip through and reach Prisma.
- This
To fully enforce the invariant, consider:
- Using the local
getWhereUniquehelper everywhere inside this function (both incachedLoadExistingand inprocessedItems), so anyundefinedvalues 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.
| 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 }>; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const orderedHeaders = headersContainer | ||
| .getValue() | ||
| .sort((a, b) => a.name.localeCompare(b.name)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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')); |
| 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'; |
There was a problem hiding this comment.
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.
...s/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.ts
Show resolved
Hide resolved
...-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.ts
Show resolved
Hide resolved
| 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: '', | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix invalid Zod record schema for jsonObject type.
The jsonObject mapping on line 40 generates invalid Zod code that will fail at runtime:
unknown()is not a valid Zod schema—it needs thez.prefix- 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).
There was a problem hiding this 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:
Error message copy-paste issue (lines 857-858): References "create operations" instead of "delete operations"
Critical bug in hook context (lines 918, 927): Both
afterExecuteandafterCommithooks receivenew: resultinstead ofresult: result. TheOperationHooksinterface expectsctx.result, and the JSDoc example (line 832) shows hooks accessingctx.result.id. With the current code,ctx.resultisundefined, 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 usescreatesemantics for relation helpersIn the upsert branch, the
updateDataBodyis still built withoperationType: 'create', so the update half of an upsert will incorrectly useconnectCreatesemantics instead ofconnectUpdate: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
connectCreatewhile update usesconnectUpdate.
📜 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.
⛔ Files ignored due to path filters (5)
examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yamltests/simple/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!tests/**,!**/generated/**tests/simple/apps/backend/src/utils/data-operations/define-operations.tsis 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.jsonpackages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-operation-callbacks.tsknip.config.jspackages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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 forsrc/)
If a particular interface or type is not exported, change the file so it is exportedIf 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.tspackages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tsknip.config.jspackages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tsknip.config.jspackages/fastify-generators/src/generators/prisma/_shared/build-data-helpers/generate-relation-build-data.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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
Useimport typefor 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
Useimport typefor 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.jsexamples/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.tsexamples/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.tsexists 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 APIThe create/update/delete callback generators correctly:
- Use
generateRelationBuildDatafor create/update, respecting thepassthroughflag.- Destructure
datawith the relation argument fragment and pass the transformedreturnFragmentinto Prisma calls.- Keep delete as a simple passthrough without unnecessary relation handling.
No functional concerns from this file as long as
generateRelationBuildDatais 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 supportsArray.prototype.toSortedThe codebase targets Node 22 (
"engines": "node": "^22.0.0"), which has native support forArray.prototype.toSortedsince 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.
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
Show resolved
Hide resolved
examples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.ts
Show resolved
Hide resolved
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this 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 fromnewtoresult.Both
afterExecute(lines 916-919) andafterCommit(lines 924-928) hooks receive a context withnew: resultinstead ofresult: result. According to theOperationHooksinterface (types.ts lines 109-122) and the JSDoc example on line 832 which showsctx.result.id, hooks expect to accessctx.result.With the current implementation,
ctx.resultwill beundefined, 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: ExportInjectedArgRequirementsper 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 useresultinstead ofnew.Lines 918 and 927 pass
new: resultto the hook invocations, but theOperationContextandTransactionalOperationContexttypes define aresultproperty, notnew. This will cause runtime errors when hooks attempt to accessctx.resultas 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 like0,false, or empty string. Consider usingif (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
undefinedon line 222. The filter now correctly removes allundefinedvalues, 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.
handleInjectedArgis 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
idfield lacks its own documentation. TheFileFieldConfiginterface below documents each field individually. Adding a brief comment for theidfield 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'supdate()will throw aRecordNotFounderror 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
afterExecutehooks. If so, consider catching the Prisma error and throwing a clearerBadRequestError: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.jsbut use separate import statements. For better readability and consistency, consolidate these into a singleimport typestatement.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.
⛔ Files ignored due to path filters (5)
examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**examples/todo-with-auth0/apps/backend/baseplate/generated/src/modules/storage/services/file-field.tsis excluded by!**/generated/**,!**/generated/**examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!tests/**,!**/generated/**tests/simple/apps/backend/src/utils/data-operations/define-operations.tsis 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 forsrc/)
If a particular interface or type is not exported, change the file so it is exportedIf 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.tspackages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tspackages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tspackages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tspackages/fastify-generators/src/generators/pothos/pothos-prisma-crud-mutation/pothos-prisma-crud-mutation.generator.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/define-operations.tsexamples/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
Useimport typefor 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.tsexamples/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.tsexamples/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
Useimport typefor 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.tsexamples/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.tsexamples/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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| if (value.update) { | ||
| update[key as keyof TFields] = | ||
| value.update as InferFieldsUpdateOutput<TFields>[keyof TFields]; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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/nestedOneToOneFieldJSDoc is still out of sync with the current config:
- Example shows
getWhereUnique(input)returningWhereUniqueInput | undefined, but the actual signature isgetWhereUnique(parentModel)returningWhereUniqueInput<TModelName>and is non-optional.- Example still includes
deleteRelation, which does not exist on the config; deletion is now driven by passingnulland 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, nodeleteRelation, delete vianull→{ delete: true }).
532-619: GuardedgetWhereUniquewrapper is still bypassed inprocessedItems
nestedOneToManyFielddefines a localgetWhereUniquewrapper that throws ifconfig.getWhereUniquereturns an object containingundefinedvalues, butprocessedItemsstill callsconfig.getWhereUniquedirectly and passes that unvalidated result into Prismaupsert: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: EnsuresplitCreateUpdateDatadoes not drop falsy update valuesThe
splitCreateUpdateDatahelper intransformFieldsstill 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,'', ornullare silently ignored, since those values are falsy.Align the condition with the
createpath:- 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.
⛔ Files ignored due to path filters (8)
examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**examples/blog-with-auth/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.tsis excluded by!**/generated/**,!**/generated/**examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!**/generated/**examples/todo-with-auth0/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.tsis excluded by!**/generated/**,!**/generated/**tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/define-operations.tsis excluded by!**/generated/**,!tests/**,!**/generated/**tests/simple/apps/backend/baseplate/generated/src/utils/data-operations/field-definitions.tsis excluded by!**/generated/**,!tests/**,!**/generated/**tests/simple/apps/backend/src/utils/data-operations/define-operations.tsis excluded by!tests/**tests/simple/apps/backend/src/utils/data-operations/field-definitions.tsis 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 forsrc/)
If a particular interface or type is not exported, change the file so it is exportedIf 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.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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.tsexamples/todo-with-auth0/apps/backend/src/utils/data-operations/field-definitions.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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
Useimport typefor 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.tsexamples/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
Useimport typefor 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.tsexamples/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.tsexamples/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.tsexamples/blog-with-auth/apps/backend/src/utils/data-operations/define-operations.tsexamples/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
| 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 }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -B2Length 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 -C3Length 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 -A5Length 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 tsxLength 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 -A3Length 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 -40Length 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.
| /** | ||
| * 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 } }); | ||
| * }, | ||
| * }), | ||
| * }; | ||
| * ``` | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 ofWhereUniqueInput | undefined, but the actual config expectsgetWhereUnique(parentModel: GetPayload<TParentModelName>): WhereUniqueInput<TModelName>and is always non-undefined. - The example still references a
deleteRelationproperty, which does not exist onNestedOneToOneFieldConfig; deletion is now driven by passingnull(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 thenullcase.
This is likely to confuse consumers trying to implement one-to-one fields.
Consider updating the docs/example to:
- Show
getWhereUniquereceiving the parent model and always returning a concreteWhereUniqueInput<TModelName>. - Remove the
deleteRelationproperty from the example and show deletion viadata.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.tsLength 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.tsLength 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 50Length 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 2Length 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 3Length 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 -150Length 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 4Length 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.tsLength 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 3Length 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 3Length 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.tsLength 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 5Length 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 200Length 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.tsLength 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 -500Length 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.
| 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 }; | ||
| }), | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
cachedLoadExistingpath, but - Still be stored as
item.whereUniqueand passed toprismaDelegate.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
fiLength 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 -20Length 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.
| 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.
Summary by CodeRabbit
New Features
Bug Fixes
Tests