feat: generate Create/Update DTO classes as mapped-type compositions (makeDtoFiles) - #108
Merged
Conversation
…(makeDtoFiles) The README's "Future Plan" has listed per-model Create/Update DTOs since the beginning, deferred on the grounds that deciding what a create payload looks like belongs to the developer. That reasoning has been overtaken by this generator's own behaviour: useValidation decides which validators are right, useSerialization and /// @exclude decide what leaves a response, validateNestedRelations assumes a nested payload shape, and @default(...) already becomes a Swagger `example`. DTOs were the one place the stated philosophy was still being applied. Applying it properly is what produces this feature rather than blocking it. The rule this repo actually holds to is "reflect what the schema states, never guess from a field's name" -- the #34/#56/#76 Date misdetection being the cautionary tale -- and which fields a client can't supply on create is stated outright in the schema: - a *function-based* @default(...) (autoincrement(), uuid(), cuid(), now(), auto(), dbgenerated(...), sequence()): the database or Prisma produces it. DMMF models these as an { name, args } object, which is exactly what distinguishes them from a literal @default(0) -- and literal defaults are kept, since "there's a fallback" isn't "you may not set it". - @updatedat: Prisma maintains it on every write. - relation fields: another entity, not a value. The relation's foreign-key scalar (authorId) stays, because that is what a REST client actually posts. An @id *without* a default stays too: `id String @id` has to come from the caller, and omitting it just because it's an @id would make the model uncreatable. The classes are compositions, not copies: export class CreatePost extends OmitType(Post, ['id', 'createdAt', 'updatedAt', 'author'] as const) {} export class UpdatePost extends PartialType(CreatePost) {} so a field's type, Swagger metadata and validators are declared in exactly one place. This is deliberately a different shape from prisma-generator-nestjs-dto's fully expanded per-DTO classes, and it's the same composition the README FAQ has always told people to write by hand. Mapped types come from @nestjs/swagger when useSwagger is on and @nestjs/mapped-types otherwise. Verified end to end rather than only as strings: the generated output (models, DTOs, enums, nullable fields, useNonNullableAssertions) compiles under `tsc --strict` against a real @nestjs/swagger install with zero errors, and at runtime OmitType carries the validators through (CreatePost rejects a numeric title, an out-of-range status enum and a non-string authorId) while PartialType makes them optional without dropping them (UpdatePost accepts {} and still rejects {title: 123}). Implementation notes: - ClassComponent gains extendsExpression/generatedClassImports/externalImports; when extendsExpression is set the class renders from DTO_CLASS_TEMPLATE with no body of its own. - The base-class import goes through FileComponent.TEMP_PREFIX like relation imports do, so it resolves to the real snake_case path once every file's location is known. Unlike a relation field it must be a *value* import: the name appears in an `extends` clause, which a type-only import can't serve. - Omitted keys are read off the generated base class's fields, not off the model, so `separateRelationFields` (whose base has already dropped relations) and /// @Skip can't produce an OmitType key that isn't `keyof` the class. - GraphQL helper imports are skipped for DTO files -- with no fields, they would be guaranteed-unused. - Composite types get no DTOs: they're embedded values, not entities with their own endpoints. Off by default, so existing output is byte-identical (fixture snapshots unchanged). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s in a table Four things a reader actually trips over in a 600-line README: - No way to navigate it. Added a one-line contents index plus a short callout for the single most-reported gotcha (#59): dryRun defaults to true, so a first run prints instead of writing. - The install step listed `npm install` and `yarn add` as two lines of one shell block, which reads like you run both. Split into separate blocks, and both now install as a *dev* dependency -- this runs inside `prisma generate` and the classes it writes never import it, so it has no business in production dependencies. - 15 options were only documented as a flat bullet list, so there was nowhere to see "what can I set, and what's the default" at a glance. Added a summary table above the existing details, and a note that Prisma passes every generator config value as a string (`dryRun = "false"`, not `dryRun = false`) -- a real first-run failure mode. - The useValidation note explained the missing relation validator as "this library hands DTO composition to the caller", which stopped being the whole story once validateNestedRelations and makeDtoFiles existed. Reworded to the actual reason (validating a nested payload means assuming a shape the schema doesn't state) and pointed at the option that opts into it. The option table was diffed against PrismaClassGeneratorOptions programmatically: 14/14 match, nothing missing or invented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the last real item on the README's "Future Plan".
Why now — the stated philosophy had already been overtaken
Per-model Create/Update DTOs were deferred on the grounds that deciding what a create payload looks like belongs to the developer. That reasoning no longer matches what this generator does:
useValidationdecides which validators are right for a fielduseSerialization+/// @excludedecide what leaves a responsevalidateNestedRelationsassumes a nested payload shape@default(...)already becomes a SwaggerexampleDTOs were the one place the old statement was still being applied. Applying the repo's actual rule produces this feature rather than blocking it.
That rule is "reflect what the schema states, never guess from a field's name" — the #34/#56/#76 Date misdetection being the cautionary tale. Which fields a client can't supply on create is stated outright in the schema, so nothing here is inferred:
Create?id Int @id @default(autoincrement())createdAt DateTime @default(now())computed String @default(dbgenerated(...))updatedAt DateTime @updatedAtisUpdatedAtauthor Author @relation(...)authorId Stringviews Int @default(0)id String @id(no default)DMMF represents function defaults as an
{ name, args }object, which is exactly what distinguishes them from literal ones — verified against real DMMF, not assumed.What it generates
Compositions, not copies:
A field's type, Swagger metadata and validators stay declared in exactly one place. This is deliberately a different shape from
prisma-generator-nestjs-dto's fully expanded per-DTO classes — and it's the same composition the README FAQ has always told people to write by hand. Mapped types come from@nestjs/swaggerwhenuseSwaggeris on,@nestjs/mapped-typesotherwise.Verified end to end, not just as strings
Generated output (models + DTOs + enums + nullable fields +
useNonNullableAssertions) compiled against a real@nestjs/swaggerinstall:And at runtime, the mapped types really do carry the decorators through:
That exercise is also what turned up the TS1263 bug fixed separately in #106.
Implementation notes
ClassComponentgainsextendsExpression/generatedClassImports/externalImports; withextendsExpressionset it renders from a newDTO_CLASS_TEMPLATEwith no body of its own.FileComponent.TEMP_PREFIXlike relation imports, so it resolves to the real snake_case path once every file's location is known. Unlike a relation field it must be a value import — the name appears in anextendsclause, which a type-only import can't serve.separateRelationFields(whose base has already dropped relations) and/// @skipcan't produce anOmitTypekey that isn'tkeyofthe class. There's a test for exactly that.typeblocks) get no DTOs: embedded values, not entities with endpoints.Compatibility
Off by default. Fixture snapshots are unchanged, so existing output is byte-identical. 228 tests pass (13 new). README options/FAQ/comparison table and the "Future Plan" list are updated.
🤖 Generated with Claude Code