使用CLI和NestJS应用程序的一种配置来配置TypeORM

时间:2020-01-25 21:16:42

标签: nestjs typeorm

我在NestJS应用程序中使用TypeORM。我的app.module.ts具有非常标准的设置并可以运行:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from './config/config.service';
import { ConfigModule } from './config/config.module';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],

      // @ts-ignore issues with the type of the database
      useFactory: async (configService: ConfigService) => ({
        type: configService.getDBType(),
        host: configService.getDBHost(),
        port: configService.getDBPort(),
        username: configService.getDBUser(),
        password: configService.getDBPassword(),
        database: configService.getDBName(),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: true,
      }),
      inject: [ConfigService],
    }),
    ConfigModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

这是东西。如果要在CLI上运行迁移,则需要安装ormconfig.js。我不想在ormconfig.js和我的config.service.js中都复制凭据。我创建了一个.env文件,如下所示:

TYPEORM_CONNECTION = mysql
TYPEORM_HOST = app-db
TYPEORM_USERNAME = user
TYPEORM_PASSWORD = password
TYPEORM_DATABASE = db-dev
TYPEORM_PORT = 3306
TYPEORM_SYNCHRONIZE = true
TYPEORM_LOGGING = true
TYPEORM_ENTITIES = src/**/*.ts
TYPEORM_MIGRATIONS = src/migrations/**/*.ts
TYPEORM_MIGRATIONS_TABLE_NAME = migrations

由于env vars的定义如下所示:TypeORM Documentation,因此我继续进行重构,将app.module.ts重构如下:

