如何基于Async Config创建Nestjs模块?

发布于 2025-02-13 12:15:38 字数 1183 浏览 0 评论 0原文

我正在尝试创建一个类似的DB模块:

const dbProvider = {
  provide: 'DB',
  useFactory: async (configService:ConfigService) => {
    const dbUrl = configService.get<string>('DB_URL')
    return Knex({
      client: 'pg',
      connection: dbUrl
    })
  },
  inject: [ConfigService]
}

@Module({
  providers: [ConfigService, dbProvider],
  exports: [dbProvider],
})
export class DbModule {}

这是AppModule定义:

@Module({
  controllers: [AppController],
  providers: [Logger, AppService, {
    provide: ConfigService,
    useFactory: getConfigFactory(['DB_URL']),
  }],
  exports: [ConfigService]
})
export class AppModule {}

and:

export function getConfigFactory(paramsToLoad: string[]) {
    return async () => {await getConfigService(paramsToLoad)}
}
export async function getConfigService(paramsToLoad: string[]) {

    const paramStoreParams = await loadParamStore(paramsToLoad)
    return new ConfigService(paramStoreParams)
}

loadParamStore使用SSM从SSM获取参数,

问题是,当执行DB设置时(上图),ConfigService仅包含所采集的ENVS从.env,DB_URL仅在以后的阶段加载(经过验证),因此在构建KNEX时,db_url尚未可用。

是否有正确的Nestjs实现此类功能的方法?

I'm trying to create a DB module like so:

const dbProvider = {
  provide: 'DB',
  useFactory: async (configService:ConfigService) => {
    const dbUrl = configService.get<string>('DB_URL')
    return Knex({
      client: 'pg',
      connection: dbUrl
    })
  },
  inject: [ConfigService]
}

@Module({
  providers: [ConfigService, dbProvider],
  exports: [dbProvider],
})
export class DbModule {}

This is the AppModule definition:

@Module({
  controllers: [AppController],
  providers: [Logger, AppService, {
    provide: ConfigService,
    useFactory: getConfigFactory(['DB_URL']),
  }],
  exports: [ConfigService]
})
export class AppModule {}

and:

export function getConfigFactory(paramsToLoad: string[]) {
    return async () => {await getConfigService(paramsToLoad)}
}
export async function getConfigService(paramsToLoad: string[]) {

    const paramStoreParams = await loadParamStore(paramsToLoad)
    return new ConfigService(paramStoreParams)
}

loadParamStore uses SSM to fetch parameters from SSM

The issue is, that when the DB setup is performed (above), the ConfigService only contains the envs taken from .env, DB_URL is only loaded at a later stage (verified), so at the time of building knex, DB_URL is not yet available.

Is there a correct Nestjs way to achieve such functionality?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

时间你老了 2025-02-20 12:15:38

首先,首先删除您的自定义getConfigFactory()getConfigservice()函数。您不应自己创建configservice实例。通过导入configmodule,它可以为您实例化。当然,您可以自己启动并传递数据,但这是configmodule的内部用法。

如果要从外部源加载配置,请在您的情况下进行SSM,请使用自定义配置文件功能。

https://docs.nestjs.coms.coms.com/techniq.s.com/techniques/techniques/configuration#custom-confom-configuratire-files-files

添加一个新文件,例如外部config.ts到您的项目。在这里,您可以编码如何加载外部配置。它应该返回返回配置对象的出厂功能。

export interface SsmConfiguration {
  database: {
    url: string;
  };
}

export async function loadExternalConfiguration(): Promise<SsmConfiguration> {
  // Load the configuration from SSM here.
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        database: {
          url: 'localhost',
        },
      });
    }, 1000);
  });
}

顺便说一句,这仍然可以与.env文件结合使用。

接下来,将configModule导入为AppModule中的全局模块。这样,它的提供商(configservice)可以在其他模块中使用,而无需重新构成configmodule。使用其forroot()方法,并指定load选项中早期创建的自定义出厂功能。

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // You can specify multiple config factory functions.
      load: [loadExternalConfiguration],
    }),
    DbModule
    ...
  ]
  controllers: [AppController],
  providers: [...]
})
export class AppModule {}

