> ## Documentation Index
> Fetch the complete documentation index at: https://woltz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# With TypeORM

> Complete guide to using Rich Domain with TypeORM

## Installation

Install the TypeORM adapter:

<CodeGroup>
  ```bash npm theme={null}
  npm install @woltz/rich-domain @woltz/rich-domain-typeorm typeorm reflect-metadata
  ```

  ```bash pnpm theme={null}
  pnpm add @woltz/rich-domain @woltz/rich-domain-typeorm typeorm reflect-metadata
  ```

  ```bash yarn theme={null}
  yarn add @woltz/rich-domain @woltz/rich-domain-typeorm typeorm reflect-metadata
  ```
</CodeGroup>

## Setup TypeORM

Create `src/data-source.ts`:

```typescript theme={null}
import { DataSource } from "typeorm";
import "reflect-metadata";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "user",
  password: "password",
  database: "mydb",
  synchronize: true,
  logging: false,
  entities: ["src/infrastructure/entities/**/*.ts"],
  migrations: [],
  subscribers: [],
});
```

Initialize in your app:

```typescript theme={null}
import { AppDataSource } from "./data-source";

await AppDataSource.initialize();
console.log("Data Source initialized");
```

## Define Entity Schema

Create your TypeORM entity:

```typescript theme={null}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity("products")
export class ProductEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  name!: string;

  @Column("decimal")
  price!: number;

  @Column("int")
  stock!: number;

  @Column()
  status!: string;

  @Column()
  createdAt!: Date;

  @Column()
  updatedAt!: Date;
}
```

## Define Your Aggregate

Create your domain model:

```typescript theme={null}
import { z } from "zod";
import { Aggregate, EntityValidation, Id } from "@woltz/rich-domain";

const productSchema = z.object({
  id: z.custom<Id>((val) => val instanceof Id),
  name: z.string().min(3),
  price: z.number().positive(),
  stock: z.number().int().min(0),
  status: z.enum(["active", "inactive"]),
});

type ProductProps = z.infer<typeof productSchema>;

class Product extends Aggregate<ProductProps> {
  protected static validation: EntityValidation<ProductProps> = {
    schema: productSchema,
    config: {
      onCreate: true,
      onUpdate: true,
      throwOnError: true,
    },
  };

  get name() {
    return this.props.name;
  }

  get price() {
    return this.props.price;
  }

  get stock() {
    return this.props.stock;
  }

  updatePrice(newPrice: number) {
    this.props.price = newPrice;
  }

  decreaseStock(amount: number) {
    this.props.stock -= amount;
  }

  activate() {
    this.props.status = "active";
  }

  deactivate() {
    this.props.status = "inactive";
  }
}
```

## Create Mappers

### ToDomain Mapper

Maps from TypeORM to Domain:

```typescript theme={null}
import { Mapper, Id } from "@woltz/rich-domain";

export class ProductToDomainMapper extends Mapper<ProductEntity, Product> {
  build(entity: ProductEntity): Product {
    return new Product({
      id: Id.from(entity.id),
      name: entity.name,
      price: entity.price,
      stock: entity.stock,
      status: entity.status as "active" | "inactive",
    });
  }
}
```

### ToPersistence Mapper

Maps from Domain to TypeORM using `TypeORMToPersistence`:

```typescript theme={null}
import { EntityManager } from "typeorm";
import { 
  TypeORMToPersistence, 
  TypeORMUnitOfWork,
  Transactional 
} from "@woltz/rich-domain-typeorm";
import { EntitySchemaRegistry } from "@woltz/rich-domain";

export class ProductToPersistenceMapper extends TypeORMToPersistence<Product> {
  protected readonly registry = new EntitySchemaRegistry().register({
    entity: "Product",
    table: "products",
  });

  protected readonly entityClasses = new Map<string, new () => any>([
    ["Product", ProductEntity],
  ]);

  @Transactional()
  protected async onCreate(product: Product, em: EntityManager): Promise<void> {
    const entity = new ProductEntity();
    entity.id = product.id.value;
    entity.name = product.name;
    entity.price = product.price;
    entity.stock = product.stock;
    entity.status = product.status;
    entity.createdAt = new Date();
    entity.updatedAt = new Date();

    await em.save(entity);
  }
}
```

<Note>
  * `onCreate` receives `EntityManager` as second parameter
  * Use `@Transactional()` decorator for automatic transaction management
  * `entityClasses` Map is required for the BatchExecutor
  * `onUpdate` is handled automatically by `TypeORMBatchExecutor` - no need to override
</Note>

## Create Repository

Implement the repository:

```typescript theme={null}
import { 
  TypeORMRepository, 
  TypeORMUnitOfWork,
  SearchableField 
} from "@woltz/rich-domain-typeorm";
import { AppDataSource } from "./data-source";
import { Repository } from "typeorm";

const uow = new TypeORMUnitOfWork(AppDataSource);

interface IProductRepository {
  save(product: Product): Promise<void>;
  findById(id: string): Promise<Product | null>;
}

class ProductRepository
  extends TypeORMRepository<Product, ProductEntity>
  implements IProductRepository
{
  constructor() {
    const typeormRepo = AppDataSource.getRepository(ProductEntity);
    
    super({
      typeormRepository: typeormRepo,
      toDomainMapper: new ProductToDomainMapper(),
      toPersistenceMapper: new ProductToPersistenceMapper(uow),
      uow,
      alias: "product", // Optional: alias for QueryBuilder
    });
  }

  protected getSearchableFields(): SearchableField<ProductEntity>[] {
    return ["name"]; // Case-insensitive search by default
  }

  protected getDefaultRelations(): string[] {
    return []; // Add relations if needed: ["category", "supplier"]
  }

  async findById(id: string): Promise<Product | null> {
    const entity = await this.typeormRepo.findOne({
      where: { id },
      relations: this.getDefaultRelations(),
    });

    return entity ? this.toDomainMapper.build(entity) : null;
  }
}
```

<Tip>
  **Key points:**

  * ToPersistenceMapper receives `uow` in constructor
  * Use `getSearchableFields()` to define which fields support search
  * Use `getDefaultRelations()` to eagerly load relations
  * `typeormRepo` is available from the base class for custom queries
</Tip>

````

## Use It

```typescript
// Initialize TypeORM
await AppDataSource.initialize();

const repository = new ProductRepository();

// Create product
const product = new Product({
  name: "Mechanical Keyboard",
  price: 149.99,
  stock: 50,
  status: "active",
});

// Save to database
await repository.save(product);
console.log("Product created:", product.id.value);

// Update
product.updatePrice(129.99);
product.decreaseStock(5);
await repository.save(product);

// Query
const found = await repository.findById(product.id.value);
console.log("Found product:", found?.name); // "Mechanical Keyboard"

// Use Criteria for complex queries
const criteria = Criteria.create<Product>()
  .where("status", "equals", "active")
  .where("price", "greaterThan", 100)
  .orderBy("name", "asc")
  .paginate(1, 10);

const results = await repository.find(criteria);
console.log(results.data); // Array of products
console.log(results.meta); // Pagination info
````

## Working with Relations

### 1:N Owned Relations

```typescript theme={null}
// TypeORM Entities
@Entity("users")
export class UserEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  email!: string;

  @Column()
  name!: string;

  @OneToMany(() => PostEntity, post => post.author)
  posts!: PostEntity[];
}

@Entity("posts")
export class PostEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  authorId!: string;

  @ManyToOne(() => UserEntity, user => user.posts)
  author!: UserEntity;
}

// ToPersistence with 1:N
export class UserToPersistenceMapper extends TypeORMToPersistence<User> {
  protected readonly registry = new EntitySchemaRegistry()
    .register({
      entity: "User",
      table: "user",
      collections: {
        posts: {
          type: "owned",
          entity: "Post",
        },
      },
    })
    .register({
      entity: "Post",
      table: "post",
      parentFk: {
        field: "authorId",
        parentEntity: "User",
      },
    });

  protected readonly entityClasses = new Map([
    ["User", UserEntity],
    ["Post", PostEntity],
  ]);

  @Transactional()
  protected async onCreate(user: User, em: EntityManager): Promise<void> {
    // Create user
    const userEntity = new UserEntity();
    userEntity.id = user.id.value;
    userEntity.email = user.email;
    userEntity.name = user.name;
    await em.save(userEntity);

    // Create owned posts
    for (const post of user.posts) {
      const postEntity = new PostEntity();
      postEntity.id = post.id.value;
      postEntity.title = post.title;
      postEntity.authorId = user.id.value;
      await em.save(postEntity);
    }
  }
}
```

### N:N with Junction Tables

```typescript theme={null}
// TypeORM Entities
@Entity("posts")
export class PostEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @ManyToMany(() => TagEntity)
  @JoinTable({
    name: "_PostToTag",
    joinColumn: { name: "A" },
    inverseJoinColumn: { name: "B" },
  })
  tags!: TagEntity[];
}

@Entity("tags")
export class TagEntity {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  name!: string;
}

// ToPersistence with N:N
export class PostToPersistenceMapper extends TypeORMToPersistence<Post> {
  protected readonly registry = new EntitySchemaRegistry().register({
    entity: "Post",
    table: "post",
    collections: {
      tags: {
        type: "reference",
        entity: "Tag",
        junction: {
          table: "_PostToTag",
          sourceKey: "A",
          targetKey: "B",
        },
      },
    },
  });

  protected readonly entityClasses = new Map([
    ["Post", PostEntity],
    ["Tag", TagEntity],
  ]);

  @Transactional()
  protected async onCreate(post: Post, em: EntityManager): Promise<void> {
    // Create post
    const entity = new PostEntity();
    entity.id = post.id.value;
    entity.title = post.title;
    entity.content = post.content;
    await em.save(entity);

    // Create junction records
    if (post.tags.length > 0) {
      for (const tag of post.tags) {
        await em.query(
          `INSERT INTO "_PostToTag" ("A", "B") VALUES ($1, $2) ON CONFLICT DO NOTHING`,
          [entity.id, tag.id.value]
        );
      }
    }
  }
}

// Usage
const post = Post.create({
  title: "My Post",
  content: "Content here",
});

const tag1 = new Tag({ id: Id.from("tag-1") });
const tag2 = new Tag({ id: Id.from("tag-2") });

post.addTag(tag1);
post.addTag(tag2);

await postRepository.save(post);
// Post and junction records are created automatically
```

## Unit of Work with Transactions

Use the `@Transactional` decorator for automatic transaction management:

```typescript theme={null}
import { Transactional } from "@woltz/rich-domain-typeorm";

class ProductService {
  constructor(private repository: ProductRepository) {}

  @Transactional()
  async transferStock(fromId: string, toId: string, amount: number) {
    const from = await this.repository.findById(fromId);
    const to = await this.repository.findById(toId);

    if (!from || !to) throw new Error("Product not found");

    from.decreaseStock(amount);
    to.increaseStock(amount);

    await this.repository.save(from);
    await this.repository.save(to);
    
    // Commits on success, rolls back on error
  }
}
```

Or use the imperative API:

```typescript theme={null}
async function transferStock(fromId: string, toId: string, amount: number) {
  await uow.transaction(async () => {
    const from = await repository.findById(fromId);
    const to = await repository.findById(toId);

    if (!from || !to) throw new Error("Product not found");

    from.decreaseStock(amount);
    to.increaseStock(amount);

    await repository.save(from);
    await repository.save(to);
  });
}
```

## Advanced Queries

TypeORM gives you full access to QueryBuilder:

```typescript theme={null}
class ProductRepository extends TypeORMRepository<Product, ProductEntity> {
  async findLowStock(threshold: number): Promise<Product[]> {
    const entities = await this.repository
      .createQueryBuilder("product")
      .where("product.stock < :threshold", { threshold })
      .andWhere("product.status = :status", { status: "active" })
      .orderBy("product.stock", "ASC")
      .getMany();

    return entities.map(e => this.toDomainMapper.map(e));
  }

  async findByPriceRange(min: number, max: number): Promise<Product[]> {
    const entities = await this.repository
      .createQueryBuilder("product")
      .where("product.price BETWEEN :min AND :max", { min, max })
      .getMany();

    return entities.map(e => this.toDomainMapper.map(e));
  }
}
```

## Transactional Outbox (optional)

If your aggregates emit domain events, pass an optional **`outboxStore`** in `TypeORMRepositoryConfig`. When set, `save()` persists uncommitted events to the outbox table in the **same transaction** as the aggregate — so events are not lost if the process crashes before `dispatchAll()`.

See **[Transactional Outbox](/integrations/outbox)** for the full setup (`OutboxEntity`, event bus decorator, background publisher). Repository wiring is documented under [TypeORM Integration → Transactional Outbox](/integrations/typeorm#transactional-outbox).

## Next Steps

<CardGroup cols={2}>
  <Card title="TypeORM Integration" icon="table" href="/integrations/typeorm">
    Deep dive into TypeORM adapter features
  </Card>

  <Card title="Transactional Outbox" icon="inbox" href="/integrations/outbox">
    Guaranteed domain event delivery with the outbox pattern
  </Card>

  <Card title="Repository Pattern" icon="book" href="/repository/overview">
    Learn advanced repository patterns
  </Card>

  <Card title="Change Tracking" icon="clock-rotate-left" href="/core/change-tracking">
    Understand how changes are tracked
  </Card>

  <Card title="Transactions" icon="arrows-rotate" href="/integrations/typeorm#transactions">
    Advanced transaction management
  </Card>
</CardGroup>
