8
votes

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?

6
Can you share your configService pleaseRiajul Islam
@RiajulIslam posted.randombits

6 Answers

10
votes

Solution

This solution allows you to use the same parameters for both CLI usage and application usage, without running into code duplication.

Use 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

If you use TypeOrmModule.forRoot() with no arguments, this will by default look for an ormconfig.json file at the root of your project. You can provide it with a TypeOrmModuleOptions parameter as well, which I would recommend. I would suggest doing this exactly as Riajul Islam and Muhammad Zeeshan did:

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

write-type-orm-config.ts

This is a simple script which allows you to generate the ormconfig.json file which is useful for CLI operations.

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
);

Project Structure

You will probably want to change the exact join statements for your entities and migrations properties based on your own file structure and how you name your entities.

My project structure is:

.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...

Sample package.json Scripts

This is likely where you will need an ormconfig.json file.

  "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"
  },

Note that you will have to specify a migration name when generating a migration: yarn run typeorm:migration:generate ${MIGRATION_NAME}

References

https://medium.com/better-programming/typeorm-migrations-explained-fdb4f27cb1b3 (good article about configuring a TypeORM environment with NestJS)
https://github.com/GauSim/nestjs-typeorm (Git repository for the above)

2
votes

Improvement of FinallyStatic answer (not really, just using NestJs config docs). I think it's just cleaner this way.

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

One line difference from FinallyStatic answer, so it's also Windows compatible with the unlink in the ts file.

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

Structure

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

I didn't find any problem with your code probably I am not able to do, I fetched the same problem I can provide you my code example hope it will helpful for you.

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

Please let me know if this solution is working or not

1
votes

I was able to come up with a solution that involves only two lines of code: ormconfig.ts

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

Here's what I did: I firstly created an app.config.ts file, which will contain everything related to configuration through the AppConfig class.

This file also contains an EnvironmentVariables class, (which is used (in the validateConfig function of the AppConfig class) to perform validation of the .env file (check missing values) and value conversion (POSTGRES_DB_PORT variable would be cast to number for example).

Here's the code: 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;
  }
}

Then, on the AppModule, all I needed to do was import the ConfigModule (which loads the enviroment variables from the .env file and perform validation of it using our validateConfig function) and set the configuration for TypeORM and Mongoose using the AppConfig class:

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,
})

And finally, for the ormconfig file, it was as simple as this:

ormconfig.ts

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

export default AppConfig.getTypeOrmConfig();

Here's also my project structure in case you need it:

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

And here are the scripts I added to package.json to use 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"
1
votes

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"
0
votes

This is how I've manage to fix it. With a single configuration file I can run the migrations on application boostrap or using TypeOrm's CLI. The only mod on package.json is to pass the config file for 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"
  },

Project structure

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