最后但并非最不重要的一点是将configService注入返回knex实例的出厂功能。那时,您应该能够读取配置configservice已为您读取。只需确保使用正确的点符号来访问您自定义出厂功能返回的配置对象的属性即可。

@Module({
  imports: [],
  providers: [
    {
      provide: DB_TOKEN,
      useFactory: (config: ConfigService) => {
        const url = config.get<string>('database.url');
        return Knex({...})
      },
      inject: [ConfigService],
    },
  ],
  exports: [DB_TOKEN],
})
export class DbModule {}

如果运行该应用程序,则在启动过程中会注意到1S延迟,当“获取”配置时,在其模拟延迟时会延迟。

现在,您可以注入db令牌标识的提供商,这些提供商是appModule的一部分的提供商。

First off, start by removing your custom getConfigFactory() and getConfigService() functions. You should not create the ConfigService instance yourself. It is instantiated for you by importing the ConfigModule. Sure, you can new it up yourself and pass it data, but that's meant for internal usage by the ConfigModule.

If you want to load configuration from an external source, SSM in your case, then use the custom configuration file feature.

https://docs.nestjs.com/techniques/configuration#custom-configuration-files

Add a new file, e.g. external-config.ts to your project. Here you can code how to load your external configuration. It should return a factory function that returns a configuration object.

export interface SsmConfiguration {
  database: {
    url: string;
  };
}

export async function loadExternalConfiguration(): Promise<SsmConfiguration> {
  // Load the configuration from SSM here.
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        database: {
          url: 'localhost',
        },
      });
    }, 1000);
  });
}

By the way, this can still be combined with .env files.

Next import the ConfigModule as a global module in the AppModule. This way its providers (ConfigService) can be used in other modules without having to re-import the ConfigModule. Use its forRoot() method and specify the custom factory function created earlier in the load option.

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // You can specify multiple config factory functions.
      load: [loadExternalConfiguration],
    }),
    DbModule
    ...
  ]
  controllers: [AppController],
  providers: [...]
})
export class AppModule {}

Last, but not least inject the ConfigService in the factory function that returns the Knex instance. At that time you should be able to read the configuration the ConfigService has read for you. Just make sure to use the correct dotted notation to access the properties of the configuration object returned by your custom factory function.

@Module({
  imports: [],
  providers: [
    {
      provide: DB_TOKEN,
      useFactory: (config: ConfigService) => {
        const url = config.get<string>('database.url');
        return Knex({...})
      },
      inject: [ConfigService],
    },
  ],
  exports: [DB_TOKEN],
})
export class DbModule {}

If you run the application you'll notice a 1s delay during the startup when its simulating a delay when "fetching" the configuration.

You can now inject the provider identified by the DB token in your providers that are part of the AppModule.

断肠人 2025-02-20 12:15:38

所以我遇到了类似的问题。我的问题陈述是 - nest js中的AWS秘密。这必须在实例化其他模块之前完成。示例 - typeorm,JWT模块都需要配置值。

我是怎么实现的?通过构建一个辅助功能,该功能从AWS获取秘密并在过程中持续存在这些

export async function secretLoader(){
  const secretsManager = new SecretsManager({
    region: process.env['AWS_REGION'],
  });
  const secretsIds = [process.env['AWS_SECRET_ID']];

  const commands = secretsIds.map(
    (secretId) =>
      new GetSecretValueCommand({
        SecretId: secretId,
      }),
  );
  const resp = commands.map((command) =>
    secretsManager.send(command),
  );
  const secrets = await Promise.all(resp);
  const response = secrets.reduce((acc, secret) => {
    const sec = JSON.parse(<string>secret.SecretString);
    return {
      ...acc,
      ...sec,
    };
  }, {});
  Object.keys(response).forEach((key) => {
    process.env[key] = response[key];
  });
}

。导入并运行此功能。

import { secretLoader } from './secretLoader';

async function bootstrap() {
  await secretLoader();
  initializeTransactionalContext(); // Initialize cls-hooked
  const app = await NestFactory.create(AppModule);
  const configService: ConfigService = app.get(ConfigService);
  await app.listen(configService.get<number>('PORT') || 3000);
}
bootstrap();

干杯!

