NestJS动态模块forRootAsync与forFeature配合报错问题排查
NestJS自定义Scylla动态模块异步初始化问题
同步的forRoot与forFeature方法配合正常,但实现forRootAsync异步方法后,搭配forFeature使用时抛出“client must be defined”错误。推测是forRootAsync中的异步逻辑导致forFeature未等待客户端创建完成就执行,尝试多种方案仍未解决。
同步模块代码
import { Module, DynamicModule } from '@nestjs/common'; import { Client, mapping } from 'cassandra-driver'; import { getTableName } from './scylla.decorator'; import Mapper = mapping.Mapper; const MAPPER_PREFIX = '_MAPPER'; interface RootOptions { contactPoints: string[]; keyspace: string; username: string; password: string; localDataCenter?: string; } @Module({}) export class ScyllaModule { private static scyllaClient: Client; static forRoot(options: RootOptions): DynamicModule { return { module: ScyllaModule, providers: [ { provide: 'SCYLLA_CLIENT', useValue: this.createClient(options), }, ], exports: ['SCYLLA_CLIENT'], }; } static forFeature( entities: (new (...args) => any)[], ): DynamicModule { const newMappers = {}; const providersNames = []; for (const entity of entities) { const tableName = getTableName(entity); if (!tableName) throw new Error('No entity name provided!'); newMappers[tableName + MAPPER_PREFIX] = new Mapper(this.scyllaClient, { models: { [tableName + 'Model']: { tables: [tableName] } }, }); providersNames.push(tableName + MAPPER_PREFIX); } return { module: ScyllaModule, providers: Object.entries(newMappers).map(([key, value]) => { const mapper = value as Mapper; return { provide: key, useValue: mapper.forModel(key.replace(MAPPER_PREFIX, '')), }; }), exports: providersNames, }; } private static createClient(options: RootOptions): Client { const { contactPoints, keyspace, username, password, localDataCenter } = options; const client = new Client({ contactPoints, localDataCenter: localDataCenter || 'datacenter1', keyspace, credentials: { username, password }, }); client.connect().catch((error) => { throw new Error(error.message); }); this.scyllaClient = client; return client; } }
同步使用示例(正常运行)
ScyllaModule.forRoot({ contactPoints: ['localhost'], keyspace: 'playground', username: 'cassandra', password: 'cassandra', }), ScyllaModule.forFeature([Test])
异步方法代码
static async forRootAsync(options: { useFactory: (...args: any[]) => Promise<RootOptions> | RootOptions; inject?: any[]; }): Promise<DynamicModule> { return { module: ScyllaModule, providers: [ { provide: 'SCYLLA_CLIENT', useFactory: async (...args: any[]) => { const clientOptions = await options.useFactory(...args); return this.createClient(clientOptions); }, inject: options.inject || [], }, ], exports: ['SCYLLA_CLIENT'], }; }
异步使用示例(报错)
imports: [ ScyllaModule.forRootAsync({ useFactory: () => { const data = { contactPoints: ['localhost'], keyspace: 'playground', username: 'cassandra', password: 'cassandra', }; return data; }, }), ScyllaModule.forFeature([Test]) ],
报错信息
throw new Error('client must be defined'); ^ Error: client must be defined at new Mapper (C:\Users\User\WebstormProjects\scyllaTest\node_modules\cassandra-driver\lib\mapping\mapper.js:68:13) at Function.forFeature (C:\Users\User\WebstormProjects\scyllaTest\src\scylla\scylla.module.ts:57:47)
问题根源
forFeature在模块初始化阶段同步执行,直接访问静态变量scyllaClient,但forRootAsync的客户端创建逻辑在异步useFactory中,此时静态变量尚未赋值,导致创建Mapper时传入undefined。
修复方案
修改forFeature逻辑,移除对静态变量的依赖,通过依赖注入获取客户端,将Mapper创建延迟到provider的useFactory中执行:
修改后的完整ScyllaModule代码
import { Module, DynamicModule, Provider } from '@nestjs/common'; import { Client, mapping } from 'cassandra-driver'; import { getTableName } from './scylla.decorator'; import Mapper = mapping.Mapper; const MAPPER_PREFIX = '_MAPPER'; interface RootOptions { contactPoints: string[]; keyspace: string; username: string; password: string; localDataCenter?: string; } @Module({}) export class ScyllaModule { static forRoot(options: RootOptions): DynamicModule { return { module: ScyllaModule, providers: [ { provide: 'SCYLLA_CLIENT', useValue: this.createClient(options), }, ], exports: ['SCYLLA_CLIENT'], }; } static async forRootAsync(options: { useFactory: (...args: any[]) => Promise<RootOptions> | RootOptions; inject?: any[]; }): Promise<DynamicModule> { return { module: ScyllaModule, providers: [ { provide: 'SCYLLA_CLIENT', useFactory: async (...args: any[]) => { const clientOptions = await options.useFactory(...args); return this.createClient(clientOptions); }, inject: options.inject || [], }, ], exports: ['SCYLLA_CLIENT'], }; } static forFeature(entities: (new (...args) => any)[]): DynamicModule { const providers: Provider[] = []; const exportTokens: any[] = []; for (const entity of entities) { const tableName = getTableName(entity); if (!tableName) throw new Error('No entity name provided!'); const mapperToken = `${tableName}${MAPPER_PREFIX}`; exportTokens.push(mapperToken); providers.push({ provide: mapperToken, useFactory: (client: Client) => { const mapper = new Mapper(client, { models: { [`${tableName}Model`]: { tables: [tableName] } }, }); return mapper.forModel(`${tableName}Model`); }, inject: ['SCYLLA_CLIENT'], }); } return { module: ScyllaModule, providers, exports: exportTokens, }; } private static createClient(options: RootOptions): Client { const { contactPoints, keyspace, username, password, localDataCenter } = options; const client = new Client({ contactPoints, localDataCenter: localDataCenter || 'datacenter1', keyspace, credentials: { username, password }, }); client.connect().catch((error) => { throw new Error(error.message); }); return client; } }
修复说明
- 移除静态变量依赖:删除全局静态
scyllaClient,避免异步初始化时的状态不一致问题。 - 延迟Mapper创建:将Mapper的创建逻辑移到provider的
useFactory中,通过inject确保客户端初始化完成后再执行。 - 保持API兼容:
forRoot、forRootAsync、forFeature的调用方式完全不变,无需修改业务代码。
内容的提问来源于stack exchange,提问作者Avangardio
相关产品推荐
相关产品推荐

