DEV Community

depak379mandal
depak379mandal

Posted on

TypeORM integration with migrations in NestJS

Love to work with you, You can hire me on Upwork.

To even think about TypeORM we must install all the required libraries in the application, already included command in getting started article. We can run below code to install TypeORM with pg Postgres driver.

npm install --save @nestjs/typeorm typeorm pg
Enter fullscreen mode Exit fullscreen mode

We can define a factory class/function that will return all the required details/config for our TypeORM module. It will be helpful to segregate modules in terms of their functionality.

Even before everything we need a database, I have created nest-series database in Postgres using DBeaver. You can use PGAdmin or any other tool just connect to your local DB Or you can use remote as per your interest. After that You have to create a connection URL, that URL will look like below.

DATABASE_URL=postgresql://<username>:<password>@localhost:5432/nest-series
Enter fullscreen mode Exit fullscreen mode

Just replace above with your credentials and place it in .env. Now we move to our config factory class. Config class will use details ormconfig.ts file, So ormconfig.ts can be used in npm command as data source.

// ormconfig.ts

import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions';

dotenv.config();

export const configs: PostgresConnectionOptions = {
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [__dirname + '/src/**/*.entity.{ts,js}'],
  migrations: [__dirname + '/src/modules/database/migrations/*{.ts,.js}'],
  dropSchema: false,
  logging: false,
};
const dataSource = new DataSource(configs);

export default dataSource;
Enter fullscreen mode Exit fullscreen mode
// src/modules/database/typeorm.factory.ts

import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import ORMConfig from '../../../ormconfig';

@Injectable()
export class TypeORMConfigFactory implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return ORMConfig.options;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use Injectable class to use configService as I have mentioned in earlier article that we will be using config service as an injected service. In constructor, we can inject configService as ConfigService. We have implemented TypeOrmOptionsFactory the interface to follow the particular contract of factory config class. Now we can use this factory in App Module file where we are registering our TypeORM Module.

// src/modules/app/app.module.ts

...
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeORMConfigFactory } from '../database/typeorm.factory';

const modules = [];

export const global_modules = [
  ...
  TypeOrmModule.forRoot({
    useClass: TypeORMConfigFactory,
  }),
];