PS-
我尝试在“加载”中使用Config Factory方法。但是它不起作用,因为在配置模块之前初始化了一些模块。

So I faced a similar problem. My problem Statement was - Load AWS Secrets in Nest JS. This had to be done before any other module is instantiated. Example - TypeORM, JWT Module both required config values.

How I achieved it? By building a helper function which fetches secrets from AWS and persists those values in process.env

export async function secretLoader(){
  const secretsManager = new SecretsManager({
    region: process.env['AWS_REGION'],
  });
  const secretsIds = [process.env['AWS_SECRET_ID']];

  const commands = secretsIds.map(
    (secretId) =>
      new GetSecretValueCommand({
        SecretId: secretId,
      }),
  );
  const resp = commands.map((command) =>
    secretsManager.send(command),
  );
  const secrets = await Promise.all(resp);
  const response = secrets.reduce((acc, secret) => {
    const sec = JSON.parse(<string>secret.SecretString);
    return {
      ...acc,
      ...sec,
    };
  }, {});
  Object.keys(response).forEach((key) => {
    process.env[key] = response[key];
  });
}

You just need to set 2 properties in env files - 'AWS_REGION' and 'AWS_SECRET_ID'

Next, in the main.ts file. Import and Run this function.

import { secretLoader } from './secretLoader';

async function bootstrap() {
  await secretLoader();
  initializeTransactionalContext(); // Initialize cls-hooked
  const app = await NestFactory.create(AppModule);
  const configService: ConfigService = app.get(ConfigService);
  await app.listen(configService.get<number>('PORT') || 3000);
}
bootstrap();

Cheers!

PS -
I tried using config factory method in 'load'. But it doesn't work since some modules initialised before config module.

只有影子陪我不离不弃 2025-02-20 12:15:38

我遇到了一个类似的问题,并且是通过创建具有异步init函数的配置的类来解决此问题的ABOE。
此异步INIT函数将从main.ts文件中调用,这可能是异步的。这样,当将配置模块加载到应用程序模块(或app.module导入的模块)时,该配置已经在类的实例上。

export class EnvConfig {
  public config: Config;

  async init() {
    this.config= await fetchConfig();
  }
}

export const envConfig = new EnvConfig();

应用模块:

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [() => envConfig.secrets],
      isGlobal: true
    })
  ]
})
export class AppModule {
}

main.ts:

async function bootstrap() {
  await envConfig.init();
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000);
}
bootstrap();

I faced a similar problem, and was aboe to solve this by creating a class that holds the config with an async init function.
this async init function will be called from the main.ts file, which can be asynchronous. This way, when the config module is loaded to app module (or to a module that app.module imports), the configuration is already on the class's instance.

export class EnvConfig {
  public config: Config;

  async init() {
    this.config= await fetchConfig();
  }
}

export const envConfig = new EnvConfig();

app module:

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [() => envConfig.secrets],
      isGlobal: true
    })
  ]
})
export class AppModule {
}

main.ts:

async function bootstrap() {
  await envConfig.init();
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000);
}
bootstrap();
烂柯人 2025-02-20 12:15:38

因此,@eedoh Kabelly的答案很好。我的用例是从 aws Secrets Manager 中获取秘密。但是,我还想要.env文件值的后备。如果.env文件包含密钥,则应使用它;否则,它将使用AWS Secrets Manager键。基于此,我创建了aws-secrets-config.ts文件。

import {Logger} from '@nestjs/common';
import fetchSecretsFromAWS from './fetch-aws-secrets';

export type AwsSecretsConfigType = Record<string, any>;

export class AWSSecretsConfig {
  public config: AwsSecretsConfigType = {};
  private readonly logger = new Logger(AWSSecretsConfig.name);

  async init() {
    this.logger.log('Init Loading AWS Secrets Config');
    await this.loadSecrets();
  }

  async loadSecrets() {
    try {
      const secrets = await fetchSecretsFromAWS();

      const keys = [
        'MONGO_USER',
        'MONGO_PASSWORD',
        'MONGO_CLUSTER',
        'MONGO_DATABASE',
        'ADMIN_USERNAME',
        'ADMIN_PASSWORD',
        'ADMIN_EMAIL',
        'BUCKET_NAME',
      ];

      const config = keys.reduce(
        (acc, key) => {
          acc[key] = process.env[key] || secrets[key];
          if (acc[key] === undefined) {
            this.logger.warn(`Configuration for ${key} is missing`);
          }
          return acc;
        },
        {} as Record<string, string | undefined>
      );

      const mongodbUri =
        process.env.MONGO_DB_URL ||
        `mongodb+srv://${config.MONGO_USER}:${config.MONGO_PASSWORD}@${config.MONGO_CLUSTER}.mongodb.net/${config.MONGO_DATABASE}?retryWrites=true&w=majority`;

      this.config = {
        MONGO_DB_URL: mongodbUri,
        ...config
      };

      this.logger.log('AWS Secrets Config loaded successfully');
    } catch (error) {
      this.logger.error('Failed to load configuration', error);
      throw new Error('Failed to load configuration');
    }
  }

  get<T>(key: string, defaultValue?: T): T | undefined {
    const value = key.split('.').reduce((o, i) => o?.[i], this.config);
    return (value !== undefined ? value : defaultValue) as T | undefined;
  }

  has(key: string): boolean {
    return this.get(key) !== undefined;
  }
}

export const AWSSecretsEnv = new AWSSecretsConfig();

fetch-aws-secrets.ts

import {SecretsManager} from '@aws-sdk/client-secrets-manager';

export default async () => {
  if (!process.env.AWS_SECRET_NAME) {
    return {};
  }
  // Create a Secrets Manager client
  const secretsManager = new SecretsManager({
    region: process.env.AWS_REGION,
    credentials:
      process.env.AWS_ACCESS_KEY && process.env.AWS_SECRET_KEY
        ? {
            accessKeyId: process.env.AWS_ACCESS_KEY,
            secretAccessKey: process.env.AWS_SECRET_KEY
          }
        : undefined
  });

  try {
    // Attempt to retrieve the secret
    const data = await secretsManager.getSecretValue({
      SecretId: process.env.AWS_SECRET_NAME
    });

    if (!data.SecretString) {
      throw new Error('The secret value is empty or undefined');
    }

    const secrets = JSON.parse(data.SecretString);
    return secrets as Record<string, string>;
  } catch (err) {
    // Rethrow the error with a general message
    // eslint-disable-next-line no-console
    console.error(`Failed to fetch AWS Secrets: ${err.message}`);
  }
};

main.ts

import {Logger} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {AppModule} from './app/app.module';
import {configureApp} from './common/config/config';
import {AWSSecretsEnv} from './config/aws-secrets-config';

async function bootstrap() {
// Load configuration before creating the app
await AWSSecretsEnv.init();
const app = await NestFactory.create(AppModule);

const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3333;

configureApp(app);

await app.listen(port);
Logger.log(`

So the answer by @Eedoh Kabelly is good. My use case was to fetch secrets from AWS Secrets Manager. However, I also wanted a fallback for .env file values. If the .env file contains the key, it should use that; otherwise, it will use the AWS Secrets Manager Keys. Based on this, I created the aws-secrets-config.ts file.

import {Logger} from '@nestjs/common';
import fetchSecretsFromAWS from './fetch-aws-secrets';

export type AwsSecretsConfigType = Record<string, any>;

export class AWSSecretsConfig {
  public config: AwsSecretsConfigType = {};
  private readonly logger = new Logger(AWSSecretsConfig.name);

  async init() {
    this.logger.log('Init Loading AWS Secrets Config');
    await this.loadSecrets();
  }

  async loadSecrets() {
    try {
      const secrets = await fetchSecretsFromAWS();

      const keys = [
        'MONGO_USER',
        'MONGO_PASSWORD',
        'MONGO_CLUSTER',
        'MONGO_DATABASE',
        'ADMIN_USERNAME',
        'ADMIN_PASSWORD',
        'ADMIN_EMAIL',
        'BUCKET_NAME',
      ];

      const config = keys.reduce(
        (acc, key) => {
          acc[key] = process.env[key] || secrets[key];
          if (acc[key] === undefined) {
            this.logger.warn(`Configuration for ${key} is missing`);
          }
          return acc;
        },
        {} as Record<string, string | undefined>
      );

      const mongodbUri =
        process.env.MONGO_DB_URL ||
        `mongodb+srv://${config.MONGO_USER}:${config.MONGO_PASSWORD}@${config.MONGO_CLUSTER}.mongodb.net/${config.MONGO_DATABASE}?retryWrites=true&w=majority`;

      this.config = {
        MONGO_DB_URL: mongodbUri,
        ...config
      };

      this.logger.log('AWS Secrets Config loaded successfully');
    } catch (error) {
      this.logger.error('Failed to load configuration', error);
      throw new Error('Failed to load configuration');
    }
  }

  get<T>(key: string, defaultValue?: T): T | undefined {
    const value = key.split('.').reduce((o, i) => o?.[i], this.config);
    return (value !== undefined ? value : defaultValue) as T | undefined;
  }

  has(key: string): boolean {
    return this.get(key) !== undefined;
  }
}

