Skip to content

Commit

Permalink
feat: add integer and format support for IsNumber
Browse files Browse the repository at this point in the history
  • Loading branch information
glebbash committed Feb 15, 2023
1 parent ec77cdf commit bb6c86f
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 4 deletions.
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@

This library combines common `@nestjs/swagger`, `class-transformer` and `class-validator` decorators that are used together into one decorator for full Nest.js DTO lifecycle including OpenAPI schema descriptions.

DTO without `nestjs-swagger-dto`:

```ts
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsOptional, IsString, MaxLength, MinLength, ValidateNested } from 'class-validator';

export class RoleDto {
@IsOptional()
@IsString()
@MinLength(3)
@MaxLength(256)
name?: string;

@IsOptional()
@IsString()
@MaxLength(256)
description?: string;

@ApiProperty({ enum: RoleStatus, enumName: 'RoleStatus' })
status!: RoleStatus;

@ValidateNested({ each: true })
@Type(() => PermissionDto)
@ApiProperty({ type: [PermissionDto] })
permissions!: PermissionDto[];
}

```

DTO with `nestjs-swagger-dto`:

```ts
import { IsEnum, IsNested, IsString } from 'nestjs-swagger-dto';

class RoleDto {
@IsString({
optional: true,
minLength: 3,
maxLength: 256,
})
name?: string;

@IsString({ optional: true, maxLength: 255 })
description?: string;

@IsEnum({ enum: { RoleStatus } })
status!: RoleStatus;

@IsNested({ type: PermissionDto, isArray: true })
permissions!: PermissionDto[];
}
```

## Installation

```sh
Expand Down
53 changes: 52 additions & 1 deletion src/decorators/is-number.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Result } from 'true-myth';

import { input, make } from '../../tests/helpers';
import { generateSchemas, input, make, output } from '../../tests/helpers';
import { IsNumber } from '../nestjs-swagger-dto';

describe('IsNumber', () => {
Expand Down Expand Up @@ -134,6 +134,57 @@ describe('IsNumber', () => {
});
});

describe('integer', () => {
class Test {
@IsNumber({ type: 'integer', format: 'int32' })
integerField!: number;
}

it('generates correct schema', async () => {
expect(await generateSchemas([Test])).toStrictEqual({
Test: {
type: 'object',
properties: {
integerField: {
type: 'integer',
format: 'int32',
},
},
required: ['integerField'],
},
});
});

it('transforms to plain without converting floats to ints', async () => {
const dto = make(Test, { integerField: 10.5 });
expect(output(dto)).toStrictEqual({ integerField: 10.5 });
});

it('accepts integers', async () => {
expect(await input(Test, { integerField: 10 })).toStrictEqual(
Result.ok(make(Test, { integerField: 10 }))
);
});

it('rejects everything else', async () => {
const testValues: unknown[] = [
{ integerField: 10.5 },
{ integerField: 'true' },
{ integerField: 'false' },
{ integerField: [] },
{ integerField: {} },
{ integerField: null },
{},
];

for (const testValue of testValues) {
expect(await input(Test, testValue)).toStrictEqual(
Result.err('integerField must be a number conforming to the specified constraints')
);
}
});
});

describe('default and stringified', () => {
class Test {
@IsNumber({ optional: true, stringified: true, default: 25 })
Expand Down
28 changes: 25 additions & 3 deletions src/decorators/is-number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,41 @@ import { IsNumber as IsNumberCV, isNumberString, Max, Min } from 'class-validato

import { compose, noop, PropertyOptions } from '../core';

/**
* *NOTE*: If type is not set it defaults to number
*
* *NOTE*: Format is only used for OpenAPI spec
*
* **WARNING**: Setting `type: 'integer'` will not convert floats to integers during `classToPlain`
*/
export const IsNumber = ({
min,
max,
stringified,
type,
format,
...base
}: PropertyOptions<
number,
{ min?: number; max?: number; stringified?: true }
{
min?: number;
max?: number;
stringified?: true;
} & (
| {
type?: undefined;
format?: 'float' | 'double';
}
| {
type: 'integer';
format?: 'int32' | 'int64';
}
)
> = {}): PropertyDecorator =>
compose(
{ type: 'number', minimum: min, maximum: max },
{ type: type ?? 'number', format, minimum: min, maximum: max },
base,
IsNumberCV(undefined, { each: !!base.isArray }),
IsNumberCV({ ...(type === 'integer' && { maxDecimalPlaces: 0 }) }, { each: !!base.isArray }),
stringified ? Transform(({ value }) => (isNumberString(value) ? Number(value) : value)) : noop,
min !== undefined ? Min(min, { each: !!base.isArray }) : noop,
max !== undefined ? Max(max, { each: !!base.isArray }) : noop
Expand Down

0 comments on commit bb6c86f

Please sign in to comment.