Skip to content
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

DtoCastType is being ignored #54

Closed
stefangeyer opened this issue Oct 3, 2024 · 18 comments
Closed

DtoCastType is being ignored #54

stefangeyer opened this issue Oct 3, 2024 · 18 comments

Comments

@stefangeyer
Copy link

Hello! Could it be that @DtoCastType is currently broken? During my tests, the following schema snippet will result in the cast being ignored. It seems like the cast import is generated correctly, but the type annotation is not updated.

Due to internal reasons, I need to express my m2m explicitly - they are transformed to the implicit counterpart for the API response, however. Therefore, I need a way to adjust the OpenAPI response model for the generated docs, which is why I would like to cast the m2m relation fields. I am thankful for any help on this matter!

generator model {
  provider            = "prisma-generator-nestjs-dto"
  output              = "../src/generated/model"
  fileNamingStyle     = "kebab"
  classValidation     = "true"
  prettier            = "true"
}

model Team {
  id            Int                @id @default(autoincrement())
  createdAt     DateTime           @default(now())
  updatedAt     DateTime           @updatedAt
  name          String
  about         String?
  thumbnailUrl  String?
  photoUrl      String?
  externalId    String?
  externalHost  ExternalHostType?
  /// @DtoApiHidden
  registrations TeamRegistration[]
  /// @DtoCastType(Player, ./player.entity)
  members       PlayerOnTeam[]

  @@unique([externalId, externalHost])
}
import { ExternalHostType } from "@prisma/client";
import { Player } from "./player.entity";
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { TeamRegistration } from "./team-registration.entity";
import { PlayerOnTeam } from "./player-on-team.entity";

export class Team {
  @ApiProperty({
    type: "integer",
    format: "int32",
  })
  id: number;
  @ApiProperty({
    type: "string",
    format: "date-time",
  })
  createdAt: Date;
  @ApiProperty({
    type: "string",
    format: "date-time",
  })
  updatedAt: Date;
  @ApiProperty({
    type: "string",
  })
  name: string;
  @ApiProperty({
    type: "string",
    nullable: true,
  })
  about: string | null;
  @ApiProperty({
    type: "string",
    nullable: true,
  })
  thumbnailUrl: string | null;
  @ApiProperty({
    type: "string",
    nullable: true,
  })
  photoUrl: string | null;
  @ApiProperty({
    type: "string",
    nullable: true,
  })
  externalId: string | null;
  @ApiProperty({
    enum: ExternalHostType,
    nullable: true,
  })
  externalHost: ExternalHostType | null;
  @ApiHideProperty()
  registrations?: TeamRegistration[];
  @ApiProperty({
    type: () => PlayerOnTeam,
    isArray: true,
    required: false,
  })
  members?: PlayerOnTeam[];
}
@Brakebein
Copy link
Owner

Currently, the DtoCastType annotation only works on scalar fields and not on relation fields. And ApiProperty type is not affected, but will be resolved from the original field type. So this feature is quite basic at the moment.

Do you need it to be cast only for the class property type, only for the ApiProperty type, or for both?

@stefangeyer
Copy link
Author

stefangeyer commented Oct 6, 2024

Assuming a service flattens the data and returns an instance satisfying the entity type, casting the property type and the ApiProperty type is the way to go in this case (I mainly need all entities to be present in the DMMF which is not the case with relation tables of many-to-many relationships in prisma by default). Only casting the ApiProperty type would mean that a service returns an instance of the explicit many-to-many relation, while the API returns the implicit version. Further, this would mean that the conversion needs to happen within a controller which is most definitely the wrong place for it 🤔.

All other property types are consistent with the type of their ApiProperty type, correct? It would be unexpected behavior for the types to differ if a cast is used.

@Brakebein
Copy link
Owner

All other property types are consistent with the type of their ApiProperty type, correct? It would be unexpected behavior for the types to differ if a cast is used.

I think, the reason that the DtoCastType annotation only applies to the property type is that it was designed to cast the type to something from an external package. Whereas for ApiProperty type, it would have made no sense, since the imported type or class doesn't have any swagger decorators or anything alike, or would even break it. In the original PR (#12) we were only talking about Json fields that would be properly typed after casting.

So, I'm not sure, if changing the behavior such that also ApiProperty type is casted, if it would break anything. Or if it needs some special annotation that has this different behavior (e.g. DtoCastTypeFull or something with a better name).

@stefangeyer
Copy link
Author