export const AWSSecretsEnv = new AWSSecretsConfig();

fetch-aws-secrets.ts

import {SecretsManager} from '@aws-sdk/client-secrets-manager';

export default async () => {
  if (!process.env.AWS_SECRET_NAME) {
    return {};
  }
  // Create a Secrets Manager client
  const secretsManager = new SecretsManager({
    region: process.env.AWS_REGION,
    credentials:
      process.env.AWS_ACCESS_KEY && process.env.AWS_SECRET_KEY
        ? {
            accessKeyId: process.env.AWS_ACCESS_KEY,
            secretAccessKey: process.env.AWS_SECRET_KEY
          }
        : undefined
  });

  try {
    // Attempt to retrieve the secret
    const data = await secretsManager.getSecretValue({
      SecretId: process.env.AWS_SECRET_NAME
    });

    if (!data.SecretString) {
      throw new Error('The secret value is empty or undefined');
    }

    const secrets = JSON.parse(data.SecretString);
    return secrets as Record<string, string>;
  } catch (err) {
    // Rethrow the error with a general message
    // eslint-disable-next-line no-console
    console.error(`Failed to fetch AWS Secrets: ${err.message}`);
  }
};

Call the init function before creation of Nest app in the main.ts

import {Logger} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {AppModule} from './app/app.module';
import {configureApp} from './common/config/config';
import {AWSSecretsEnv} from './config/aws-secrets-config';

async function bootstrap() {
  // Load configuration before creating the app
  await AWSSecretsEnv.init();
  const app = await NestFactory.create(AppModule);

  const globalPrefix = 'api';
  app.setGlobalPrefix(globalPrefix);
  const port = process.env.PORT || 3333;

  configureApp(app);

  await app.listen(port);
  Logger.log(`???? Application is running on: http://localhost:${port}/${globalPrefix}`);
}

bootstrap();

Load the configuration in the ConfigModule within app.module.ts. Please note that load will only allow synchronous configurations to be loaded. That's why it is important to load the secrets in main.ts before the ConfigModule attempts to load them.

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      expandVariables: true,
      load: [
        () => AWSSecretsEnv.config // Use the config loaded by `AWSSecretsEnv.config`
      ]
    }),
    DatabaseModule,
  ],
})

Your Database Module can now use the secret directly from confService

// database.module.ts
import {Module} from '@nestjs/common';
import {ConfigService} from '@nestjs/config';
import {MongooseModule} from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      useFactory: async (configService: ConfigService) => {
        const mongoUrl = configService.get<string>('MONGO_DB_URL');
        if (!mongoUrl) {
          throw new Error('MongoDB URI is not defined');
        }
        return {uri: mongoUrl};
      },
      inject: [ConfigService]
    })
  ]
})
export class DatabaseModule {}

In your .env file, you only need to include:

  • AWS_REGION
  • AWS_SECRET_NAME

If you are not using an instance role or are testing locally, you will also need to add:

  • AWS_ACCESS_KEY
  • AWS_SECRET_KEY

I’ve tested this setup and have successfully accessed the confService in other parts of the app; it works perfectly. The logic is to load the secrets before the Nest app is created because the ConfigModule does not support forRootAsync, and the load function does not allow for asynchronous configuration loading.

If you need the secrets to be fetched dynamically at runtime, you can directly invoke fetchSecretsFromAWS() in your app to get the latest values.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文