@Module({
  imports: [TypeOrmModule.forRoot(), ConfigModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

现在,当我使用DATABASE_HOST时,遇到env变量DATABASE_PORTtypeorm cli等缺失的错误。

这是我的config.service.ts

import * as dotenv from 'dotenv';
import * as Joi from '@hapi/joi';
import * as fs from 'fs';
import { Injectable } from '@nestjs/common';
import { keys, pick } from 'lodash';

export type EnvConfig = Record<string, string>;

@Injectable()
export class ConfigService {
  private readonly envConfig: Record<string, string>;

  constructor(filePath: string) {
    const envNames = keys(this.getJoiObject());
    const envFromProcess = pick(process.env, envNames);
    const envFromFile = fs.existsSync(filePath) ? dotenv.parse(fs.readFileSync(filePath)) : {};
    const envConfig = Object.assign(envFromFile, envFromProcess);

    this.envConfig = this.validateInput(envConfig);
  }

  private validateInput(envConfig: EnvConfig): EnvConfig {
    const envVarsSchema: Joi.ObjectSchema = Joi.object(this.getJoiObject());

    const { error, value: validatedEnvConfig } = envVarsSchema.validate(envConfig);

    if (error) {
      throw new Error(`Config validation error: ${error.message}`);
    }

    return validatedEnvConfig;
  }

  private getJoiObject(): object {
    return {
      NODE_ENV: Joi.string()
        .valid('development', 'production', 'test', 'provision')
        .default('development'),
      PORT: Joi.number().default(3000),

      DATABASE_TYPE: Joi.string()
        .valid('mysql')
        .default('mysql'),

      DATABASE_HOST: Joi.string().required(),
      DATABASE_PORT: Joi.number().required(),
      DATABASE_NAME: Joi.string().required(),
      DATABASE_USER: Joi.string().required(),
      DATABASE_PASSWORD: Joi.string().required(),
    };
  }

  get(key: string): string {
    return this.envConfig[key];
  }

  getPort(): number {
    return parseInt(this.envConfig.PORT, 10);
  }

  getDBType(): string {
    return this.envConfig.DATABASE_TYPE;
  }

  getDBHost(): string {
    return this.envConfig.DATABASE_HOST;
  }

  getDBPort(): number {
    return parseInt(this.envConfig.DATABASE_PORT, 10);
  }

  getDBName(): string {
    return this.envConfig.DATABASE_NAME;
  }

  getDBUser(): string {
    return this.envConfig.DATABASE_USER;
  }

  getDBPassword(): string {
    return this.envConfig.DATABASE_PASSWORD;
  }
}

这里的TYPEORM_ env var是否互斥?为了让TypeORM在CLI和NestJS应用程序的上下文中起作用,我们是否真的需要将环境变量复制为它们的DATABASE_形式?这似乎是非常错误的。使TypeORM在CLI(我想在开发中进行迁移)和NestJS应用程序中工作而又不必重复这些变量的正确方法是什么?

6 个答案:

答案 0 :(得分:7)

解决方案

此解决方案使您可以在CLI使用和应用程序使用中使用相同的参数,而不会遇到代码重复。

使用path.join():

config.service.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { join } from 'path';

// tslint:disable-next-line: no-var-requires
require('dotenv').config();

class ConfigService {

   constructor(private env: { [k: string]: string | undefined }) {}

   //...etc

  public getTypeOrmConfig(): TypeOrmModuleOptions {
    return {

      // obviously, change these if you're using a different DB
      type: 'postgres',
      host: this.getValue('POSTGRES_HOST'),
      port: Number(this.getValue('POSTGRES_PORT')),
      username: this.getValue('POSTGRES_USER'),
      password: this.getValue('POSTGRES_PASSWORD'),
      database: this.getValue('POSTGRES_DB'),

      entities: [join(__dirname, '**', '*.entity.{ts,js}')],

      migrationsTableName: 'migration',
      migrations: [join(__dirname, '..', 'migrations', '*.ts')],

      cli: {
        migrationsDir: '../migrations',
      },

      synchronize: true,
      ssl: this.isProduction(),
    };
  }
}

const configService = new ConfigService(process.env);

export default configService;

app.module.ts

如果使用不带参数的TypeOrmModule.forRoot(),则默认情况下,它将在项目的根目录下查找ormconfig.json文件。您也可以为其提供TypeOrmModuleOptions参数,这是我建议的。我建议这样做就像Riajul Islam和Muhammad Zeeshan所做的那样:

@Module({
    imports: [
        TypeOrmModule.forRoot(configService.getTypeOrmConfig()),
        // add other modules here as well
    ]
})
export class AppModule {}

write-type-orm-config.ts

这是一个简单的脚本,可让您生成ormconfig.json文件,该文件可用于CLI操作。

import configService from '../src/config.service';
import fs = require('fs');

fs.writeFileSync(
  'ormconfig.json',
  JSON.stringify(configService.getTypeOrmConfig(), null, 2), // last parameter can be changed based on how you want the file indented
);

项目结构

您可能希望根据自己的文件结构以及实体的命名方式来更改entitiesmigrations属性的精确连接语句。

我的项目结构是:

.env // ALL environmental variables are stored here, both for Node and for other processes such as Docker
src
   | config.service.ts
   | app.module.ts // calls configService.getTypeOrmConfig()
   | main.ts
scripts // for CLI only operations
   | seed.ts // calls configService.getTypeOrmConfig() when creating a ConnectionOptions object for the database
   | write-type-orm-config.ts // calls configService.getTypeOrmConfig() to create an ormconfig.json file at the root, which I use for some NPM scripts
migrations
   | DB migrations go here...

示例package.json脚本

这可能是您需要ormconfig.json文件的地方。

  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:dev:db:seed": "ts-node -r tsconfig-paths/register scripts/seed.ts",
    "start:debug": "nest start --debug --watch",
    "start:dev:autoconfig": "yarn run typeorm:migration:run && yarn run start:dev:db:seed",
    "start:prod": "node dist/src/main",
    "pretypeorm": "(rm ormconfig.json || :) && ts-node -r tsconfig-paths/register scripts/write-type-orm-config.ts",
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
    "typeorm:migration:generate": "yarn run typeorm -- migration:generate -n",
    "typeorm:migration:run": "yarn run typeorm -- migration:run"
  },

请注意,在生成迁移时,您必须指定迁移名称:yarn run typeorm:migration:generate ${MIGRATION_NAME}

参考文献

https://medium.com/better-programming/typeorm-migrations-explained-fdb4f27cb1b3(有关使用NestJS配置TypeORM环境的好文章)
https://github.com/GauSim/nestjs-typeorm(上面的Git存储库)

答案 1 :(得分:1)

我发现您的代码没有任何问题,可能是我做不到,我也遇到了同样的问题,我可以为您提供代码示例,希望对您有所帮助。

    1. .env
APP_PORT=
TYPEORM_CONNECTION = <mysql | mongodb | pg>
TYPEORM_HOST = 
TYPEORM_USERNAME = 
TYPEORM_PASSWORD = 
TYPEORM_DATABASE = 
TYPEORM_PORT = 
TYPEORM_SYNCHRONIZE = <true | false>
TYPEORM_LOGGING = <true | false>

TYPEORM_ENTITIES=**/*.entities.ts,src/**/*.entities.ts,src/**/*.entity.ts
TYPEORM_MIGRATIONS=database/migration/*.ts
TYPEORM_MIGRATIONS_DIR=database/migration

    1. config.service.ts
import {TypeOrmModuleOptions} from '@nestjs/typeorm';
// tslint:disable-next-line: no-var-requires
require('dotenv').config();

class ConfigService {
    constructor(private env: {[key: string]: string | undefined}) {}

    private getValue(key: string, throwOnMissing = true): string {
        const value = this.env[key];
        if (!value && throwOnMissing) {
            throw new Error(`config error - missing env.${key}`);
        }

        return value;
    }

    public ensureValues(keys: string[]) {
        keys.forEach(key => this.getValue(key, true));
        return this;
    }
    public getTypeOrmConfig(): TypeOrmModuleOptions {
        return {
            type: 'mysql',
            keepConnectionAlive: true,
            host: process.env.TYPEORM_HOST,
            port: parseInt(process.env.TYPEORM_PORT) || 3306,
            database: process.env.TYPEORM_DATABASE,
            username: process.env.TYPEORM_USERNAME,
            password: process.env.TYPEORM_PASSWORD,
            entities: [__dirname + '/../**/*.entities{.ts,.js}']
        };
    }
}