@Module({
  imports: [...global_modules, ...modules],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

After this, you can run npm run start:dev to confirm if anything is going wrong in setup for TypeORM. As we move forward, we need entities as per our requirement we have defined in the first article. We require entities, we will start from user entity But user also require base entity that will automatically handle the timestamps and ID for us.

// src/entities/base.ts

import { ApiProperty } from '@nestjs/swagger';
import {
  BaseEntity as _BaseEntity,
  CreateDateColumn,
  DeleteDateColumn,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

export abstract class BaseEntity extends _BaseEntity {
  // we are using uuid instead of integers
  @ApiProperty()
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ApiProperty()
  @CreateDateColumn()
  created_at: Date;

  @ApiProperty()
  @UpdateDateColumn()
  updated_at: Date;

  @DeleteDateColumn()
  deleted_at: Date;
}
Enter fullscreen mode Exit fullscreen mode

I have introduced swagger decorator above, You can see ApiProperty decorator that handles Swagger doc generation. deleted_at will not be exposed in swagger doc, So we are not adding decorator above for it. One little thing you might have noticed that I have created an abstract class, reason behind that is we don’t want any table related to Base entity or any other tracking from TypeORM. Now we move to user entity and user token entity.

// src/entities/user.entity.ts

import { Column, Entity } from 'typeorm';
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from './base';

@Entity({ name: 'users' })
export class User extends BaseEntity {
  @ApiProperty({ example: 'Danimai' })
  @Column({ type: 'varchar', length: 50 })
  first_name: string;

  @ApiProperty({ example: 'Mandal' })
  @Column({ type: 'varchar', length: 50, nullable: true })
  last_name: string;

  @ApiProperty({ example: 'example@danimai.com' })
  @Column({ type: 'varchar', length: 255, unique: true })
  email: string;

  @ApiProperty({ example: 'Password@123' })
  @Column({ type: 'varchar', length: 255, nullable: true })
  password: string;

  @ApiHideProperty()
  @Column({ type: 'timestamp with time zone', nullable: true })
  email_verified_at: Date;

  @ApiHideProperty()
  @Column({ type: 'boolean', default: false })
  is_active: boolean;
}
Enter fullscreen mode Exit fullscreen mode
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { BeforeInsert, Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { User } from './user.entity';
import { BaseEntity } from './base';

export enum TokenType {
  REGISTER_VERIFY = 'REGISTER_VERIFY',
  RESET_PASSWORD = 'RESET_PASSWORD',
}

@Entity({ name: 'user_tokens' })
export class Token extends BaseEntity {
  @Column({ type: 'varchar', length: 100 })
  token: string;

  @Column({ type: 'boolean', default: false })
  is_used: boolean;

  @Column({ type: 'enum', enum: TokenType })
  type: TokenType;

  @Column({ type: 'timestamp' })
  expires_at: Date;

  @Column({ type: 'uuid' })
  user_id: string;

  @ManyToOne(() => User, (user) => user.tokens)
  @JoinColumn({ name: 'user_id' })
  user: User;

  // This decorator allows to run before insert command
  // setting up token automatically before insert
  @BeforeInsert()
  async generateToken() {
    // generate long token for registration and forgot password
    this.token = `${randomStringGenerator()}-${randomStringGenerator()}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

We also need something we called migration files to be generated for these entities So everyone can sync their DB. to do that we are going to add scripts for migration in package.json

{
  ...
  "scripts": {
    ...
    "migration:generate": "typeorm-ts-node-commonjs migration:generate src/modules/database/migrations/migrations -d ormconfig.ts",
    "migration:run": "typeorm-ts-node-commonjs -d ormconfig.ts migration:run",
    "migration:revert": "typeorm-ts-node-commonjs -d ormconfig.ts migration:revert"
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

After adding above command in package.json file we now can generate migration file using npm run migration:generate command and then to migrate them npm run migration:run, if we think that migration was wrong Or wanted to edit just revert the migration in DB by npm run migration:revert. So if you wanted to follow the migration, you can just follow me. Run below command.

npm run migration:generate
npm run migration:run
Enter fullscreen mode Exit fullscreen mode

After running generate command, we will get a migration file in src/modules/database/migrations folder, mine is.

// src/modules/database/migrations/1712332008837-migrations.ts

import { MigrationInterface, QueryRunner } from 'typeorm';

export class Migrations1712332008837 implements MigrationInterface {
  name = 'Migrations1712332008837';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TYPE "public"."user_tokens_type_enum" AS ENUM('REGISTER_VERIFY', 'RESET_PASSWORD')`,
    );
    await queryRunner.query(
      `CREATE TABLE "user_tokens" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "token" character varying(100) NOT NULL, "is_used" boolean NOT NULL DEFAULT false, "type" "public"."user_tokens_type_enum" NOT NULL, "expires_at" TIMESTAMP NOT NULL, "user_id" uuid NOT NULL, CONSTRAINT "PK_63764db9d9aaa4af33e07b2f4bf" PRIMARY KEY ("id"))`,
    );
    await queryRunner.query(
      `CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP, "first_name" character varying(50) NOT NULL, "last_name" character varying(50), "email" character varying(255) NOT NULL, "password" character varying(255), "email_verified_at" TIMESTAMP WITH TIME ZONE, "is_active" boolean NOT NULL DEFAULT false, CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"), CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
    );
    await queryRunner.query(
      `ALTER TABLE "user_tokens" ADD CONSTRAINT "FK_9e144a67be49e5bba91195ef5de" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "user_tokens" DROP CONSTRAINT "FK_9e144a67be49e5bba91195ef5de"`,
    );
    await queryRunner.query(`DROP TABLE "users"`);
    await queryRunner.query(`DROP TABLE "user_tokens"`);
    await queryRunner.query(`DROP TYPE "public"."user_tokens_type_enum"`);
  }
}
Enter fullscreen mode Exit fullscreen mode

And after npm run migration:run it should fill up your DB like mine

Image description

As we are moving forward, we are arranging our structure, So the current structure looks pretty good and convincing in terms of clean code.

src
├── entities
│   ├── base.ts
│   ├── user.entity.ts
│   └── user_token.entity.ts
├── main.ts
└── modules
    ├── app
    │   └── app.module.ts
    ├── auth
    ├── config
    │   ├── app.config.ts
    │   ├── database.config.ts
    │   └── index.ts
    ├── database
    │   ├── migrations
    │   │   └── 1712332008837-migrations.ts
    │   └── typeorm.factory.ts
    └── user
Enter fullscreen mode Exit fullscreen mode

If you run the npm run start:dev You will see below If having any doubts or issues, please mention them in comments.

[9:23:31 PM] Starting compilation in watch mode...

[9:23:33 PM] Found 0 errors. Watching for file changes.

[Nest] 26803  - 04/05/2024, 9:23:34 PM     LOG [NestFactory] Starting Nest application...
[Nest] 26803  - 04/05/2024, 9:23:34 PM     LOG [InstanceLoader] AppModule dependencies initialized +11ms
[Nest] 26803  - 04/05/2024, 9:23:34 PM     LOG [InstanceLoader] TypeOrmModule dependencies initialized +1ms
[Nest] 26803  - 04/05/2024, 9:23:34 PM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +0ms
[Nest] 26803  - 04/05/2024, 9:23:34 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +9ms
[Nest] 26803  - 04/05/2024, 9:23:35 PM     LOG [InstanceLoader] TypeOrmCoreModule dependencies initialized +183ms
[Nest] 26803  - 04/05/2024, 9:23:35 PM     LOG [NestApplication] Nest application successfully started +6ms
Enter fullscreen mode Exit fullscreen mode

In the next article, we will be having our controllers and validation in place. So we can attach our Service in next series of articles.

Top comments (0)