Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save programmerShinobi/5b6b0c5131c8b58b594d9f3557aa5218 to your computer and use it in GitHub Desktop.
Save programmerShinobi/5b6b0c5131c8b58b594d9f3557aa5218 to your computer and use it in GitHub Desktop.
TypeORM integration with migration in NestJS using Docker (NestJS Series 03)

How to Integrate TypeORM with migrations in NestJS using Docker | Ubuntu 22.04 (Jammy)

  1. Get the latest version of the PostgreSQL image.

    $ sudo docker pull postgres
  2. Run a PostgreSQL container with the name my-postgres, and run the container in the background.

    $ sudo docker run --name my-postgres \
      -e POSTGRES_USER=postgres \
      -e POSTGRES_PASSWORD=postgres \
      -e POSTGRES_DB=nest-series \
      -p 5432:5432 \
      -d postgres
  3. Checking into the Docker container named my-postgres and running the PostgreSQL CLI (psql) as the postgres user.

    $ sudo docker exec -it my-postgres psql -U postgres

    Output:

    psql (16.4 (Debian 16.4-1.pgdg120+1))
    Type "help" for help.
    
    Exit to continue installation
    postgres=# exit
    
  4. Install the public key for the repository (if not done previously).

    $ curl -fsS https://www.pgadmin.org/static/packages_pgadmin_org.pub \
      | sudo gpg --dearmor -o /usr/share/keyrings/packages-pgadmin-org.gpg
  5. Create the repository configuration file.

    sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" \
    > /etc/apt/sources.list.d/pgadmin4.list && \
    apt update'
  6. Edit the pgAdmin Repository File Use a text editor, such as nano, to edit the pgAdmin repository file:

    $ sudo nano /etc/apt/sources.list.d/pgadmin4.list

    Change 'horus' to 'jammy ' Change the entry from horus to jammy, so the line becomes:

    deb [signed-by=/usr/share/keyrings/packages-pgadmin-org.gpg] https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/jammy pgadmin4 main
    
  7. Install for both desktop and web modes.

    $ sudo apt update
    $ sudo apt install pgadmin4
  8. Install for web mode only.

    $ sudo apt install pgadmin4-web 
  9. Configure the webserver, if you installed pgadmin4-web.

    $ sudo /usr/pgadmin4/bin/setup-web.sh
  10. Run the code below to install TypeORM with the Postgres pg driver.

    $ yarn add @nestjs/typeorm typeorm pg
  11. Just replace above with your credentials and place it in .env. Now we move to our config factory class.

    ...
    # Database Configuration
    DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nest-series
  12. Config class will use details ormconfig.ts file, So ormconfig.ts

    // ormconfig.ts
    
    import * as dotenv from 'dotenv';
    import { DataSource } from 'typeorm';
    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;
  13. Implement the TypeOrmOptionsFactory interface to follow a particular contract of the factory configuration class.

    // 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;
      }
    }
  14. Use this factory in the Application Module file where we registered our TypeORM Module.

    // src/modules/app/app.module.ts
    
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { configLoads } from '../config/index.config';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { TypeOrmConfigFactory } from '../database/typeorm.factory';
    
    const modules = [];
    
    const typeOrmConfig = new TypeOrmConfigFactory().createTypeOrmOptions();
    
    export const global_modules = [
      ConfigModule.forRoot({
        load: configLoads,
        isGlobal: true,
        envFilePath: ['.env'],
      }),
      TypeOrmModule.forRoot(typeOrmConfig),
    ];
    
    @Module({
      imports: [...global_modules, ...modules],
      controllers: [],
      providers: [],
    })
    export class AppModule {}
  15. It requires feeding entities starting from the user entity, but the user also needs a base entity that will automatically handle timestamps and IDs for us.

    // src/entities/base.entity.ts
    
    import { ApiProperty } from '@nestjs/swagger';
    import {
      BaseEntity as _BaseEntity,
      CreateDateColumn,
      DeleteDateColumn,
      PrimaryGeneratedColumn,
      UpdateDateColumn,
    } from 'typeorm';
    
    export abstract class BaseEntity extends _BaseEntity {
      @ApiProperty()
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      @ApiProperty()
      @CreateDateColumn()
      created_at: Date;
    
      @ApiProperty()
      @UpdateDateColumn()
      updated_at: Date;
    
      @DeleteDateColumn()
      deleted_at: Date;
    }
  16. Add UserTokenTypeEnum for user token type.

    // src/enums/user-token-type.enum.ts
    
    export enum UserTokenTypeEnum {
      REGISTER_VERIFY = 'REGISTER_VERIFY',
      RESET_PASSWORD = 'RESET_PASSWORD',
    }
  17. Add User Token Entity.

    // src/entities/user-token.entity.ts
    
    import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
    import { BeforeInsert, Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
    import { UserEntity as User } from './user.entity';
    import { BaseEntity } from './base.entity';
    import { UserTokenTypeEnum } from 'src/enums/user-token-type.enum';
    
    @Entity({ name: 'user_tokens' })
    export class UserTokenEntity extends BaseEntity {
      @Column({ type: 'varchar', length: 100 })
      token: string;
    
      @Column({ type: 'boolean', default: false })
      is_used: boolean;
    
      @Column({ type: 'enum', enum: UserTokenTypeEnum })
      type: UserTokenTypeEnum;
    
      @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()}`;
      }
    }
  18. One small thing you might have noticed that I have created an abstract class, the reason behind that is we don't want any tables associated with Base entities or any other tracking from TypeORM. Now we move on to user entities.

    // src/entities/user.entity.ts
    
    import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
    import { Column, Entity } from 'typeorm';
    import { BaseEntity } from './base.entity';
    
    @Entity({ name: 'users' })
    export class UserEntity extends BaseEntity {
      @ApiProperty({ example: 'Faqih' })
      @Column({ type: 'varchar', length: 50 })
      first_name: string;
    
      @ApiProperty({ example: 'Pratama Muhti' })
      @Column({ type: 'varchar', length: 50, nullable: true })
      last_name: string;
    
      @ApiProperty({ example: '[email protected]' })
      @Column({ type: 'varchar', length: 255, unique: true })
      email: string;
    
      @ApiProperty({ example: 'StrongP@ssword123' })
      @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: true })
      is_active: boolean;
    
      @Column({ type: 'varchar', length: 100, nullable: true })
      tokens: string;
    }
  19. To do that we will add a script 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"
      },
      ...
    }
  20. Now can generate migration file And then to migrate. If we think that migration was wrong Or wanted to edit just revert the migration in DB by yarn run migration:revert.

    $ yarn run migration:generate
    $ yarn run migration:run
  21. Runs the script defined in the package.json file in projects that use Yarn as a package manager.

    $ yarn run start:dev
  22. Over time, we organized our structure, so the current structure looks pretty good and reassuring in terms of clean code.

    src
    ├── entities
    │   ├── base.entity.ts
    │   ├── user.entity.ts
    │   └── user_token.entity.ts
    ├── enums
    │   └── user-token-type.enum.ts
    ├── main.ts
    └── modules
        ├── app
        │   └── app.module.ts
        ├── auth
        ├── config
        │   ├── app.config.ts
        │   ├── database.config.ts
        │   └── index.ts
        ├── database
        │   ├── migrations
        │   │   └── 1727090604947-migrations.ts
        │   └── typeorm.factory.ts
        └── user
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment