Use TypeOrm Custom Repository in NestJS

 0 total views

NestJS has out of box integration with TypeORM, a TypeScript and JavaScript ORM that has good support of PostgreSQL. The library offers decorators to model data types and entities and tools to generate migration scripts to update database schema; therefore, it is a popular choice to perform CRUD operations.

TypeORM offers custom repository that allows developer to extend a repository and add custom methods. It is a powerful feature because developers can inject custom repository in a service and call custom methods as if they are built-in. Moreover, it is easier to mock custom methods than builder methods such as leftJoinAndSelect() and getMany() in unit test.

Our company uses nestJS at work and we recently adopted custom repository to encapsulate TypeORM queries in our custom repositories. I am going to illustrate how the team did it for the rest of the post.

Step 1: Create docker-compose.yml to pull PostgreSQL image to docker

docker-compose.yml

version: '3.7'

services:
  postgres:
    container_name: postgres_container2
    image: postgres:12.7-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PGDATA: /data/postgres
    volumes:
      - postgres:/data/postgres
    ports:
      - '5433:5432'
    restart: always
    networks:
      - nest-poc

  pgadmin:
    container_name: pgadmin_container2
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
      PGADMIN_DEFAULT_PASSWORD: pgadmin
    volumes:
      - pgadmin:/root/.pgadmin
    ports:
      - '5556:80'
    links:
      - 'postgres:pgsql-server'
    restart: always
    networks:
      - nest-poc

volumes:
  postgres:
  pgadmin:

networks:
  nest-poc:
    name: nest-poc

Step 2: Install TypeORM and PostgreSQL dependencies

npm install --save @nestjs/typeorm typeorm pg

ormconfig.json

{
    "type": "postgres",
    "host": "localhost",
    "port": 5433,
    "username": "postgres",
    "password": "postgres",
    "entities": ["dist/**/*.entity{.ts,.js}"],
    "migrations": ["dist/migrations/**/*.js"],
    "synchronize": false,
    "cli": {
      "migrationsDir": "src/migrations"
    }
}

Step 3: Create entities, custom repositories and repository module

Depends on your preference, our company creates a repository module that defines all the entities and custom repositories used in the project. Then, we export them from the module such that they are accessible to other modules.

user.entity.ts

import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
  VersionColumn,
} from 'typeorm'
import { Report } from './report.entity'

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ type: 'text' })
  name: string

  @Column({ type: 'text' })
  lastname: string

  @Column({ type: 'integer' })
  age: number

  @CreateDateColumn({ type: 'timestamp with time zone' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamp with time zone' })
  updatedAt: Date

  @VersionColumn({ type: 'integer', default: 1 })
  version: number

  @OneToMany(() => Report, (report) => report.owner)
  reports: Report[]
}

user.repository.ts

import { NewUser } from './type'
import { User } from '@/entities'
import { EntityRepository, Repository } from 'typeorm'

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async getUsers(): Promise<User[]> {
    return this.find({
      order: {
        name: 'ASC',
        lastname: 'ASC',
      },
    })
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.findOne(id)
  }

  async createUser(newUser: NewUser): Promise<User> {
    return this.save(this.create(newUser))
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
  }
}
repositories.module.ts

import { ReportRepository } from './report.repository'
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User, Report } from '@/entities'
import { UserRepository } from './user.repository'

@Module({
  imports: [TypeOrmModule.forFeature([User, Report, UserRepository, ReportRepository])],
  exports: [TypeOrmModule],
})
export class RepositoryModule {}

Step 4: Create new modules to use repository module

We create user and report modules and import RepositoryModule in UserModule and ReportModule respectively.

user.module.ts

import { RepositoryModule } from '@/repositories'
import { Module } from '@nestjs/common'
import { UserService } from './services'
import { UserController } from './user.controller'

@Module({
  imports: [RepositoryModule],
  providers: [UserService],
  controllers: [UserController],
})
export class UserModule {}

We create user controller and user service to perform user actions. Similarly, there are report controller and report service to handle report actions

user.service.ts

import { User } from '@/entities'
import { UserRepository } from '@/repositories'
import { Injectable } from '@nestjs/common'
import { CreateUserDto } from '../dtos'

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async getUsers(): Promise<User[]> {
    return this.userRepository.getUsers()
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.userRepository.getUser(id)
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.userRepository.getUserWithReports(id)
  }

  async createUser(dto: CreateUserDto): Promise<User> {
    return this.userRepository.createUser(dto)
  }
}

user.controller.ts

import { UserService } from './services'
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { User } from '@/entities'
import { CreateUserDto } from './dtos'

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  async getUsers(): Promise<User[]> {
    return this.userService.getUsers()
  }

  @Get(':id/report')
  async getUserWithReports(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUserWithReports(id)
  }

  @Get(':id')
  async getUser(@Param('id') id: string): Promise<User | undefined> {
    return this.userService.getUser(id)
  }

  @Post()
  async createUser(@Body() dto: CreateUserDto): Promise<User> {
    return this.userService.createUser(dto)
  }
}