const configService = new ConfigService(process.env).ensureValues([
    'TYPEORM_DATABASE',
    'TYPEORM_USERNAME',
    'TYPEORM_PASSWORD'
]);

export {configService};

    1. app.module.ts
@Module({
    imports: [
        TypeOrmModule.forRoot(configService.getTypeOrmConfig()),
    ]
})
export class AppModule {}

请让我知道此解决方案是否有效

答案 2 :(得分:0)

FinallyStatic答案的改进(不是真的,只是使用NestJs配置文档)。我认为这样更干净。

db-config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { registerAs } from "@nestjs/config";
import { config as setConfig } from 'dotenv';

setConfig();
setConfig({ path: '.dev.env' }); // use this if you use another .env file. Take the two setConfig if you use .env + other.env

export default registerAs('typeOrmConfig', (): TypeOrmModuleOptions => ({
    type: 'mysql',
    host: process.env.MYSQL_HOST || 'localhost',
    port: Number(process.env.MYSQL_PORT) || 3306,
    username: process.env.MYSQL_USER || 'test',
    password: process.env.MYSQL_PASSWORD || 'test',
    database: process.env.MYSQL_DATABASE || 'test',
    entities: ['dist/**/*.entity{.ts,.js}'],
    charset: "utf8mb4_unicode_ci",
    synchronize: false,
    cli: {
        migrationsDir: "src/migrations"
    },
    migrations: ["dist/migrations/**/*.js"],
}));

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import dbConfig from './config/db-config';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.dev.env',
      load: [dbConfig]
    }),
    TypeOrmModule.forRoot(dbConfig()),
    // etc...
  ],
  // etc...
});

write-type-orm-config.ts

import * as fs from 'fs';
import dbConfig from './config/db-config';

try {
    fs.unlinkSync('ormconfig.json');
}
catch { }
fs.writeFileSync(
    'ormconfig.json',
    JSON.stringify(dbConfig(), null, 4),
);

package.json

与FinalStatic答案仅差一线,因此它也与ts文件中的unlink Windows兼容。

"pretypeorm": "ts-node -r tsconfig-paths/register src/write-type-orm-config.ts",

结构

|-- src/
| |-- config/
| | |-- db-config.ts
| |
| |-- migrations/
| | |-- *migration files*
| |
| |-- app.module.ts
| |-- write-type-orm-config.ts
|
|-- .env
|-- ormconfig.json

答案 3 :(得分:0)

这就是我设法修复它的方式。使用单个配置文件,我可以在应用程序 boostrap 或使用 TypeOrm 的 CLI 上运行迁移。 package.json 上唯一的 mod 是传递 typeorm 的配置文件。

src/config/ormconfig.ts

import parseBoolean from '@eturino/ts-parse-boolean';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import * as dotenv from 'dotenv';
import { join } from 'path';

dotenv.config();

