Relations

One-to-Many and Many-to-One

A category has many products; an order has many order items; each item points to one product.

// categories/entities/category.entity.ts
@Entity('categories')
export class Category {
  @PrimaryGeneratedColumn() id: number;
  @Column({ unique: true }) slug: string;
  @Column() name: string;
  @OneToMany(() => Product, (product) => product.category) products: Product[];
}

// orders/entities/order.entity.ts
@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid') id: string;
  @ManyToOne(() => User, { nullable: false }) user: User;
  @OneToMany(() => OrderItem, (item) => item.order, { cascade: ['insert'] }) items: OrderItem[];
  @Column({ type: 'numeric', precision: 12, scale: 2 }) total: string;
  @Column({ default: 'PENDING' }) status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
  @CreateDateColumn() createdAt: Date;
}

// orders/entities/order-item.entity.ts
@Entity('order_items')
export class OrderItem {
  @PrimaryGeneratedColumn() id: number;
  @ManyToOne(() => Order, (order) => order.items, { onDelete: 'CASCADE' }) order: Order;
  @ManyToOne(() => Product, { eager: true }) product: Product;
  @Column('int') quantity: number;
  @Column({ type: 'numeric', precision: 10, scale: 2 }) unitPrice: string;   // price at time of order
}

Loading Relations and Transactions

Relations are not loaded unless you ask (relations option or a join). Placing an order touches several tables, so wrap it in a transaction: all succeed or none.

src/orders/orders.service.ts

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class OrdersService {
  constructor(private readonly dataSource: DataSource) {}

  findMine(userId: number) {
    return this.dataSource.getRepository(Order).find({
      where: { user: { id: userId } },
      relations: { items: { product: true } },
      order: { createdAt: 'DESC' },
    });
  }

  place(userId: number, items: { productId: number; quantity: number }[]) {
    return this.dataSource.transaction(async (manager) => {
      let total = 0;
      const orderItems: Partial<OrderItem>[] = [];
      for (const { productId, quantity } of items) {
        // atomic stock check + decrement; 0 rows affected = not enough stock
        const res = await manager.decrement(Product, { id: productId, stock: MoreThanOrEqual(quantity) }, 'stock', quantity);
        if (res.affected === 0) throw new OutOfStockException(productId, quantity, 0);
        const product = await manager.findOneByOrFail(Product, { id: productId });
        total += product.price * quantity;
        orderItems.push({ product, quantity, unitPrice: product.price.toFixed(2) });
      }
      return manager.save(Order, { user: { id: userId }, items: orderItems, total: total.toFixed(2) });
    });   // any throw -> ROLLBACK
  }
}