We use Postman/CURL to call endpoints to ensure the logic works and data is persisted in database

curl "http://localhost:3000/user"
curl "http://localhost:3000/user/1/report"

Step 5: Unit test service and controller

Earlier in the post, I claimed that mocking custom repository is easier than mocking builder methods of TypeORM. Let’s write some test cases to test user service and user controller to show it.

user.service.spec.ts

describe('UserService', () => {
  let service: UserService
  let spyUserRepository: UserRepository

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    service = module.get<UserService>(UserService)
    spyUserRepository = module.get<UserRepository>(UserRepository)
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]
      spyUserRepository.getUsers = jest.fn(() => Promise.resolve(users))
      const result = await service.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      spyUserRepository.createUser = jest.fn(() => Promise.resolve(newUser))
      const result = await service.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})
user.controller.spec.ts

describe('UserController', () => {
  let controller: UserController
  let userService: UserService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {},
        },
      ],
    }).compile()

    controller = module.get<UserController>(UserController)
    userService = module.get<UserService>(UserService)
  })

  it('should be defined', () => {
    expect(controller).toBeDefined()
  })

  describe('getUsers', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]

      jest.spyOn(userService, 'getUsers').mockImplementation(() => Promise.resolve(users))
      const result = await controller.getUsers()
      expect(result).toEqual(users)
    })
  })

  describe('createUser', () => {
    let now: Date

    beforeEach(() => {
      now = new Date(Date.now())
    })

    it('should return a new user', async () => {
      const id = v4()
      const newUserDto = {
        name: 'hello',
        lastname: 'world',
        age: 88,
      }

      const newUser: User = { ...newUserDto, id, createdAt: now, updatedAt: now, version: 1, reports: [] }

      jest.spyOn(userService, 'createUser').mockImplementation(() => Promise.resolve(newUser))
      const result = await controller.createUser(newUserDto)
      expect(result).toEqual(newUser)
    })
  })
})

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version

Replace MomentJS with date-fns in Nestjs

 0 total views

We use NestJS at work and the backend project needs to perform date calculations and formatting in the services. Our team chose momentJS because it is the one of the most popular date-time open source project. After a while, we discovered that the library size is huge (~290kb) and it is not tree-shakable. If I want to use format function to display a date in YYYY-MM-DD format, I must import the entire library into the code.

In Angular Architect Training Course, Bonnie Brennan said moment is so huge that that we should import moment locale instead. I chose a different route and decided to get rid of moment once and for all.

After doing some research, our team decided to replace momentJS with date-fns library, a lightweight date-time library, that has all the functions our project needs.

We made the migration in five steps:

  1. Install and extend eslint-plugin-you-dont-need-momentjs plugin, run eslint on the files to find all momentJS errors
  2. Replace momentJS functions with native JS if possible
  3. Create DateFnsService service in CoreModule that encapsulates the functionality of date-fns/fp submodule
  4. Import CoreModule into other modules of the project
  5. Inject DateFnsService in constructors and replace the remaining momentJS functions with functions of DateFnsService

Step 1: Install and extend eslint-plugin-you-dont-need-momentjs plugin

npm install --save-dev eslint-plugin-you-dont-need-momentjs
"extends" : ["plugin:you-dont-need-momentjs/recommended"],

Executed npm run lint command on terminal, the plugin outputted errors and warnings.

Step 2: Convert momentJS functions to native JS

Follow the examples in https://github.com/you-dont-need/You-Dont-Need-Momentjs#parse to convert momentJS functions to native JS

For example, moment() is replaced with new Date() and isAfter is replaced with

// Before: in moment
moment('2010-10-20').isAfter('2010-10-19') => true

// After: in native JS
new Date(2010, 9, 20) > new Date(2010, 9, 19) => true

Step 3: Create DateFnsService service in CoreModule

The requirement of the service is to provide functionality to format date, and add days, months and years to a given date. It is feasible by using functions defined in date-fns/fp submodule

First, we install date-fns dependency in the project

npm install date-fns --save

Create DateFnsService service in CoreModule

 import { format, addDays, addMonths, addYears } from 'date-fns/fp'

Injectable()
export class DateFnsService {

    format(date: Date | number, format: string): string {
        return format(format)(date)
    }

    addDays(date: Date | number, amount: number): Date {
        return addDays(amount)(date)
    }

    addMonths(date: Date | number, amount: number): Date {
        return addMonths(amount)(date)
    }

    addYears(date: Date | number, amount: number): Date {
        return addYears(amount)(date)
    }
}

