Налаштування ORM TypeORM для веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Налаштування ORM TypeORM для веб-застосунку
Середня
~1 робочий день
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Налаштування ORM TypeORM для веб-додатку

TypeORM — один із найстаріших ORM для TypeScript/Node.js. Підтримує Active Record та Data Mapper паттерни, декоратори для визначення сутностей та багатий набір функцій: міграції, subscribers, relations, query builder. Широко використовується в NestJS-проектах — там це де-факто стандарт.

Встановлення

npm install typeorm reflect-metadata
npm install pg  # PostgreSQL
# або mysql2, better-sqlite3, mongodb

# У tsconfig.json
{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

Підключення до бази

// db/data-source.ts
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entities/User'
import { Post } from './entities/Post'

export const AppDataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [User, Post],
  migrations: ['dist/db/migrations/*.js'],
  migrationsTableName: 'migrations',
  synchronize: false,  // НІКОЛИ true в production
  logging: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
  extra: {
    max: 20,
    idleTimeoutMillis: 30000,
  }
})

// Ініціалізація
await AppDataSource.initialize()

Сутності

// db/entities/User.ts
import {
  Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
  UpdateDateColumn, OneToMany, Index, BeforeInsert, BeforeUpdate
} from 'typeorm'
import bcrypt from 'bcrypt'
import { Post } from './Post'

export enum UserRole {
  USER = 'user',
  MODERATOR = 'moderator',
  ADMIN = 'admin',
}

@Entity('users')
@Index(['email'])
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ length: 255, unique: true })
  email: string

  @Column({ length: 255 })
  name: string

  @Column({ select: false })  // не включати у SELECT за умовчанням
  passwordHash: string

  @Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
  role: UserRole

  @OneToMany(() => Post, (post) => post.author)
  posts: Post[]

  @CreateDateColumn({ type: 'timestamptz' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamptz' })
  updatedAt: Date

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword() {
    if (this.passwordHash && !this.passwordHash.startsWith('$2b$')) {
      this.passwordHash = await bcrypt.hash(this.passwordHash, 12)
    }
  }
}
// db/entities/Post.ts
import {
  Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
  UpdateDateColumn, ManyToOne, ManyToMany, JoinTable, JoinColumn, Index
} from 'typeorm'
import { User } from './User'
import { Tag } from './Tag'

@Entity('posts')
@Index(['authorId', 'createdAt'])
@Index(['published', 'createdAt'])
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string

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

  @Column({ type: 'text', nullable: true })
  content: string | null

  @Column({ default: false })
  published: boolean

  @Column({ name: 'author_id' })
  authorId: string

  @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'author_id' })
  author: User

  @ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true })
  @JoinTable({
    name: 'posts_to_tags',
    joinColumn: { name: 'post_id' },
    inverseJoinColumn: { name: 'tag_id' }
  })
  tags: Tag[]

  @Column({ default: 0 })
  viewCount: number

  @Column({ type: 'timestamptz', nullable: true })
  publishedAt: Date | null

  @CreateDateColumn({ type: 'timestamptz' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamptz' })
  updatedAt: Date
}

Міграції

# Генерація міграції з diff
npx typeorm migration:generate -n AddUserProfile -d dist/db/data-source.js

# Створити порожню міграцію вручную
npx typeorm migration:create -n AddIndexes

# Застосувати
npx typeorm migration:run -d dist/db/data-source.js

# Відкатити останню
npx typeorm migration:revert -d dist/db/data-source.js

Query Builder

const postRepository = AppDataSource.getRepository(Post)

// Пошук з розбиванням на сторінки
async function findPosts(opts: { page: number; limit: number; search?: string }) {
  const { page, limit, search } = opts
  const qb = postRepository.createQueryBuilder('post')
    .leftJoinAndSelect('post.author', 'author')
    .leftJoinAndSelect('post.tags', 'tag')
    .where('post.published = :published', { published: true })
    .orderBy('post.createdAt', 'DESC')
    .skip((page - 1) * limit)
    .take(limit)

  if (search) {
    qb.andWhere(
      'post.title ILIKE :search OR post.content ILIKE :search',
      { search: `%${search}%` }
    )
  }

  const [items, total] = await qb.getManyAndCount()
  return { items, total, pages: Math.ceil(total / limit) }
}

Subscribers (хуки)

import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'

@EventSubscriber()
export class PostSubscriber implements EntitySubscriberInterface<Post> {
  listenTo() { return Post }

  async afterInsert(event: InsertEvent<Post>) {
    await SearchIndexer.index('posts', event.entity)
  }

  async afterUpdate(event: UpdateEvent<Post>) {
    if (event.updatedColumns.some(c => c.propertyName === 'published')) {
      await NotificationService.notifyFollowers(event.entity)
    }
  }
}

NestJS інтеграція

// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        url: config.get('DATABASE_URL'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        migrations: [__dirname + '/db/migrations/*{.ts,.js}'],
        migrationsRun: true,
        synchronize: false,
      })
    }),
    TypeOrmModule.forFeature([User, Post])
  ]
})
export class AppModule {}

Терміни

Базове налаштування TypeORM з сутностями, міграціями та репозиторіями: 1–2 дні. Інтеграція в NestJS-проект з модулями та тестами: 2–3 дні. Перевід існуючого проекту з іншого ORM на TypeORM: 3–5 днів.