export = [
  {
    //name: 'default',
    type: 'mssql',
    host: process.env.DEFAULT_DB_HOST,
    username: process.env.DEFAULT_DB_USERNAME,
    password: process.env.DEFAULT_DB_PASSWORD,
    database: process.env.DEFAULT_DB_NAME,
    options: {
      instanceName: process.env.DEFAULT_DB_INSTANCE,
      enableArithAbort: false,
    },
    logging: parseBoolean(process.env.DEFAULT_DB_LOGGING),
    dropSchema: false,
    synchronize: false,
    migrationsRun: parseBoolean(process.env.DEFAULT_DB_RUN_MIGRATIONS),
    migrations: [join(__dirname, '..', 'model/migration/*.{ts,js}')],
    cli: {
      migrationsDir: 'src/model/migration',
    },
    entities: [
      join(__dirname, '..', 'model/entity/default/**/*.entity.{ts,js}'),
    ],
  } as TypeOrmModuleOptions,
  {
    name: 'other',
    type: 'mssql',
    host: process.env.OTHER_DB_HOST,
    username: process.env.OTHER_DB_USERNAME,
    password: process.env.OTHER_DB_PASSWORD,
    database: process.env.OTHER_DB_NAME,
    options: {
      instanceName: process.env.OTHER_DB_INSTANCE,
      enableArithAbort: false,
    },
    logging: parseBoolean(process.env.OTHER_DB_LOGGING),
    dropSchema: false,
    synchronize: false,
    migrationsRun: false,
    entities: [],
  } as TypeOrmModuleOptions,
];

src/app.module.ts

import configuration from '@config/configuration';
import validationSchema from '@config/validation';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerService } from '@shared/logger/logger.service';
import { UsersModule } from '@user/user.module';
import { AppController } from './app.controller';
import ormconfig = require('./config/ormconfig'); //path mapping doesn't work here

@Module({
  imports: [
    ConfigModule.forRoot({
      cache: true,
      isGlobal: true,
      validationSchema: validationSchema,
      load: [configuration],
    }),
    TypeOrmModule.forRoot(ormconfig[0]), //default
    TypeOrmModule.forRoot(ormconfig[1]), //other db
    LoggerService,
    UsersModule,
  ],
  controllers: [AppController],
})
export class AppModule {}

package.json

  "scripts": {
    ...
    "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config ./src/config/ormconfig.ts",
    "typeorm:migration:generate": "npm run typeorm -- migration:generate -n",
    "typeorm:migration:run": "npm run typeorm -- migration:run"
  },

项目结构

src/
├── app.controller.ts
├── app.module.ts
├── config
│   ├── configuration.ts
│   ├── ormconfig.ts
│   └── validation.ts
├── main.ts
├── model
│   ├── entity
│   ├── migration
│   └── repository
├── route
│   └── user
└── shared
    └── logger

答案 4 :(得分:0)

我想出了一个只涉及两行代码的解决方案: ormconfig.ts

import { AppConfig } from "./app.config";
export default AppConfig.getTypeOrmConfig();

这是我所做的:我首先创建了一个 app.config.ts 文件,该文件将包含通过 AppConfig 类与配置相关的所有内容。

