0

I've been trying to get my TypeOrm Config to work nicely with Migrations and .env without needing to specify it in two places.

The struggle is this - My migration script needs to read an immediately returned object -

{
  type: 'postgres',
  host: '127.0.0.1',
  port: 5432,
  username: 'postgres',
  password: 'myPassword',
  database: 'postgres',
  entities: [ 'dist/**/*.entity.js' ],
  logging: [ 'query', 'error', 'schema' ],
  synchronize: false,
  migrations: [ 'dist/app/database/migrations/*.js' ],
  cli: { migrationsDir: 'src/app/database/migrations' },
  namingStrategy: SnakeNamingStrategy {
    nestedSetColumnNames: { left: 'nsleft', right: 'nsright' },
    materializedPathColumnName: 'mpath'
  },
  subscribers: [],
  migrationsRun: false,
  dropSchema: false
}

But when I use NestJs's proposed configuration while also utilizing a .env (DOTENV) file, the solution looks like this:

import {TypeOrmModuleOptions, TypeOrmOptionsFactory} from "@nestjs/typeorm";
import {SnakeNamingStrategy} from "typeorm-naming-strategies";

export class DatabaseConfiguration implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions | Promise<TypeOrmModuleOptions> {
    return {
      type: "postgres",
      host: process.env.POSTGRES_HOST,
      port: parseInt(process.env.POSTGRES_PORT, 10) || 5432,
      username: process.env.POSTGRES_USERNAME,
      password: process.env.POSTGRES_PASSWORD,
      database: process.env.POSTGRES_DATABASE,

      entities: [process.env.TYPEORM_ENTITIES],
      logging: true,
      synchronize: false,
      migrations: [process.env.TYPEORM_MIGRATIONS],
      cli: {
        migrationsDir: process.env.TYPEORM_MIGRATIONS_DIR,
      },
      namingStrategy: new SnakeNamingStrategy(),
    };
  }
}

I've told TypeOrm where to find the config file (very important) in my package.json like so (see --config tag):

"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config dist/app/config/database.configuration.js"

Which means my migration would need to call (new DatabaseConfiguration()).createTypeOrmOptions() to get the object. Otherwise it would be hidden inside the DatabaseConfiguration class, inside the createTypeOrmOptions function.

This created the TypeOrm error: MissingDriverError: Wrong driver: "undefined" given. Supported drivers are: "aurora-data-api", "aurora-data-api-pg", "better-sqlite3", "capacitor", "cockroachdb", "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "nativescript", "oracle", "postgres", "react-native", "sap", "sqlite", "sqljs"

Because as you can see in the node_modules/typeorm/driver/DriverFactory.js file, it's looking for the connection to come through as an object connection.options where it can pick up the type to create the correct driver)-

DriverFactory.prototype.create = function (connection) {
        console.log(connection.options);
        var type = connection.options.type;
        switch (type) {
            case "mysql":
                return new MysqlDriver_1.MysqlDriver(connection);
            case "postgres":
                return new PostgresDriver_1.PostgresDriver(connection);
        ...
       }
    }
};

My type isn't getting defined.

So, for the people who don't want to spend hours debugging this, please see my detailed setup below... and upvote my question and answer if it helped, because I couldn't find a comprehensive answer anywhere.

Jay
  • 566
  • 6
  • 18

2 Answers2

7

Here's my set up start to finish.

First lets address a basic set up for just running the DB with NestJs, dotenv, and config files. Obviously we've moved away from ormconfig.json which doesn't support .env.

app.module.ts

imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      useClass: DatabaseConfiguration,
    }),
    ...
]

database.configuration.ts

import {TypeOrmModuleOptions, TypeOrmOptionsFactory} from "@nestjs/typeorm";
import {SnakeNamingStrategy} from "typeorm-naming-strategies";

export class DatabaseConfiguration implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions | Promise<TypeOrmModuleOptions> {
    return {
      type: "postgres",
      host: process.env.POSTGRES_HOST,
      port: parseInt(process.env.POSTGRES_PORT, 10) || 5432,
      username: process.env.POSTGRES_USERNAME,
      password: process.env.POSTGRES_PASSWORD,
      database: process.env.POSTGRES_DATABASE,

      entities: [process.env.TYPEORM_ENTITIES],
      logging: true,
      synchronize: false,
      migrations: [process.env.TYPEORM_MIGRATIONS],
      cli: {
        migrationsDir: process.env.TYPEORM_MIGRATIONS_DIR,
      },
      namingStrategy: new SnakeNamingStrategy(),
    };
  }
}

.env

POSTGRES_TYPE="postgres"
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_USERNAME="postgres"
POSTGRES_PASSWORD="takeMyPassword123"
POSTGRES_DATABASE=postgres

TYPEORM_ENTITIES="dist/**/*.entity.js"
TYPEORM_MIGRATIONS="dist/database/migrations/*.js"
TYPEORM_MIGRATIONS_DIR="src/database/migrations"

With that, your app should connect to a postgres DB and run TypeOrm queries. But what if you want to support migrations?

We need to add a small additional layer on top of the database.configuration.ts file to extract the object that needs to get returned for migrations.

So here's a new config file:

migration.configuration.ts

import {DatabaseConfiguration} from "./database.configuration";

export default (new DatabaseConfiguration()).createTypeOrmOptions()

Then all that's left to do, is update our package.json to look to the migration.configuration.ts file which is calling our databaseConfig file and returning the rendered object.

package.json

"scripts": {
    ...
    "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config dist/app/config/migration.configuration.js",
    "make:migration-initial": "npm run build && npm run typeorm migration:generate -- -n initial",
    "migrate": "npm run build && npm run typeorm migration:run",
    "migrate:rollback": "npm run build && npm run typeorm migration:revert",
    "migrate:destroy": "npm run build && npm run typeorm migration:revert && rimraf src/app/database/migrations"
    ...
  },

I left in some of my other scripts in there... but it's the typeorm one that matters, again see --config which defines the location where the TypeOrm config object will be returned.

Maulana Adam Sahid
  • 375
  • 2
  • 4
  • 12
Jay
  • 566
  • 6
  • 18
  • 1
    Thank you so much! I too am new to this and this got my migrations working. Ran into an issue with docker but by adding overrides to the migrations config script, I was able to resolve it. Docker uses a different host and port than the app due to migrations running locally – Jeremy Apr 26 '22 at 23:27
  • 3
    I followed these directions and it worked perfectly! Until I tried to run migrations and I get "Missing required argument: dataSource". How do I resolve this? – whitaay Aug 21 '22 at 21:34
  • Thanks! I have been struggling for about 2 hours on this error. But what is the actual way of solving this issue. By the actual solution, I mean TypeORM's and NestJS's recommended solution. – nicklee Mar 21 '23 at 15:10
  • Frameworks can't build such specialized solutions and have them as "recommended solutions". The shear weight of responsibility that would create in maintenance would be exhausting to keep up to date. This is a viable solution to this issue. – Jay Apr 20 '23 at 19:25
1

Solution presented by @Jay worked for me, except migrations. I have managed to fix it by slightly modifying the migration.configuration.ts as follows:

import { DatabaseConfiguration } from './database.configuration';
import { DataSource, DataSourceOptions } from 'typeorm';
import { NestFactory } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';

export default NestFactory.create(
  ConfigModule.forRoot(),
).then(() => {
  const dataSourceOptions =
    new DatabaseConfiguration().createTypeOrmOptions() as DataSourceOptions;
  return new DataSource(dataSourceOptions);
});

The trick was to instantiate ConfigModule in order to read .env file present in the project's root directory.
In order to utilize different *.env file, one might want to pass it as an argument to forRoot() method (as described in documentation), e.g.:

ConfigModule.forRoot({
  envFilePath: '.development.env',
});
the0ffh
  • 407
  • 1
  • 4
  • 14