> ## 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.

# Overview

> Abstract persistence layer for domain aggregates

## What is a Repository?

A Repository is an abstraction that mediates between the domain layer and the data mapping layer. It provides a collection-like interface for accessing domain aggregates while hiding the complexity of database operations.

```typescript theme={null}
// Repository abstracts away persistence details
const user = await userRepository.findById("user-123");
user.changeName("New Name");
await userRepository.save(user);

// The domain code doesn't know about:
// - Database connections
// - SQL/ORM queries  
// - Data mapping
// - Which database is being used
```

## Why Use Repositories?

<CardGroup cols={2}>
  <Card title="Domain Isolation" icon="shield-halved">
    Domain code stays free of persistence concerns
  </Card>

  <Card title="Testability" icon="flask-vial">
    Easy to mock or use in-memory implementations for testing
  </Card>

  <Card title="Flexibility" icon="arrows-rotate">
    Switch databases or ORMs without changing domain code
  </Card>

  <Card title="Consistency" icon="check-double">
    Centralized place for query logic and data access patterns
  </Card>
</CardGroup>

## Repository Types

The library provides several base classes for different use cases:

### Repository (Full CRUD)

Complete read and write operations:

```typescript theme={null}
import { Repository, Aggregate, Criteria, PaginatedResult } from "@woltz/rich-domain";

abstract class Repository<TDomain extends Aggregate<any>> {
  abstract find(criteria?: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
  abstract findById(id: string): Promise<TDomain | null>;
  abstract save(entity: TDomain): Promise<void>;
  abstract delete(entity: TDomain): Promise<void>;
  abstract count(criteria?: Criteria<TDomain>): Promise<number>;
  abstract exists(id: string): Promise<boolean>;
}
```

### ReadRepository

Read-only operations (useful for CQRS read models):

```typescript theme={null}
abstract class ReadRepository<TDomain extends Aggregate<any>> {
  abstract find(criteria?: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
  abstract findById(id: string): Promise<TDomain | null>;
  abstract count(criteria?: Criteria<TDomain>): Promise<number>;
  abstract exists(id: string): Promise<boolean>;
}
```

### WriteRepository

Write-only operations:

```typescript theme={null}
abstract class WriteRepository<TDomain extends Aggregate<any>> {
  abstract save(entity: TDomain): Promise<void>;
  abstract delete(entity: TDomain): Promise<void>;
}
```

## Implementing a Repository

### Step 1: Define Your Aggregate

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

interface UserProps {
  id: Id;
  name: string;
  email: string;
  status: "active" | "inactive";
}

class User extends Aggregate<UserProps> {
  get name() { return this.props.name; }
  get email() { return this.props.email; }
  get status() { return this.props.status; }

  changeName(name: string) {
    this.props.name = name;
  }

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

### Step 2: Create Mappers

Mappers transform data between domain models and persistence format:

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

// Domain → Persistence
class UserToPersistenceMapper extends Mapper<User, UserRecord> {
  build(user: User): UserRecord {
    return {
      id: user.id.value,
      name: user.name,
      email: user.email,
      status: user.status,
      created_at: new Date(),
      updated_at: new Date(),
    };
  }
}

// Persistence → Domain
class UserToDomainMapper extends Mapper<UserRecord, User> {
  build(record: UserRecord): User {
    return new User({
      id: Id.from(record.id),
      name: record.name,
      email: record.email,
      status: record.status as "active" | "inactive",
    });
  }
}
```

### Step 3: Implement Repository

Here's a database-agnostic implementation skeleton:

```typescript theme={null}
import { Repository, Criteria, PaginatedResult } from "@woltz/rich-domain";

class UserRepository extends Repository<User> {
  constructor(
    private db: YourDatabaseClient,  // Any database client
    private mapperToDomain: UserToDomainMapper,
    private mapperToPersistence: UserToPersistenceMapper
  ) {
    super();
  }

