Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configure TypeORM with one configuration for CLI and NestJS application

Tags:

typeorm

nestjs

I am using TypeORM in my NestJS application. My app.module.ts has a very standard setup and works:

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 {}

Here's the thing. If I want to run migrations on the CLI, I need to have an ormconfig.js. I do not wish to duplicate credentials in both ormconfig.js and in my config.service.js. I created a .env file that looks like the following:

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

Since env vars are now defined as depicted here: TypeORM Documentation, I went ahead and refactored app.module.ts to look like the following:

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

Now I get errors that env vars DATABASE_HOST, DATABASE_PORT etc are missing when I use typeorm cli.

Here is my 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;
  }
}

Are the TYPEORM_ env vars mutually exclusive here? Do we really need to replicate the environment variables to their DATABASE_ form in order to have TypeORM work in CLI and in the context of the NestJS application? This seems very wrong. What is the right way to have TypeORM work both CLI(I want this for migrations in development) and in the NestJS application without having to duplicate these variables?

like image 442
randombits Avatar asked Jan 25 '20 21:01

randombits


Video Answer


1 Answers

My configuration.

  • Without duplication of configuration declarations
  • Without installing and requiring 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()

it's require 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"
like image 143
Robert Avatar answered Sep 18 '22 20:09

Robert