I think, the reason that the DtoCastType annotation only applies to the property type is that it was designed to cast the type to something from an external package.

Is there a reason why this only applies to scalars? I don't see many ways this could lead to unexpected errors as the annotation needs to be provided explicitly for the cast to happen. If the user wants to override the type of a relation to something else explicitly, why not let them?

So, I'm not sure, if changing the behavior such that also ApiProperty type is casted, if it would break anything. Or if it needs some special annotation that has this different behavior (e.g. DtoCastTypeFull or something with a better name).

I think it would be a good idea to split up the overrides for the property type and the ApiProperty type into two separate annotations. This would provide maximum flexibility depending on the use case. Could be named DtoOverrideType and DtoApiOverrideType.

@Brakebein
Copy link
Owner

Is there a reason why this only applies to scalars? I don't see many ways this could lead to unexpected errors as the annotation needs to be provided explicitly for the cast to happen. If the user wants to override the type of a relation to something else explicitly, why not let them?

Yes, overriding the property type shouldn't break anything. As I said, the only reason, for which the current behavior is as it is now, is that the original PR was only focusing on Json fields. So overriding the relation fields should be no problem in general. My only concern above was the (automatic) override of the ApiProperty types.

I think it would be a good idea to split up the overrides for the property type and the ApiProperty type into two separate annotations. This would provide maximum flexibility depending on the use case. Could be named DtoOverrideType and DtoApiOverrideType.

These are better (or more precise) namings of the annotations. Also better than DtoCastType, in my opinion. That's something I can implement. Hence, DtoOverrideType would replace DtoCaseType (which would be deprecated), and DtoApiOverrideType (same syntax) would be added.

@stefangeyer
Copy link
Author

Would be highly appreciated as I currently don't have the resources to provide a PR. Thank you very much.

@Brakebein
Copy link
Owner

You can now please test 1.24.0-beta2.

It should now work for all fields including relation fields with both new annotations as stated above.

However, there is some not very clear behavior when it comes to relation fields with annotions like @DtoRelationCanCreateOnCreate or @DtoRelationCanConnectOnUpdate, etc. Then, the CreateDtos and UpdateDtos are getting a bit more complex. Now, @DtoOverrideType will also replace the linked Create/UpdateDtos. This also applies similarly to complex types (mongodb). So, DtoOverrideType and DtoApiOverrideType should only be used with great care.

@stefangeyer
Copy link
Author

Thanks a bunch! I will hopefully get around to testing within the next week.

@Brakebein
Copy link
Owner

Did you find some time to test it?

Brakebein added a commit that referenced this issue Oct 26, 2024
add DtoApiOverrideType annotation
refactor makeImports
@stefangeyer
Copy link
Author

stefangeyer commented Oct 28, 2024

Hello! Sorry for the delay - I am finally getting to test this. Here are my first findings:

The following model is used for the provided test cases:

model Team {
  id            Int                @id @default(autoincrement())
  createdAt     DateTime           @default(now())
  updatedAt     DateTime           @updatedAt
  name          String
  about         String?
  thumbnailUrl  String?
  photoUrl      String?
  externalId    String?
  externalHost  ExternalHostType?
  /// @DtoApiHidden
  registrations TeamRegistration[]
  members       PlayerOnTeam[]

  @@unique([externalId, externalHost])
}

Test case 1 - using @DtoOverrideType isolated:

/// @DtoOverrideType(Player, ./player.entity)
members       PlayerOnTeam[]

Produces correct result.

@ApiProperty({
  type: () => PlayerOnTeam,
  isArray: true,
  required: false,
})
members?: Player[];

Test case 2 - using @DtoApiOverrideType isolated:

/// @DtoApiOverrideType(Player, ./player.entity)
members       PlayerOnTeam[]

Produces correct result.

@ApiProperty({
  type: Player,
  isArray: true,
  required: false,
})
members?: PlayerOnTeam[];

Test case 3 - using @DtoOverrideType and @DtoOverrideApiType in combination:

/// @DtoOverrideType(Player, ./player.entity)
/// @DtoApiOverrideType(Player, ./player.entity)
members       PlayerOnTeam[]

Produces correct result.

@ApiProperty({
  type: Player,
  isArray: true,
  required: false,
})
members?: Player[];

Summary:

@DtoOverrideType - seems to work as expected
@DtoApiOverrideType - seems to work as expected

@Brakebein
Copy link
Owner