  async findById(id: string): Promise<User | null> {
    // Implement using your database client
    const record = await this.db.findOne("users", { id });
    return record ? this.mapperToDomain.build(record) : null;
  }

  async find(criteria: Criteria<User>): Promise<PaginatedResult<User>> {
    // Convert criteria to your database's query format
    const query = this.buildQuery(criteria);
    const pagination = criteria.getPagination();

    const [records, total] = await Promise.all([
      this.db.query("users", query, pagination),
      this.db.count("users", query),
    ]);

    const users = records.map((r) => this.mapperToDomain.build(r));
    return PaginatedResult.create(users, pagination, total);
  }

  async save(user: User): Promise<void> {
    const data = this.mapperToPersistence.build(user);
    
    if (user.isNew()) {
      await this.db.insert("users", data);
    } else {
      await this.db.update("users", user.id.value, data);
    }
  }

  async delete(user: User): Promise<void> {
    await this.db.delete("users", user.id.value);
  }

  async exists(id: string): Promise<boolean> {
    return this.db.exists("users", { id });
  }

  async count(criteria?: Criteria<User>): Promise<number> {
    const query = criteria ? this.buildQuery(criteria) : {};
    return this.db.count("users", query);
  }

  private buildQuery(criteria: Criteria<User>): object {
    // Convert criteria filters/orders to your database format
    // This is ORM/database-specific
  }
}
```

<Note>
  For complete implementations with specific ORMs, see:

  * [Prisma Integration](/integrations/prisma) - Full-featured adapter with change tracking
  * [TypeORM Integration](/integrations/typeorm) - Full-featured adapter with change tracking
  * [Drizzle Integration](/integrations/drizzle) - Coming soon
</Note>

## Using the Repository

### Finding Entities

```typescript theme={null}
// Find by ID
const user = await userRepository.findById("user-123");

// Find with criteria
const activeUsers = await userRepository.find(
  Criteria.create<User>()
    .whereEquals("status", "active")
    .orderByDesc("createdAt")
    .paginate(1, 20)
);

// Check existence
const exists = await userRepository.exists("user-123");

// Count
const count = await userRepository.count(
  Criteria.create<User>().whereEquals("status", "active")
);
```

### Saving Entities

```typescript theme={null}
// Create new entity
const user = new User({
  name: "John Doe",
  email: "john@example.com",
  status: "active",
});
await userRepository.save(user);  // INSERT

// Update existing entity
const user = await userRepository.findById("user-123");
user.changeName("Jane Doe");
await userRepository.save(user);  // UPDATE
```

### Deleting Entities

```typescript theme={null}
const user = await userRepository.findById("user-123");
await userRepository.delete(user);
```

## Custom Query Methods

Add domain-specific query methods to your repository:

```typescript theme={null}
class UserRepository extends Repository<User> {
  // ... base implementation

  async findByEmail(email: string): Promise<User | null> {
    const record = await this.db.findOne("users", { email });
    return record ? this.mapperToDomain.build(record) : null;
  }

  async findActiveUsers(page: number, limit: number): Promise<PaginatedResult<User>> {
    return this.find(
      Criteria.create<User>()
        .whereEquals("status", "active")
        .orderByDesc("createdAt")
        .paginate(page, limit)
    );
  }

  async findRecentlyJoined(days: number): Promise<User[]> {
    const since = new Date();
    since.setDate(since.getDate() - days);

    const result = await this.find(
      Criteria.create<User>()
        .where("createdAt", "greaterThan", since)
        .orderByDesc("createdAt")
    );

    return result.data;
  }