此文件还包含一个 EnvironmentVariables 类(用于(在 AppConfig 类的 validateConfig 函数中)执行.env 文件(检查缺失值)和值转换(例如,POSTGRES_DB_PORT 变量将被转换为数字)。

代码如下: app.config.ts

import { join } from "path";
import { plainToClass } from "class-transformer";
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { MongooseModuleOptions } from "@nestjs/mongoose";
import { IsNumber, IsIn, validateSync, IsString } from "class-validator";

const enviroments = ["development", "test", "production"] as const;
type Environment = typeof enviroments[number];

class EnvironmentVariables {
  @IsIn(enviroments)
  NODE_ENV: Environment;

  @IsString()
  POSTGRES_DB_HOST: string;

  @IsNumber()
  POSTGRES_DB_PORT: number;

  @IsString()
  POSTGRES_DB_NAME: string;

  @IsString()
  POSTGRES_DB_USERNAME: string;

  @IsString()
  POSTGRES_DB_PASSWORD: string;

  @IsString()
  MONGO_DB_URI: string;
}

export class AppConfig {
  private static get env(): EnvironmentVariables {
    return plainToClass(EnvironmentVariables, process.env, {
      enableImplicitConversion: true,
    });
  }

  public static getTypeOrmConfig(): TypeOrmModuleOptions {
    return {
      type: "postgres",
      host: this.env.POSTGRES_DB_HOST,
      port: this.env.POSTGRES_DB_PORT,

      username: this.env.POSTGRES_DB_USERNAME,
      password: this.env.POSTGRES_DB_PASSWORD,
      database: this.env.POSTGRES_DB_NAME,

      // the rest of your config for TypeORM
    };
  }

  public static get getMongooseUri(): string {
    return this.env.MONGO_DB_URI;
  }

  public static getMongooseConfig(): MongooseModuleOptions {
    return { useFindAndModify: false };
  }

  public static validateConfig(config: Record<string, unknown>) {
    const validatedConfig = plainToClass(EnvironmentVariables, config, {
      enableImplicitConversion: true,
    });

    const errors = validateSync(validatedConfig, {
      skipMissingProperties: false,
    });

    if (errors.length > 0) {
      throw new Error(errors.toString());
    }

    return validatedConfig;
  }
}

然后,在 AppModule 上,我需要做的就是导入 ConfigModule(它从 .env 文件加载环境变量并使用我们的 validateConfig 函数对其进行验证)并使用 AppConfig 设置 TypeORM 和 Mongoose 的配置班级:

app.module.ts

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { MongooseModule } from "@nestjs/mongoose";

import { AppConfig } from "./app.config";
import { AppService } from "./app.service";
import { AppController } from "./app.controller";


@Module({
  imports: [
    ConfigModule.forRoot({
      validate: AppConfig.validateConfig,
    }),

    TypeOrmModule.forRoot(AppConfig.getTypeOrmConfig()),

    MongooseModule.forRoot(
      AppConfig.getMongooseUri,
      AppConfig.getMongooseConfig(),
    ),
  ],
  controllers: [AppController],
  providers: [
    AppService,
})

最后,对于 ormconfig 文件,就这么简单:

ormconfig.ts

import { AppConfig } from "./app.config";

export default AppConfig.getTypeOrmConfig();

这里还有我的项目结构,以备不时之需:

project/
    src/
        app.config.ts
        app.controller.ts
        app.module.ts
        app.service.ts
        main.ts
        ormconfig.ts
        config
    .env
    package.json
    package.lock.json

这里是我添加到 package.json 以使用 typeorm-cli 的脚本:

"typeorm:cli": "ts-node ./node_modules/typeorm/cli.js --config src/ormconfig.ts",
"run-migrations": "npm run typeorm:cli -- migration:run",
"create-migration": "npm run typeorm:cli -- migration:create --name",
"make-migrations": "npm run typeorm:cli -- migration:generate --pretty --name"

答案 5 :(得分:0)

我的配置。

  • 没有重复的配置声明
  • 无需安装和需要 dotenv。
// src/config/db.config.ts

import {registerAs} from "@nestjs/config";

export default registerAs('database', () => {
    return {
        type: "postgres",
        logging: true,
        host: process.env.DB_MAIN_HOST,
        port: parseInt(process.env.DB_MAIN_PORT),
        username: process.env.DB_MAIN_USER,
        password: process.env.DB_MAIN_PASSWORD,
        database: process.env.DB_MAIN_DATABASE,
        autoLoadEntities: true,
        // synchronize: process.env.MODE === "dev",
        entities: ["src/**/*.entity.ts"],
        migrations: ['src/migrations/*{.ts,.js}'],
        cli: {
            migrationsDir: 'src/migrations'
        },
    }
})
// app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {ConfigModule, ConfigService} from '@nestjs/config';
import {TypeOrmModule} from "@nestjs/typeorm";

import dbConfiguration from "./config/db.config";

@Module({
  imports: [
      ConfigModule.forRoot({
          isGlobal: true,
          load: [dbConfiguration],
      }),
      TypeOrmModule.forRootAsync({
          inject: [ConfigService],
          useFactory: async (configService: ConfigService) => ({...configService.get('database')})
      })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
// ormconfig.ts

import {ConfigModule} from "@nestjs/config";
import dbConfiguration from "./src/config/db.config";

ConfigModule.forRoot({
    isGlobal: true,
    load: [dbConfiguration],
})

export default dbConfiguration()

需要 ts-node

//package.json
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
"typeorm:migration:generate": "npm run typeorm -- migration:generate -n",
"typeorm:migration:run": "npm run typeorm -- migration:run"