Export DateFnsService from CoreModule such that it can be used outside of Core

@Module({
    providers: [DateFnsService],
    exports: [DateFnsService]    
})
export class CoreModule {}

Step 4: Import CoreModule to other modules

import { CoreModule } from '@/core'

@Module({
    imports: [CoreModule],
    providers: [AppController, AppService],
    exports: []    
})
export class AppModule {}

Step 5: Use DateFnsService in place of momentJS

Inject DateFnsService in AppService’s constructor and call its functions to manipulate date inside the function

import { DateFnsService } from '@/core'
// import * as moment from 'moment'

@Inject()
export class AppService {
   constructor (private datefnsService: DateFnsService) {}

   someFunction(): string {
      // Before:  
      // return = moment()
      //    .add(1, 'days')
      //    .add(1, 'months')
      //    .add(1, 'years')
      //    .format('YYYY-MM-DD')

      // After: 
      let mydate = this.datefnsService.addDays(new Date(), 1)
      mydate = this.datefnsService.addMonths(mydate, 1)
      mydate = this.datefnsService.addYears(mydate, 1)
      return this.datefnsService.format(mydate, 'yyyy-MM-dd');  <= '2022-06-19'
   }
}

Step 5 is repeated in all services until npm run lint does not produce any momentJS error. Afterward, we remove moment dependency from package.json and the project saves around 260kb.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. https://github.com/you-dont-need/You-Dont-Need-Momentjs#days-in-month
  2. https://date-fns.org/v2.21.3/docs/fp/Getting-Started

You probably don’t need Lodash in Nestjs

 0 total views

I completed Angular Architect Training Course from Bonnie Brennan at Angular Nation and lesson 1 is about style and structure. Even though the tips are for Angular application but a couple of them applies to NestJS. One of them is to void fat libraries such as lodash and moment.

In this post, I am going to describe how I limited the import size and usage of lodash library in our NestJS project at work.

Lodash contains a lot of useful functions but it is a rather large library with the size of 70kb. When developers import an lodash function using ES6 import, they are in fact import the entire library into the file that is unexpected.

After reading few blog posts, I found out that both statements are equivalent and import the whole library to file

  • import _ from ‘lodash’
  • import { sort } from ‘lodash’

Even though NestJS app resides in server side and bundle size is not a criterial factor, I wish to import lodash functions that the project is using only and replace other lodash functions with native JS.

you-dont-use-lodash-underscore plugin

NestJS app uses eslint to lint files; therefore, I installed eslint plugin, eslint-plugin-you-dont-need-lodash-underscore, and extended it in eslintrc.js

npm install --save-dev eslint-plugin-you-dont-need-lodash-underscore
"extends" : ["plugin:you-dont-need-lodash-underscore/compatible"]

Execute npm run lint command on terminal and the plugin outputted lodash errors. For example, uniq, flatten and omit can be replaced with native JS.

I overrided @typescript-eslint/no-unused-vars rule such that eslint does not complain unused variable(s) when rest spread operator is used

"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
// Replace omit with rest spread operator
const something = omit(object, ['p1'])

const { p1, ...something } = object
// Replace uniq with Array destructuring and Set 
const u = uniq([1,2,3,1,1,1])

const u = [...new Set<number>([1,2,3,1,1,1])]
// Replace flatten with Array.flat if ES2019 is used
const f = flatten([1,[2,3], 4)

const f = [1,[2,3], 4].flat()

When all lodash errors were resolved, re-run npm run lint and it produced zero errors

Correct way to import lodash

I tried

import pick from 'lodash/pick' 

but the compiler complained. When I used

 import pick = require('lodash/pick')

it worked perfectly.

These are the steps I used to replace lodash functions with light-weight alternatives and import the rest one by one.

Resources:

  1. https://www.blazemeter.com/blog/the-correct-way-to-import-lodash-libraries-a-benchmark
  2. https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore

My Spanish app in PANNG stack

 2 total views

Intention: I am learning Spanish from a Spanish teacher and I cannot read my own notes due to my horrible handwriting. My resolution is to build a full-stack app to store new Spanish vocabulary.

Stack: PANNG (Prisma, Angular, NestJS, Nx, GraphQL)

Repo: https://github.com/railsstudent/nx-apollo-angular-course

Characteristics:

  • Nx Monorepo
  • GraphQL module in Nest
  • Apollo GraphQL
  • Angular Apollo
  • TypeScript GraphQL code generation to generate GraphQL services
  • Tailwind CSS

Features:

  • Cursor-based pagination in course, lesson and sentence lists respectively
  • Add and retrieve course
  • Add and retrieve lesson
  • Add, delete and retrieve sentence
  • Add, delete and retrieve translation
  • Speak Spanish texts

Local graphQL playground

http://localhost:3333/graphql