From 23206b026afe49c3b8cb0cfd1f946b3829934397 Mon Sep 17 00:00:00 2001 From: muqiuhan Date: Wed, 13 Aug 2025 10:39:46 +0000 Subject: deploy: fe21ae23c1c3d062e430cf8c6227decb63dd2377 --- search.xml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'search.xml') diff --git a/search.xml b/search.xml index 4adf0221..10107697 100644 --- a/search.xml +++ b/search.xml @@ -2735,6 +2735,61 @@ await prisma.$transaction(async tx => {
  • “Retrying failing jobs” · BullMQ Guide
    https://docs.bullmq.io/guide/retrying-failing-jobs
  • BullMQ Guide & Patterns · Process Step Jobs (completed event only fires after process resolves)
    https://docs.bullmq.io/patterns/process-step-jobs
  • +]]> + + Technique + + + + NestJS 中 enableImplicitConversion 与 @Transform 的冲突 + /2025/08/13/nestjs-enableImplicitConversion-and-transform/ + 在 NestJS 生态中,class-validator 和 class-transformer 这两个库提供了以声明式的方式对 DTO 进行验证和转换。然而在处理布尔值时,如果在全局验证管道或仅仅是在局部同时开启了 enableImplicitConversion,可能会引入一个极其隐蔽且违反直觉的 Bug:前端传过来的布尔值恒为 true。

    +

    一个简单的筛选功能

    假设正在开发一个电子商务平台的 API,需要实现一个产品列表的筛选功能。希望能够根据产品是否有库存 (hasStock)、是否为特色产品 (isFeatured) 等布尔条件进行筛选。

    +

    前端发出的请求 URL 可能如下所示: /products?filter[hasStock]=true&filter[isFeatured]=false

    +

    在 NestJS 后端,首先会在 main.ts 中配置一个全局的 ValidationPipe,以自动处理 DTO 的验证和转换。为了方便,通常会启用 enableImplicitConversion,期望它能自动将 URL 查询参数中的字符串(如 "123", "true")转换为 DTO 中定义的类型(number, boolean):

    +
    // main.ts
    import { ValidationPipe } from '@nestjs/common';
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';

    async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    app.useGlobalPipes(
    new ValidationPipe({
    transform: true, // 启用转换
    whitelist: true,
    forbidNonWhitelisted: true,
    transformOptions: {
    // 启用基于 TypeScript 类型的隐式转换
    enableImplicitConversion: true,
    },
    }),
    );

    // ... 其他配置
    await app.listen(3000);
    }
    bootstrap();
    + +

    接着,定义一个 ProductFilterDto 来接收这些筛选条件。

    +

    一个看似正确的 DTO 定义:

    +
    // product-filter.dto.ts
    import { IsBoolean, IsOptional } from 'class-validator';

    export class ProductFilterDto {
    @IsOptional()
    @IsBoolean()
    hasStock?: boolean;

    @IsOptional()
    @IsBoolean()
    isFeatured?: boolean;
    }
    + +

    在控制器中使用这个 DTO:

    +
    // products.controller.ts
    @Controller('products')
    export class ProductsController {
    @Get()
    find(@Query('filter') filter: ProductFilterDto) {
    // 期望 filter.isFeatured 的值为 boolean false
    console.log(filter);
    // ... 业务逻辑
    }
    }
    + +

    当请求 .../products?filter[isFeatured]=false 到达时,本来期望在 find 方法中得到的 filter.isFeatured 的值是布尔类型的 false。然而,控制台输出的结果却令人意外:{ isFeatured: true }

    +

    问题剖析

    这个问题的根源在于 class-transformer 内部的转换执行顺序,以及 JavaScript 中 Boolean 函数的类型转换行为。

    +

    所有通过 URL 查询参数传递的值,其本质都是字符串。当 NestJS 接收到请求时,filter.isFeatured 的原始值是字符串 "false"。

    +

    ValidationPipe 启动 class-transformer 的转换流程。由于在全局管道中设置了 enableImplicitConversion: true,转换器会首先检查 DTO 属性的 TypeScript 类型。

    +
      +
    1. 隐式转换优先执行:class-transformer 看到 ProductFilterDto 中的 isFeatured 属性被声明为 boolean 类型。
    2. +
    3. 错误的类型转换:它立即尝试将字符串 "false" 转换为布尔值。这个转换等同于执行 Boolean("false")。在 JavaScript 中,任何非空字符串(包括 "false")通过 Boolean() 构造函数转换后都会得到 true。
    4. +
    5. 结果覆盖:这个错误的 true 值被作为该属性的转换结果。
    6. +
    +

    此时,即使尝试添加一个自定义的 @Transform 装饰器来手动处理这个问题,也为时已晚。

    +

    例如,定义一个 booleanTransformer:

    +
    // boolean-transformer.ts
    export const booleanTransformer = ({ value }: { value: any }) => {
    if (typeof value === 'string') {
    return value === 'true';
    }
    return value;
    };
    + +

    然后更新 dto:

    +
    // product-filter.dto.ts (错误的尝试)
    import { Transform } from 'class-transformer';
    import { IsBoolean, IsOptional } from 'class-validator';
    import { booleanTransformer } from './boolean-transformer';

    export class ProductFilterDto {
    // ...
    @IsOptional()
    @Transform(booleanTransformer) // 添加自定义转换
    @IsBoolean()
    isFeatured?: boolean;
    }
    + +

    流程会变成这样:

    +
      +
    1. 隐式转换首先执行:Boolean("false") -> true。
    2. +
    3. @Transform 装饰器执行:此时传递给 booleanTransformer 的 value 已经是上一步错误转换后的布尔值 true,而不是原始的字符串 "false"。转换函数无从下手。
    4. +
    +

    最终结果依然是 true。

    +

    ## 解决方案:用 any 绕过隐式转换

    要解决这个问题,核心在于阻止 class-transformer 进行那次错误的、优先的隐式转换,从而确保自定义 @Transform 函数能接收到最原始的字符串值。

    +

    最直接且侵入性最小的方法,是将 DTO 中相关属性的 TypeScript 类型从 boolean 改为 any。

    +

    修正后的 DTO 定义:

    +
    // product-filter.dto.ts (正确的实现)
    import { Transform } from 'class-transformer';
    import { IsBoolean, IsOptional } from 'class-validator';
    import { booleanTransformer } from './boolean-transformer';

    // 一个更健壮的 booleanTransformer
    export const robustBooleanTransformer = ({ value }: { value: string }) =>
    value === 'true' ? true : value === 'false' ? false : value;

    export class ProductFilterDto {
    @IsOptional()
    @Transform(robustBooleanTransformer)
    @IsBoolean()
    hasStock?: any; // <-- 类型从 boolean 改为 any

    @IsOptional()
    @Transform(robustBooleanTransformer)
    @IsBoolean()
    isFeatured?: any; // <-- 类型从 boolean 改为 any
    }
    + +

    这个改动虽然看起来放弃了 TypeScript 的类型检查,但在这个特定的场景下,它非常安全且有效。原因如下:

    +
      +
    1. 阻止隐式转换:当 class-transformer 看到属性类型是 any 时,它不知道该隐式转换成什么目标类型,因此会“跳过”这个属性的隐式转换步骤。
    2. +
    3. @Transform 接管:如此一来,原始的字符串值("true" 或 "false")就能原封不动地传递给 robustBooleanTransformer 函数。该函数现在可以正确地将字符串转换为期望的布尔值。
    4. +
    5. @IsBoolean 守门:在自定义转换完成后,@IsBoolean() 装饰器会进行最后的验证,确保存入 DTO 的最终值必须是 true 或 false。这保证了在业务逻辑中,该属性的类型是绝对安全的。
    6. +
    +

    好用,爱用。

    ]]>
    Technique -- cgit v1.2.3