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

Decimal inconsistency #63

Open
rjnkr opened this issue Dec 25, 2024 · 6 comments
Open

Decimal inconsistency #63

rjnkr opened this issue Dec 25, 2024 · 6 comments

Comments

@rjnkr
Copy link

rjnkr commented Dec 25, 2024

Prisma model

model OperBrandstof {
  @@map("oper_brandstof")

  /// @description Prijs per liter van de brandstof
  PRIJS                 Decimal?  @db.Decimal(6, 2)
}

The Create DTO looks like

  @ApiProperty({
    description: "Het bedrag van de tankbeurt om te kunnen factureren",
    type: "number",  <-- number
    format: "double",
    required: false,
    nullable: true,
  })
  @IsOptional()
  @IsDecimal()
  PRIJS?: Prisma.Decimal | null;

The swagger example

{
  "PRIJS": 0,     <--  number
}

So far so good. It means that I have to provide numeric value for these fields. However when calling the api I need to provide these decimals as string. such as "PRIJS": "2.01" to make it work. If is specify "PRIJS": 2.01, the error message "PRIJS is not a valid decimal number."

Also for the output the decimal is provided as string. So the interface implementation and the definition are inconsistent. I prefer to work with numbers instead of string. How can I solve this ?

@Brakebein
Copy link
Owner

Actually, I haven't yet used Decimal types in my own projects. With regards to the docs and this comment, Prisma uses Decimal.js library under the hood and returns a Decimal.js object when querying the database, which will be serialized to a string value (otherwise, it will loose its precision). So, the generated DTO is wrong concerning the ApiProperty type, which should be type: 'string'. I will need to fix that.

To solve your issue, you may need to ask yourself if you really need Decimal, or a simple Float type is just sufficient enough?

Brakebein added a commit that referenced this issue Jan 18, 2025
@Brakebein
Copy link
Owner

The type definition for Decimal fields is now fixed (type: 'string') with the latest release. As I said above, if you only want to work or to pass simple numbers, you might consider working with Prisma Float type instead.

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
@ctjhoa
Copy link

ctjhoa commented Feb 9, 2025

@Brakebein IMHO this is a valid use case.
Decimal.js is needed only as an internal tool to fill the gap left by JS on decimal precision when doing addition, substraction....
Both Number and Float have the same decimal precision issue.
I work on a trading app and I need to do calculus on decimals with correct precision level. However I don't think it makes sense to expose this internal tool (decimal.js) in my API. Number can represent decimals fine as long as there is no compute with other decimal numbers.
I can probably help on implementing such feature if you are interested.

@Brakebein
Copy link
Owner

Well, for the CreateDto and UpdateDto, the type could actually be string or number, since it can be both parsed by Decimal.js. There is, however, no standard class-validator that can be imported (e.g. IsNumberOrString).

For the response, when just returning the results from the query, the Decimal value gets serialized as string. If it should be returned as number, the result would need to be mapped. For example:

const restult = await this.prisma.product.findUnique(...);

return {
  ...result,
  price: result.price.toNumber(),
};

The type in the generated files could be overriden using @DtoOverrideType or @DtoOverrideApiPropertyType. It's probably better to use a flag here (to be implemented).

@ctjhoa How do you imagine the "feature" you are talking of?

And regarding "exposing the internal tool": Decimal.js is of course not exposed to the outside (the consumers of your API), since only serialized values are transmitted. However, it's also not only used by Prisma as an internal tool, since a Decimal.js instance is returned when querying the database. Here, it needs to be handled appropriately to your needs (as stated above). Correct me, if I'm missing something.

@ctjhoa
Copy link

ctjhoa commented Feb 10, 2025

Thanks for the reply @Brakebein
To clarify my point of view, I was referring to this question from the original message:

Also for the output the decimal is provided as string. So the interface implementation and the definition are inconsistent. I prefer to work with numbers instead of string. How can I solve this ?

I was thinking only of request DTOs (like CreateDto and UpdateDto), as in the original message.

I was reacting to the fact that the type definition for Decimal fields is forced to be the type 'string' (which I consider to be a Decimal.js design choice). I also wasn't aware of @DtoOverrideType or @DtoOverrideApiPropertyType, which can be a workaround. Thanks for pointing that out.

How do you imagine the "feature" you are talking of?

I was thinking more of a global parameter to control how decimals should be typed in the generated DTOs. For example:

generator nestjsDto {
  provider             = "prisma-generator-nestjs-dto"
  output                = "../src/generated/nestjs-dto"
  decimalsType    = "number" | "string"
}

Using @DtoOverrideType and @DtoOverrideApiPropertyType is error-prone on large databases with many models.

@Brakebein
Copy link
Owner

Additionally, @DtoOverrideType and @DtoOverrideApiPropertyType is not changing the class-validator. They are usually used for Json fields. So, having an additional config/global parameter would in general be better.

What should the output look like? Currently, the following is generated:

export class ProductDto {
  @ApiProperty({
    type: 'string',
    format: 'Decimal.js',
  })
  price!: Prisma.Decimal;
}

export class CreateProductDto {
  @ApiProperty({
    type: 'string',
    format: 'Decimal.js',
  })
  @IsNotEmpty()
  @IsDecimal()
  price!: Prisma.Decimal;
}

I agree that format: 'Decimal.js' is maybe inappropriate for the API docs.

So for decimalType = "number" it should look like:

export class ProductDto {
  @ApiProperty({
    type: 'number',
    format: 'float',
  })
  price!: number;
}

export class CreateProductDto {
  @ApiProperty({
    type: 'number',
    format: float',
  })
  @IsNotEmpty()
  @IsNumber()
  price!: number;
}

For decimalType = "string"

export class ProductDto {
  @ApiProperty({
    type: 'string',
    format: 'float',
  })
  price!: string;
}

export class CreateProductDto {
  @ApiProperty({
    type: 'string',
    format: 'float',
  })
  @IsNotEmpty()
  @IsDecimal()
  price!: string;
}

In both cases, it would need something like below, because the Prisma query returns a Decimal.js instance:

getProduct(): Promise<ProductDto> {
  const restult = await this.prisma.product.findUnique(...);

  return {
    ...result,
    price: result.price.toNumber(), // or .toString()
  };
}

Maybe, we add decimalType = "default", which would generate the current output with Prisma.Decimal. But for CreateDto, it should then be price!: Prisma.Decimal.Value;, which is string | number | Decimal. However, the class-validator decorator would be quite complex, if we add it from our side.

So, what do you think?

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