  async countByStatus(status: "active" | "inactive"): Promise<number> {
    return this.count(
      Criteria.create<User>().whereEquals("status", status)
    );
  }
}
```

## Repository with Change Tracking

For aggregates with complex nested structures, use change tracking to efficiently persist only what changed:

```typescript theme={null}
class OrderRepository extends Repository<Order> {
  async save(order: Order): Promise<void> {
    const changes = order.getChanges();

    // Skip if no changes
    if (changes.isEmpty() && !order.isNew()) {
      return;
    }

    // Use transaction for consistency
    await this.db.transaction(async (tx) => {
      if (order.isNew()) {
        // Create root aggregate
        await tx.insert("orders", this.mapOrder(order));
      }

      // Process batch operations in correct order
      const batch = changes.toBatchOperations();

      // 1. Deletes (leaf → root for FK constraints)
      for (const del of batch.deletes) {
        await tx.deleteMany(del.entity, { id: { in: del.ids } });
      }

      // 2. Creates (root → leaf for FK constraints)
      for (const create of batch.creates) {
        await tx.insertMany(
          create.entity,
          create.items.map((item) => this.mapItem(item))
        );
      }

      // 3. Updates
      for (const update of batch.updates) {
        for (const item of update.items) {
          await tx.update(update.entity, item.id, item.changedFields);
        }
      }
    });

    // Mark aggregate as clean after successful save
    order.markAsClean();
  }
}
```

<Tip>
  The [Prisma Adapter](/integrations/prisma) provides `PrismaBatchExecutor` that handles all this automatically.
</Tip>

## Testing with Repositories

### In-Memory Implementation

Create an in-memory implementation for unit tests:

```typescript theme={null}
class InMemoryUserRepository extends Repository<User> {
  private store = new Map<string, User>();

  async findById(id: string): Promise<User | null> {
    return this.store.get(id) || null;
  }

  async save(user: User): Promise<void> {
    this.store.set(user.id.value, user);
  }

  async delete(user: User): Promise<void> {
    this.store.delete(user.id.value);
  }

  async find(criteria: Criteria<User>): Promise<PaginatedResult<User>> {
    const all = Array.from(this.store.values());
    // PaginatedResult.fromArray applies criteria in-memory
    return PaginatedResult.fromArray(all, criteria);
  }

  async exists(id: string): Promise<boolean> {
    return this.store.has(id);
  }

  async count(criteria?: Criteria<User>): Promise<number> {
    if (!criteria) return this.store.size;
    const result = await this.find(criteria);
    return result.meta.total;
  }

  // Test helpers
  clear(): void {
    this.store.clear();
  }

  getAll(): User[] {
    return Array.from(this.store.values());
  }
}
```

### Using in Tests

```typescript theme={null}
describe("UserService", () => {
  let userRepository: InMemoryUserRepository;
  let userService: UserService;

  beforeEach(() => {
    userRepository = new InMemoryUserRepository();
    userService = new UserService(userRepository);
  });

  afterEach(() => {
    userRepository.clear();
  });

  it("should create a user", async () => {
    const user = await userService.createUser({
      name: "John",
      email: "john@example.com",
    });

    expect(user.id).toBeDefined();
    expect(await userRepository.exists(user.id.value)).toBe(true);
  });

  it("should find active users", async () => {
    // Setup
    await userRepository.save(new User({ name: "Active", status: "active" }));
    await userRepository.save(new User({ name: "Inactive", status: "inactive" }));

    // Test
    const result = await userRepository.find(
      Criteria.create<User>().whereEquals("status", "active")
    );

    expect(result.data.length).toBe(1);
    expect(result.data[0].name).toBe("Active");
  });
});
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Criteria" icon="filter" href="/criteria/overview">
    Learn about type-safe query building
  </Card>

  <Card title="Mappers" icon="right-left" href="/repository/mappers">
    Domain ↔ persistence data transformation
  </Card>

  <Card title="Schema Registry" icon="sitemap" href="/repository/schema-registry">
    Map entities to database tables
  </Card>

  <Card title="Prisma Integration" icon="database" href="/integrations/prisma">
    Complete Prisma implementation
  </Card>
</CardGroup>