Brakebein commented Oct 29, 2024

Thanks! However, it's not @DtoOverrideApiType, but @DtoApiOverrideType as you have proposed in one of your comments above. But maybe @DtoOverrideApiType is more intuitive?

@stefangeyer
Copy link
Author

Ah, my bad... One shouldn't do stuff like this in a rush 🙃 - I will update my comment. Regarding the name: I would keep it consistent with whatever other annotations you have concerning @ApiProperty

Maybe @DtoOverrideApiPropertyType would make the most sense even...

@stefangeyer
Copy link
Author

stefangeyer commented Oct 29, 2024

Also on an unrelated note:

  /// @DtoOverrideType(Player, ./player.entity)
  /// @DtoApiOverrideType(Player, ./player.entity)
  members       PlayerOnTeam[]

will generate

  @ApiProperty({
    type: Player,
    isArray: true,
    required: false,
  })
  members?: Player[];

which will then again produce

  "members": {
    "type": "array",
    "items": {
      "required": false,
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Player"
      }
    }
  }

in comparison to if I add no annotations at all

  @ApiProperty({
    type: () => PlayerOnTeam,
    isArray: true,
    required: false,
  })
  members?: PlayerOnTeam[];

which produces

  "members": {
    "type": "array",
    "items": {
      "$ref": "#/components/schemas/PlayerOnTeam"
    }
  }

It would seem like overriding the type from a function to providing the class explicitly will mess up the typing, exposing the data type as a two-dimensional array, which is incorrect.

Manually changing the @ApiProperty type to

  @ApiProperty({
    type: () => Player,
    isArray: true,
    required: false,
  })
  members?: Player[];

produces the correct result:

  "members": {
    "type": "array",
    "items": {
      "$ref": "#/components/schemas/Player"
    }
  }

Brakebein added a commit that referenced this issue Oct 30, 2024
@Brakebein
Copy link
Owner

I changed the annotation to @DtoOverrideApiPropertyType. It's more explicit and better distinguishable from the other one. The type property is now provided as funktion. 1.24.0-beta5

@stefangeyer
Copy link
Author

stefangeyer commented Oct 30, 2024

Awesome - the overrides seem to work now!

I have one more question regarding the use of the @DtoRelationRequired annotation:

  /// @DtoRelationRequired
  /// @DtoOverrideApiPropertyType(Player, ./player.entity)
  members       PlayerOnTeam[]

generates

  @ApiProperty({
    type: () => Player,
    isArray: true,
    required: false,
  })
  members?: PlayerOnTeam[];

Using @DtoRelationRequired does not seem to force the required field. Am I missing something?

@Brakebein
Copy link
Owner

I actually haven't touched the @DtoRelationRequired annotation and its behavior since I've forked the repo. Nor did I use it in my projects. The readme states that it should be required as you would assume by the name. However, the example also shows optional properties. I will have a look into it.

@KoenLemmen
Copy link

Did you perchance have the time to look at @DtoRelationRequired. I noticed that for my interface exports for the client side relations are also given the TypeScript ?, which I did not expect.

Brakebein added a commit that referenced this issue Nov 30, 2024
@Brakebein
Copy link
Owner

Sorry for the late respone. November was quite busy.

So yes, for me it looked like a bug. @DtoRelationRequired was only hiding null, but the property was still optional.

Please try 1.24.0-beta6

ablyeom added a commit to ablyeom/prisma-generator-nestjs-dto that referenced this issue Jan 21, 2025
* brakebein/main:
  version 1.24.1
  fix Decimal type definition Brakebein#63
  version 1.24.0
  update readme
  deprecate outputApiPropertyType
  fix @DtoRelationRequired Brakebein#54
  change DtoApiOverrideType to DtoOverrideApiPropertyType Brakebein#54
  fix encapsulateString for null Brakebein#58
  fix encapsulateString for date strings Brakebein#58
  add @DtoCreateRequired decorator Brakebein#55
  replace DtoCastType with DtoOverrrideType Brakebein#54
  fix don't generate enums.ts if there are no enums defined
  add flag to show fields with default attribute by default Brakebein#51
  add enumName to ApiProperty Brakebein#52
  generate enums if noDependencies = true Brakebein#48
  wrap relations as type to fix circular dependencies Brakebein#49
  shorten duplicate code checks on double imports
  sort all imports
  shorten duplicate code to make class-validator imports
  fix ApiProperty default value if field is a list
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants