DEV Community

Rubén Aguilera Díaz-Heredero
Rubén Aguilera Díaz-Heredero

Posted on • Updated on

Integrar TypeORM migrations en NestJS

Introducción

Migrations de TypeORM es la tecnología que nos permite llevar un control de los cambios en el modelo de base de datos con el fin de que sean automáticamente aplicados sobre la base de datos real cuándo subamos de versión la aplicación.

En el mundo Java la tecnología de migrations se puede asemejar a Flyway o Liquibase.

Estas tecnologías nos facilitan la obtención de las sentencias SQL necesarias para aplicar los cambios a una base de datos. Siempre guardan una tabla especial que registra los cambios que ya se han aplicado, de forma que estos cambios se aplican de forma incremental; es decir si la base de datos está vacía se aplican todos por orden, si ya tiene algún cambio aplicado, solo se van a aplicar los que falten en orden cronológico.

Vamos al lío

Nota: para seguir este tutorial se recomienda haber visto este anterior donde hacemos uso de TypeORM para hacer un API Rest con PostgreSQL.

Lo primero que queremos es generar de forma automática el modelo de nuestras entidades para obtener los ficheros de migrations que nos permitirán hacer los cambios en base de datos al arrancar la aplicación.

Para eso TypeORM nos ofrece un CLI con distintas operaciones que configuraremos más adelante como scripts dentro del fichero package.json.

Si has seguido el tutorial anterior se habrás dado cuenta de que hemos configurado el acceso a base de datos en el fichero app.module.ts de esta forma:

import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    ConfigModule.forRoot(), 
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT),
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
      autoLoadEntities: true,
      synchronize: !!process.env.DB_SYNC,
      keepConnectionAlive: true
    }),
    UsersModule
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

La primera piedra en el camino es NestJS delega completamente el manejo de migrations a TypeORM, es decir, no tiene una integración directa y por tanto esta configuración no puede ser leída por los scripts de migrations de TypeORM que veremos después.

En todos los tutoriales que he visto te recomiendan que saques esta configuración a un fichero ormconfig.json, que hace complicado poder seguir utilizando las variables de entorno para la definición de la conexión a base de datos.

Por tanto, lo mejor es crearse un fichero src/ormconfig.ts que si nos va a permitir hacer uso de las variables de entorno como ves a continuación:

import { ConnectionOptions } from 'typeorm';

const config: ConnectionOptions = {
    type: 'postgres',
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT),
    username: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
    entities: [__dirname + '/**/*.entity{.ts,.js}'],
    synchronize: false,
    migrationsRun: true,
    logging: true,
    migrations: [__dirname + '/../migrations/**/*{.ts,.js}'],
    cli: {
        migrationsDir: './migrations',
    }
}

export = config;
Enter fullscreen mode Exit fullscreen mode

En este fichero tenemos definidas las mismas propiedades, más todas las relativas a migrations, pero hemos perdido las que son propias de NestJS como el autoLoadEntities o el keepConnectionAlive que veremos cómo recuperarlas más adelante.

Detalles de esta configuración, con la propiedad "cli" le estamos indicando donde queremos que se generen los archivos de migrations y con la propiedad migrations de donde tiene que leerlos, además con la propiedad migrationsRun le estamos indicando que queremos aplicar todas los ficheros de migrations en el arranque de la aplicación.

Ahora aplicamos esta configuración a nuestro módulo de TypeOrm en el fichero app.module.ts, de forma que lo vamos a hacer añadiendo las propiedades que decíamos eran propias de NestJS, quedando de esta forma:

import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import * as ormconfig from './ormconfig';

@Module({
  imports: [
    ConfigModule.forRoot(), 
    TypeOrmModule.forRoot({...ormconfig, 
                           keepConnectionAlive: true, 
                           autoLoadEntities: true}),
    UsersModule
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Hecho esto es el momento de añadir los siguientes scripts al fichero package.json:

...
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config ./src/ormconfig.ts",
"typeorm:migrate": "npm run typeorm migration:generate -- -n",
"typeorm:run": "npm run typeorm migration:run"
...
Enter fullscreen mode Exit fullscreen mode

De esta forma para generar un fichero de migración con el estado actual del modelo de entidades, tenemos que primero asegurarnos de que la base de datos está levantada y es accesible por la aplicación y después ejecutar:

$> npm run typeorm:migrate nombre-migracion
Enter fullscreen mode Exit fullscreen mode

Si este comando acaba con éxito, se habrá creado la carpeta "migrations" en la raíz de nuestro proyecto y dentro se habrá creado un fichero con el patrón TIMESTAMP-nombre-migracion.ts que contiene dos métodos públicos: uno llamado "up" con las sentencias SQL necesarias para crear la base de datos de cero; y otro llamado "down" con las sentencias SQL necesarias para revertir los cambios de base de datos.

Este comando lo volveremos a ejecutar siempre que haya un cambio en el modelo de entidades de nuestra aplicación y se guardarán en el orden del TIMESTAMP.

Ahora cuando arranquemos la aplicación debemos ver que antes de arrancar se han ejecutado las sentencias SQL necesarias para crear las tablas y los datos que tengamos en los ficheros migrations de TypeORM.

Discussion (6)

Collapse
miguelchico profile image
Miguel Angel Chico

Hola!
Después de generar los ficheros de migración sin problemas, arranco la app y tengo este error:

import { MigrationInterface, QueryRunner } from 'typeorm';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Module._compile (internal/modules/cjs/loader.js:895:18)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
Enter fullscreen mode Exit fullscreen mode

Alguna idea?

Collapse
jventor profile image
Jaime Ventor
...
  migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
  cli: {
    migrationsDir: './src/migrations',
  },
...
Enter fullscreen mode Exit fullscreen mode

migrations dentro de "src"

Collapse
davidijsud profile image
David

Lo mismo me pasa :(

Collapse
neumartin profile image
neumartin

Hola!
Cuando hago el spread TypeOrmModule.forRoot({...ormconfig el objeto ormconfig no tiene los datos del .env, esta todo en undefined, sin embargo si pongo un breakpoint en esa linea, examinando el process.env si veo las variables de entorno.
¿Puede ser que haya que iniciar el dotenv en el archivo ormconfig.ts?
Gracias!!

Collapse
leandro070 profile image
Leandro Gutierrez

Hola, puede que el import del config se este ejecutando antes de dotenv. Intenta usar require() en ves de import y después del dotenv. Sino, maybe, podes hacer que ejecutes una función que te devuelve esa config en ves de directamente exportar la config

Collapse
neumartin profile image
neumartin

Bueno, lo resolví poniendo:

import {ConfigModule} from "@nestjs/config";
ConfigModule.forRoot();

Antes de:

const config: ConnectionOptions = {