Careeroza
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
}
}