TypeORM connection with Jest unit test - jestjs

I have 2 db, one for development, one for testing. I want to connect to test db when running jest test, I set up 2 .env config, development use .env, testing use .env.test. But unit test can't connect db through connection.ts.
Update directory schema
app
├── .env
├── .env.test
├── loadEnv.ts
├── package.json
├── ormconfig-cli.ts
├── src
│ ├── connection.ts
│ ├── app.module.ts
│   ├── user
│ │   ├── entities
│     │   │   └── user.entity.ts
│  │   ├── user.module.ts
│   │    ├── user.resolver.spec.ts
│   │    ├── user.resolver.ts
│   │    ├── user.service.spec.ts
│  │    └── user.service.ts
Update connection error
Can't create connection, getConnection().isConnected = false and
getConnection('test') (or 'default') throw "Connection was not found." error.
//connection.ts
import { loadEnv } from '../loadEnv';
import connectionOptions from '../ormconfig-cli';
loadEnv();
async create(): Promise<Connection> {
const connOptions: ConnectionOptions = connectionOptions.find(option => option.name == process.env.NODE_ENV);
return await createConnection({ ...connOptions, name: 'default' });
},
Here's the setting:
// package.json
{
...,
"scripts": {
...,
"test": "NODE_ENV=test jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
}, {...},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"clearMocks": true
}
}
// .env.test
NODE_ENV=test
TYPEORM_HOST=localhost
TYPEORM_PORT=5432
TYPEORM_USERNAME=root
TYPEORM_PASSWORD=test
TYPEORM_DATABASE=test
# TEST_DATABASE="postgres://root:test#localhost:5432/test jest"
// loadEnv.ts
import * as dotenv from 'dotenv';
export const loadEnv = () => {
const loadFile = () => {
if (process.env.NODE_ENV === 'test') return '.env.test';
return '.env';
};
return dotenv.config({
path: loadFile(),
});
};
// ormconfig.ts
import { loadEnv } from './loadEnv';
import { ConnectionOptions } from 'typeorm';
loadEnv();
const SnakeNamingStrategy = require('typeorm-naming-strategies')
.SnakeNamingStrategy;
const connectionOptions: ConnectionOptions[] = [
{
name: 'development',
type: 'postgres',
host: String(process.env.TYPEORM_HOST),
port: Number(process.env.TYPEORM_PORT),
username: String(process.env.TYPEORM_USERNAME),
password: String(process.env.TYPEORM_PASSWORD),
database: String(process.env.TYPEORM_DATABASE),
synchronize: false,
logging: true,
cli: {
migrationsDir: 'src/migrations',
},
namingStrategy: new SnakeNamingStrategy(),
entities: ['dist/**/*.entity{.ts,.js}'],
migrations: ['dist/src/migrations/*.{js,ts}'],
migrationsTransactionMode: 'each',
},
{
name: 'test',
type: 'postgres',
host: String(process.env.TYPEORM_HOST),
port: Number(process.env.TYPEORM_PORT),
username: String(process.env.TYPEORM_USERNAME),
password: String(process.env.TYPEORM_PASSWORD),
database: String(process.env.TYPEORM_DATABASE),
synchronize: false,
dropSchema: true,
logging: true,
cli: {
migrationsDir: 'src/migrations',
},
namingStrategy: new SnakeNamingStrategy(),
entities: ['dist/**/*.entity{.ts,.js}'],
migrations: ['dist/src/migrations/*.{js,ts}'],
migrationsTransactionMode: 'each',
},
];
export = connectionOptions;
// app.module.ts
#Module({
imports: [
TypeOrmModule.forRoot({
name: 'development',
type: 'postgres',
host: process.env.TYPEORM_HOST,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
migrationsRun: true,
synchronize: false,
migrationsTransactionMode: 'each',
migrations: ['dist/src/migrations/*.{js,ts}'],
namingStrategy: new SnakeNamingStrategy(),
autoLoadEntities: true,
}),
TypeOrmModule.forRoot({
name: 'test',
type: 'postgres',
host: process.env.TYPEORM_HOST,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
logging: false,
dropSchema: true,
migrationsRun: true,
synchronize: false,
migrationsTransactionMode: 'each',
migrations: ['dist/src/migrations/*.{js,ts}'],
namingStrategy: new SnakeNamingStrategy(),
autoLoadEntities: true,
}),
...,
],
})
export class AppModule {}
// connection.ts
import { Connection, createConnection, getConnection, getConnectionOptions } from 'typeorm';
import { loadEnv } from '../loadEnv';
loadEnv();
export const connection = {
async create(): Promise<Connection> {
const connectionOptions = await getConnectionOptions(process.env.NODE_ENV);
return await createConnection({ ...connectionOptions, name: 'default' });
},
async close() {
await getConnection().close();
},
async clear() {
const connection = getConnection();
const entities = connection.entityMetadatas;
entities.forEach(async (entity) => {
const repo = connection.getRepository(entity.name);
await repo.query(`DELETE FROM ${entity.tableName}`);
});
},
async isConnected() {
return getConnection().isConnected;
},
};
If run npm test src/test/user.service.spec.ts, will get error: No connection options were found in any orm configuration files.
The log console.log("connection.isConnected(): ", connection.isConnected()); get error:
ConnectionNotFoundError: Connection "default" was not found.
Here's the test case:
import { getConnection, Repository } from 'typeorm';
import { createMock } from '#golevelup/nestjs-testing';
import { User } from '../user/user.entity';
import { connection } from '../connection';
describe('UserService', () => {
beforeAll(async () => {
await connection.create();
});
afterAll(async () => {
await connection.clear();
await connection.close();
});
describe('beforeAll connection.create()', async () => {
const repo = createMock<Repository<User>>();
it('should create user', function() {
console.log('env: ', process.env.NODE_ENV); // env: test
console.log("connection.isConnected(): ", connection.isConnected());
const user = {
id: 'uuid',
avatar: 'avatar',
name: 'user A',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: new Date(),
};
console.log(user);
repo.create(user);
repo.find.mockResolvedValue([user]);
});
});
});
If not using connection.create() to connect, connection.isConnected() will be true.
...
beforeAll(async () => {
await createConnection({
type: "postgres",
host: "localhost",
port: 5432,
username: "root",
password: "test",
database: "test",
logging: false,
});
});
...
How could I update my setting and connect database through my test config?

Related

How to create faker data with factory by typeorm-seeding?

From the typeorm-seeding guide, I created some files below:
database/factories/post.factory.ts
database/seeds/post.seed.ts
post.factory.ts
import { define } from 'typeorm-seeding';
import { Post } from '../../src/entity';
define(Post, (faker: typeof Faker) => {
return new Post({
id: faker.random.uuid(),
title: 'test',
body: 'test',
});
});
post.seed.ts
import { Factory, Seeder } from 'typeorm-seeding';
import { Connection } from 'typeorm';
import { Post } from '../../src/entity';
export default class PostSeeder implements Seeder {
public async run(factory: Factory, _connection: Connection): Promise<any> {
await factory(Post)().createMany(10);
}
}
When run npm run seed:run, it caused an error:
❌ Could not save entity
QueryFailedError: null value in column "title" of relation "posts" violates not-null constraint
query: 'INSERT INTO "blog"."posts"("id", "title", "body", "created_at", "updated_at") VALUES (DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) RETURNING "id", "title", "created_at", "updated_at"',
parameters: [],
detail: 'Failing row contains (111111111-2a7d-4316-9a85-4d38d6e3febb, null, null, 2022-11-26 03:28:54.895454, null).',
The seed script in package.json:
{
"name": "blog",
"version": "0.0.1",
"description": "Blog",
"main": "index.js",
"scripts": {
"seed:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm-seeding/dist/cli.js seed -c seed",
...
"dependencies": {
"typeorm-seeding": "^1.6.1",
...
The seed related ormconfig.js:
module.exports = [
{
name: 'seed',
type: 'postgres',
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
schema: 'blog',
uuidExtension: 'pgcrypto',
synchronize: false,
logging: false,
entities: ['src/entity/index.ts'],
factories: ['database/factories/**/*.factory.ts'],
seeds: ['database/seeds/**/*.seed.ts'],
cli: {
entitiesDir: 'src/entity',
migrationsDir: 'database/migrations',
},
},
It seems when run seed it can't get parameters from factory. Where is wrong?

Error during migration generation: Error: Given data source file must contain export of a DataSource instance

Using latest version of TypeORM: 0.3.9 with NestJS.
While I'm trying to generate migration with following command:
npm run typeorm migration:generate -- ./migrations/Init -o -d ormconfig.js
Getting error:
Error during migration generation:
Error: Given data source file must contain export of a DataSource instance
Here is my DataSource file:
ormconfig.js
var dbConfig = {
synchronize: false,
migrations: ['migrations/*.js'],
cli: {
migrationsDir: 'migrations',
},
};
switch (process.env.NODE_ENV) {
case 'development':
Object.assign(dbConfig, {
type: 'sqlite',
database: 'db.sqlite',
entities: ['**/*.entity.js'],
});
break;
case 'test':
Object.assign(dbConfig, {
type: 'sqlite',
database: 'test.sqlite',
entities: ['**/*.entity.ts'],
migrationsRun: true,
});
break;
case 'production':
Object.assign(dbConfig, {
type: 'postgres',
url: process.env.DATABASE_URL,
migrationsRun: true,
entities: ['**/*.entity.js'],
ssl: {
rejectUnauthorized: false,
},
});
break;
default:
throw new Error('unknown environment');
}
module.exports = dbConfig;

Also TypeORM script snippet from:
package.json
"scripts": {
"typeorm": "cross-env NODE_ENV=development node --require ts-node/register ./node_modules/typeorm/cli.js"
},

Nestjs Typeorm query still using 'postgres' database

When I try to query using repository for login api, I receive this error
QueryFailedError: relation "user" does not exist
After debuging, I realize the nestjs still using database with name "postgres". eventhough I set my database name in my .env as "nestjs". (I can confirm because when I migrate and seed the database "postgres" the login function works)
DB_TYPE=postgres
DB_HOST=db
DB_USER=postgres
DB_PASS=postgres
DB_NAME=nestjs
DB_NAME_TEST=nestjs_test
DB_PORT=5432
All my migration and seed process already using database "nestjs". Below is my files to configure my database connection
configuration.ts
import { registerAs } from '#nestjs/config';
export default registerAs('database', () => ({
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
name: process.env.DB_NAME,
nameTest: process.env.DB_NAME_TEST,
port: process.env.DB_PORT,
}));
database/config.service.ts
...
get name(): string {
return this.configService.get<string>(
this.configService.get<string>('app.env') === 'test'
? 'database.nameTest'
: 'database.name',
);
}
...
database/config.migration.ts
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
export = {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database:
process.env.NODE_ENV === 'test'
? process.env.DB_NAME_TEST
: process.env.DB_NAME,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../../database/migrations/*{.ts,.js}'],
cli: {
migrationsDir: __dirname + '/../../database/migrations',
},
extra: {
charset: 'utf8mb4_unicode_ci',
},
synchronize: false,
logging: true,
keepConnectionAlive: true,
namingStrategy: new SnakeNamingStrategy(),
};
database/provider.module.ts
import { Module } from '#nestjs/common';
import { TypeOrmModule, TypeOrmModuleOptions } from '#nestjs/typeorm';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { DatabaseConfigModule } from '../../config/database/config.module';
import { DatabaseConfigService } from '../../config/database/config.service';
#Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [DatabaseConfigModule],
inject: [DatabaseConfigService],
useFactory: async (
service: DatabaseConfigService,
): Promise<TypeOrmModuleOptions> => ({
type: service.type,
host: service.host,
port: service.port,
username: service.user,
password: service.password,
name: service.name,
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/../../database/migrations/*{.ts,.js}'],
cli: {
migrationsDir: __dirname + '/../../database/migrations',
},
extra: {
charset: 'utf8mb4_unicode_ci',
},
synchronize: false,
logging: true,
keepConnectionAlive: true,
namingStrategy: new SnakeNamingStrategy(),
}),
}),
],
})
export class DatabaseProviderModule {}
And here is my databases:
If you guys need more file for information please let me know. I'm new to nestjs and typeorm, So please tell me what's wrong with my code. Thank you!
TypeormModule has the database property which provides the database name. You are using name in provider.module.ts:
Wrong name : databaseName
Right database : databaseName
TypeOrmModule.forRootAsync({
useFactory: () => ({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
});

Having an error "MissingDriverError: Wrong driver: "undefined" given." while generating migration with TypeORM

I'm trying to generate migrations. I have added these lines to package.json
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config src/config/configuration.ts",
"migration:generate":"npm run typeorm migration:generate -- -n"
I have a configuration.ts file instead of ormconfig.json which looks like this.
import { join } from 'path';
export default () => {
const host = process.env.DB_HOST;
const port = process.env.DB_PORT;
const username = process.env.DB_USERNAME;
const password = process.env.DB_PASSWORD;
const database = process.env.DB_DATABASE;
return {
database: {
type: 'mariadb',
host,
port,
username,
password,
database,
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: false,
migrations: ['/src/migration/**/*.ts'],
keepConnectionAlive: true,
charset: 'utf8mb4',
cli: {
migrationsDir: '/src/migrations',
},
},
};
};
while running the command
npm run migration:generate -- softDeleteUser
having this error
> user-service#0.0.1 migration:generate
> npm run typeorm migration:generate -- -n "softDeleteUser"
> user-service#0.0.1 typeorm
> node --require ts-node/register ./node_modules/typeorm/cli.js --config src/config/configuration.ts "migration:generate" "-n" "softDeleteUser"
Error during migration generation:
MissingDriverError: Wrong driver: "undefined" given. Supported drivers are: "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "oracle", "postgres", "sqlite", "better-sqlite3", "sqljs", "react-native", "aurora-data-api", "aurora-data-api-pg".
at new MissingDriverError (/Users/akash/Documents/Projects/pulp-backend/user-service/src/error/MissingDriverError.ts:8:9)
at DriverFactory.create (/Users/akash/Documents/Projects/pulp-backend/user-service/src/driver/DriverFactory.ts:67:23)
at new Connection (/Users/akash/Documents/Projects/pulp-backend/user-service/src/connection/Connection.ts:128:43)
at ConnectionManager.create (/Users/akash/Documents/Projects/pulp-backend/user-service/src/connection/ConnectionManager.ts:64:28)
at Object.<anonymous> (/Users/akash/Documents/Projects/pulp-backend/user-service/src/index.ts:230:35)
at step (/Users/akash/Documents/Projects/pulp-backend/user-service/node_modules/tslib/tslib.js:141:27)
at Object.next (/Users/akash/Documents/Projects/pulp-backend/user-service/node_modules/tslib/tslib.js:122:57)
at /Users/akash/Documents/Projects/pulp-backend/user-service/node_modules/tslib/tslib.js:115:75
at new Promise (<anonymous>)
at Object.__awaiter (/Users/akash/Documents/Projects/pulp-backend/user-service/node_modules/tslib/tslib.js:111:16)
Any suggestions?
Did you try to export config like :
export = ORMConfig;
I've had the same error, so I tried to do what the docs say. I imported my database config inside app.module imports like so:
`import { Module } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
#Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
but unfortunately it wasn't a cause.
The real cause was in my entity file, where I put columns in the entity table.
I had:
#Column({
type: 'float',
precision: 6,
scale: 2,
})
price: number;
instead of:
#Column({
type: 'decimal',
precision: 6,
scale: 2,
})
price: number;
because only decimals take precision and scale properties
All variants are described there: https://typeorm.io/entities#column-options
The below solution works in my case.
File app.module.ts:
imports: [
ConfigModule.forRoot({isGlobal: true}),
TypeOrmModule.forRootAsync({
useClass: DatabaseConnectionService //DatabaseConnectionService is
//the file containing the connection code
}) ,
UserModule
]

Configure TypeORM with one configuration for CLI and NestJS application

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?
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"
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)
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
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"
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.
.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
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};
app.module.ts
#Module({
imports: [
TypeOrmModule.forRoot(configService.getTypeOrmConfig()),
]
})
export class AppModule {}
Please let me know if this solution is working or not
Solution
To use both TypeORM CLI and ConfigModule/ConfigService seamlessly, I found this approach from improving Robert answer, without adding additional script like any above answer mentioned. I think this solution is more "NestJS" way and a cleaner solution as well.
Load the config as what has been Robert's answer exemplified.
Also load in your module (imports the ConfigModule and also the TypeOrmModule).
The additional step will be creating your own separate migration configuration file and merge with your database configuration like the example below.
// src/config/migration.config.ts
/**
* Load the dot env manually without the NestJS config module.
*/
config();
/**
* Defines the migration configuration using TypeORM DataSource.
*/
export default new DataSource({
...dbConfig(), // or any name you use for your database config, this is the same object that you use for `registerAs()` method
//* Additional config for your migration
migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
migrationsTableName: 'custom-name',
});
The key step here is to load your environment variable first, without running the NestJS application to get the env variables from ConfigService
And the last step is to add the CLI command to the package.json like this
// package.json
"typeorm": "typeorm-ts-node-commonjs",
"migration:create": "npm run typeorm -- migration:create",
"schema:drop": "npm run typeorm -- schema:drop -d src/config/migration.config.ts",
"migration:generate": "npm run typeorm -- migration:generate -d src/config/migration.config.ts",
"migration:run": "npm run typeorm -- migration:run -d src/config/migration.config.ts",
"migration:revert": "npm run typeorm -- migration:revert -d src/config/migration.config.ts"
And whoala, you could use both TypeORM CLI to run the migration and load the TypeORM configuration inside the NestJS application with a single database configuration file (no configuration file duplication) which loads the same environment variables.
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
My solution:
#1 src/config/database.ts
database configuration
import { ConnectionOptions } from 'typeorm';
const config: ConnectionOptions = {
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
...
};
export default config;
#2 src/config/index.ts
complete configuration for application
import * as dotenv from 'dotenv';
export async function getConfig() {
dotenv.config();
return {
database: (await import('./database')).default,
/* ...config sections for other modules */
};
}
#3 ormconfig.ts
...just a proxy for src/config/database.ts
import config from './src/config/database';
export = config;
#4 src/app.module.ts
import { getConfig } from './config/index';
...
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
ignoreEnvFile: true,
load: [getConfig],
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return configService.get<ConnectionOptions>('database');
},
}),
],
controllers: [...],
providers: [...],
})
#5 fix build
Add ormconfig.ts to excluded in tsconfig.build.json.

Resources