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

# Prisma

> Plug-and-play integration between rich-domain and Prisma ORM

## Overview

`@woltz/rich-domain-prisma` provides seamless integration between rich-domain and Prisma ORM. It leverages Prisma's nested writes and transaction support to align perfectly with rich-domain's change tracking and Unit of Work patterns.

```bash theme={null}
npm install @woltz/rich-domain @woltz/rich-domain-prisma
```

<CardGroup cols={2}>
  <Card title="Unit of Work" icon="arrows-rotate">
    Request-isolated transactions with AsyncLocalStorage
  </Card>

  <Card title="Repository Base Class" icon="database">
    PrismaRepository with built-in Criteria support
  </Card>

  <Card title="Change Tracking" icon="code-compare">
    PrismaToPersistence with automatic change detection
  </Card>

  <Card title="Batch Operations" icon="layer-group">
    PrismaBatchExecutor for efficient bulk writes
  </Card>

  <Card title="Transactional Outbox" icon="inbox" href="/integrations/outbox">
    Optional `outboxStore` on the repository for guaranteed event delivery
  </Card>
</CardGroup>

<Note>
  For a complete working example, see the [fastify-with-prisma example](https://github.com/tarcisioandrade/rich-domain/tree/main/examples/backend/fastify-with-prisma) in the repository.
</Note>

## Quick Start

### 1. Setup

```typescript theme={null}
import { PrismaClient } from "@prisma/client";
import { PrismaUnitOfWork } from "@woltz/rich-domain-prisma";

const prisma = new PrismaClient();
const uow = new PrismaUnitOfWork(prisma);
```

### 2. Create Repository

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

class UserRepository extends PrismaRepository<User, UserRecord> {
  protected readonly model = "user";
  protected readonly includes = { posts: true, profile: true };

  constructor(prisma: PrismaClient, uow: PrismaUnitOfWork) {
    super(
      new UserToPersistenceMapper(prisma, uow),
      new UserToDomainMapper(),
      prisma,
      uow
    );
  }
}
```

### 3. Use It

```typescript theme={null}
const userRepository = new UserRepository(prisma, uow);

// Create
const user = new User({
  name: "John",
  email: "john@example.com",
  posts: [],
});
await userRepository.save(user);

// Find with Criteria
const criteria = Criteria.create<User>()
  .where("name", "contains", "John")
  .orderBy("createdAt", "desc")
  .paginate(1, 10);

const result = await userRepository.find(criteria);

// Update with change tracking
const found = await userRepository.findById(user.id.value);
found.name = "John Updated";
found.posts.push(new Post({ title: "Hello", content: "World" }));
await userRepository.save(found); // Only changed data is persisted

// Delete
await userRepository.delete(found);
```

***

## PrismaUnitOfWork

Manages transactions with per-request isolation using `AsyncLocalStorage`.

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

const uow = new PrismaUnitOfWork(prisma);
```

### Transaction Execution

```typescript theme={null}
// Execute multiple operations atomically
await uow.transaction(async () => {
  await userRepository.save(user);
  await orderRepository.save(order);
  await notificationRepository.save(notification);
  // All or nothing - auto rollback on failure
});
```

### Request Isolation

Each HTTP request gets its own transaction context, preventing cross-request interference:

```typescript theme={null}
// Request 1
app.post("/users", async (req, res) => {
  await uow.transaction(async () => {
    // This transaction is isolated to Request 1
    await userRepository.save(user);
  });
});

// Request 2 (concurrent)
app.get("/users/:id", async (req, res) => {
  // NOT affected by Request 1's transaction
  const user = await userRepository.findById(req.params.id);
});
```

### API Reference

| Method                | Description                             |
| --------------------- | --------------------------------------- |
| `transaction(work)`   | Execute work function in a transaction  |
| `isInTransaction()`   | Check if currently in a transaction     |
| `getCurrentContext()` | Get current transaction context or null |

***

## @Transactional Decorator

Decorator that automatically wraps a method in a transaction.

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

class CreateUserUseCase {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly uow: PrismaUnitOfWork
  ) {}

  @Transactional()
  async execute(input: CreateUserInput): Promise<User> {
    // Everything here runs in a transaction automatically
    const user = new User({ ...input, posts: [] });
    await this.userRepository.save(user);
    return user;
  }
}
```

### With Explicit UoW Parameter

You can pass the UoW instance directly to the decorator instead of relying on constructor injection:

```typescript theme={null}
class CreateUserUseCase {
  constructor(private readonly userRepository: UserRepository) {}

  @Transactional(myUnitOfWork)
  async execute(input: CreateUserInput): Promise<User> {
    // Uses the explicitly provided UoW
    const user = new User({ ...input, posts: [] });
    await this.userRepository.save(user);
    return user;
  }
}
```

### UoW Resolution Order

The decorator looks for the UoW instance in this order:

1. **Decorator parameter** - `@Transactional(myUow)`
2. **Instance property** - `this.uow`
3. **Private property** - `this._uow`
4. **Any property** - Any property that is a `PrismaUnitOfWork` instance

```typescript theme={null}
// Option 1: Via decorator parameter
@Transactional(globalUow)
async method1() { ... }

// Option 2: Via constructor (recommended)
class MyService {
  constructor(private readonly uow: PrismaUnitOfWork) {}

  @Transactional()
  async method2() { ... }
}

// Option 3: Via any instance property
class MyService {
  unitOfWork = new PrismaUnitOfWork(prisma);

  @Transactional()
  async method3() { ... }
}
```

### Behavior

| Scenario               | Behavior                |
| ---------------------- | ----------------------- |
| Direct call            | Creates new transaction |
| Already in transaction | Reuses existing one     |
| Error thrown           | Automatic rollback      |

***

## PrismaRepository

Base class for repositories with full Criteria support.

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

abstract class PrismaRepository<TDomain, TPersistence, TContext = PrismaClientLike> {
  // Required: Prisma model name
  protected abstract get model(): string;

  // Required: Search query generator
  protected abstract generateSearchQuery(search: string): any[];

  // Optional: relations to include
  protected readonly includes: Record<string, any> = {};

  // Built-in methods
  async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
  async findById(id: string): Promise<TDomain | null>;
  async findManyByIds(ids: string[]): Promise<TDomain[]>;
  async count(criteria?: Criteria<TDomain>): Promise<number>;
  async exists(id: string): Promise<boolean>;
  async save(entity: TDomain): Promise<void>;
  async delete(entity: TDomain): Promise<void>;
  async deleteById(id: string): Promise<void>;
  async transaction<T>(work: () => Promise<T>): Promise<T>;
}
```

### Complete Implementation

```typescript theme={null}
import {
  PrismaRepository,
  PrismaUnitOfWork,
  PrismaOutboxStore,
} from "@woltz/rich-domain-prisma";

class UserRepository extends PrismaRepository<User, UserRecord> {
  protected readonly model = "user";
  protected readonly includes = {
    posts: true,
    profile: true,
  };

  constructor(
    prisma: PrismaClient,
    uow: PrismaUnitOfWork,
    outboxStore?: PrismaOutboxStore
  ) {
    super(
      new UserToPersistenceMapper(prisma, uow),
      new UserToDomainMapper(),
      prisma,
      uow,
      outboxStore
    );
  }
  
  // Required: implement search query generation
  protected generateSearchQuery(search: string): any[] {
    return [
      { name: { contains: search, mode: "insensitive" } },
      { email: { contains: search, mode: "insensitive" } },
    ];
  }

  // Custom query methods
  async findByEmail(email: string): Promise<User | null> {
    const data = await this.context.user.findUnique({
      where: { email },
      include: this.includes,
    });
    return data ? this.toDomainMapper.build(data) : null;
  }
}
```

### Context Awareness

The repository automatically uses the transaction context when available:

```typescript theme={null}
class UserRepository extends PrismaRepository<User, UserRecord> {
  // Use this.context for transaction-aware queries
  async customQuery(): Promise<User[]> {
    const data = await this.context.user.findMany({
      where: { status: "active" },
      include: this.includes,
    });
    return data.map((d) => this.toDomainMapper.build(d));
  }
}
```

### Transactional Outbox

Pass an optional **`outboxStore`** as the fifth argument to `PrismaRepository`'s constructor. When set, `save()` automatically persists uncommitted domain events to the outbox table in the **same database transaction** as the aggregate write.

| Property      | Type                | Required | Description                                    |
| ------------- | ------------------- | -------- | ---------------------------------------------- |
| `outboxStore` | `PrismaOutboxStore` | No       | Enables auto-save of domain events on `save()` |

Use `PrismaOutboxStore` from `@woltz/rich-domain-prisma`. For the full setup (table migration, event bus decorator, background publisher), see **[Transactional Outbox](/integrations/outbox)**.

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

const outboxStore = new PrismaOutboxStore(prisma);

class OrderRepository extends PrismaRepository<Order, OrderRecord> {
  constructor(prisma: PrismaClient, uow: PrismaUnitOfWork) {
    super(
      new OrderToPersistenceMapper(prisma, uow),
      new OrderToDomainMapper(),
      prisma,
      uow,
      outboxStore // ← events saved atomically with the aggregate
    );
  }
}
```

***

## EntitySchemaRegistry

Maps domain entities to database tables, handles field mapping, and configures relationships.

<Info>
  See the complete [Schema Registry documentation](/repository/schema-registry) for all features.
</Info>

### Basic Registration

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

const registry = new EntitySchemaRegistry()
  .register({
    entity: "User",
    table: "user",
  })
  .register({
    entity: "Post",
    table: "post",
    fields: {
      content: "main_content", // domain → database mapping
    },
    parentFk: {
      field: "authorId",
      parentEntity: "User",
    },
  });
```

### Collection Configuration (N:N Relations)

For N:N relationships, configure collections with `type: "reference"`:

```typescript theme={null}
const registry = new EntitySchemaRegistry()
    .register({
      entity: "User",
      table: "user",
      collections: {
        // 'owned': Children entities that belong exclusively to this aggregate
        // They are created/deleted with the parent
        posts: {
          type: "owned",
          entity: "Post",
        },
      },
    })
    .register({
      entity: "Post",
      table: "post",
      // Parent foreign key for owned entities
      parentFk: {
        field: "authorId",
        parentEntity: "User",
      },
      collections: {
        // Field name related to the relationship in the domain;
        // 'posts.tags' <- Domain Relation field name is 'tags'
        tags: {
          // 'reference': Independent entities connected via junction table
          // They exist independently and are only linked/unlinked
          type: "reference",
          entity: "Tag",
          // Optional: set when the Prisma relation field name differs from the domain property name.
          // e.g. domain property: "tags", Prisma schema field: "post_tags"
          // relationName: "post_tags",
          // Junction config required for manually created pivot tables
          junction: {
            table: "tagPost",    // Pivot table name
            sourceKey: "postId", // FK to Post
            targetKey: "tagId",  // FK to Tag
          },
        },
      },
    });
```

### Collection Types

| Type        | Relationship | Batch Behavior              |
| ----------- | ------------ | --------------------------- |
| `owned`     | 1:N          | `createMany` / `deleteMany` |
| `reference` | N:N          | `connect` / `disconnect`    |

The `PrismaBatchExecutor` automatically uses the correct Prisma operations based on collection type:

```typescript theme={null}
// For 'owned' collections (1:N):
// - Creates use: prisma.post.createMany({ data: [...] })
// - Deletes use: prisma.post.deleteMany({ where: { id: { in: [...] } } })

// For 'reference' collections (N:N):
// - Creates use: prisma.post.update({ data: { tags: { connect: [...] } } })
// - Deletes use: prisma.post.update({ data: { tags: { disconnect: [...] } } })
// Note: if the Prisma relation field name differs from the domain property name,
// set `relationName` in the collection config so the executor uses the correct field.
```

***

## PrismaToPersistence

Base mapper class with change tracking integration.

```typescript theme={null}
import { PrismaToPersistence } from "@woltz/rich-domain-prisma";
import { EntitySchemaRegistry, AggregateChanges } from "@woltz/rich-domain";

abstract class PrismaToPersistence<TDomain, PrismaClient = PrismaClientLike> extends Mapper<TDomain, void> {
  // Required: registry for field mapping
  protected abstract readonly registry: EntitySchemaRegistry;

  // Required: handle entity creation
  protected abstract onCreate(entity: TDomain): Promise<void>;

  // Optional: override for custom update logic (default uses PrismaBatchExecutor)
  protected async onUpdate(
    changes: AggregateChanges,
    entity: TDomain
  ): Promise<void>;

  // Returns transaction client when inside a transaction, otherwise the prisma client
  protected get context(): PrismaClient;
}
```

### Complete Example

```typescript theme={null}
class UserToPersistenceMapper extends PrismaToPersistence<User> {
  protected readonly registry = new EntitySchemaRegistry()
    .register({ 
      entity: "User", 
      table: "user",
      collections: {
        posts: { type: "owned" },
        tags: { type: "reference", entity: "Tag" },
      },
    })
    .register({
      entity: "Post",
      table: "post",
      parentFk: { field: "authorId", parentEntity: "User" },
    });

  protected async onCreate(user: User): Promise<void> {
    await this.context.user.create({
      data: {
        id: user.id.value,
        name: user.name,
        email: user.email,
        posts: {
          createMany: {
            data: user.posts.map((p) => ({
              id: p.id.value,
              title: p.title,
              content: p.content,
              authorId: user.id.value,
            })),
          },
        },
        // N:N - connect existing tags
        tags: {
          connect: user.tags.map((t) => ({ id: t.id.value })),
        },
      },
    });
  }

  // onUpdate uses PrismaBatchExecutor by default — override only if needed
}
```

When overriding, use [`changes.without()`](/core/change-tracking#excluding-entities) to persist one entity manually and pass the remaining changes to `super.onUpdate()`.

***

## PrismaBatchExecutor

Executes batch operations from `AggregateChanges` with proper ordering and relationship handling.

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

const executor = new PrismaBatchExecutor(context, {
  registry: schemaRegistry,
});

await executor.execute(changes);
```

### Execution Order

The executor respects referential integrity:

1. **Deletes** - Leaf → Root (depth DESC)
   * `owned`: Uses `deleteMany`
   * `reference`: Uses `disconnect`

2. **Creates** - Root → Leaf (depth ASC)
   * `owned`: Uses `createMany`
   * `reference`: Uses `connect`

3. **Updates** - Any order

Batch executors and repositories resolve the correct PK column via `registry.getPrimaryKeyField()` and `registry.buildWhereById()`. Configure `primaryKey` when a table does not use an `id` column (e.g. `factoryProfile.factoryId`). Remember: `primaryKey` names the **database column**; the value always comes from `entity.id`, not from a domain property with the same name.

### Convenience Function

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

// Shorthand for simple cases
await executeBatch(context, changes, {
  registry: schemaRegistry,
});
```

***

## Complete Example

### Domain Model

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

// Post Entity
const postSchema = z.object({
  id: z.custom<Id>((v) => v instanceof Id),
  title: z.string().min(1),
  content: z.string(),
  published: z.boolean().default(false),
});

class Post extends Entity<z.infer<typeof postSchema>> {
  protected static validation: EntityValidation<z.infer<typeof postSchema>> = {
    schema: postSchema,
  };

  get title() { return this.props.title; }
  get content() { return this.props.content; }
  get published() { return this.props.published; }

  publish() {
    this.props.published = true;
  }
}

// Tag Entity (for N:N reference)
class Tag extends Entity<{ id: Id; name: string }> {
  get name() { return this.props.name; }
}

// User Aggregate
const userSchema = z.object({
  id: z.custom<Id>((v) => v instanceof Id),
  name: z.string().min(2),
  email: z.string().email(),
  posts: z.array(z.custom<Post>((v) => v instanceof Post)),
  tags: z.array(z.custom<Tag>((v) => v instanceof Tag)),
});

class User extends Aggregate<z.infer<typeof userSchema>> {
  protected static validation = { schema: userSchema };

  get name() { return this.props.name; }
  set name(value: string) { this.props.name = value; }
  get email() { return this.props.email; }
  get posts() { return this.props.posts; }
  get tags() { return this.props.tags; }

  addPost(post: Post) {
    this.props.posts.push(post);
  }

  removePost(postId: Id) {
    const index = this.props.posts.findIndex((p) => p.id.equals(postId));
    if (index !== -1) this.props.posts.splice(index, 1);
  }

  addTag(tag: Tag) {
    if (!this.props.tags.some((t) => t.id.equals(tag.id))) {
      this.props.tags.push(tag);
    }
  }

  removeTag(tagId: Id) {
    const index = this.props.tags.findIndex((t) => t.id.equals(tagId));
    if (index !== -1) this.props.tags.splice(index, 1);
  }
}
```

### Schema Registry

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

const schemaRegistry = new EntitySchemaRegistry()
  .register({
    entity: "User",
    table: "user",
    collections: {
      posts: { type: "owned" },
      tags: { type: "reference", entity: "Tag" },
    },
  })
  .register({
    entity: "Post",
    table: "post",
    parentFk: { field: "authorId", parentEntity: "User" },
  })
  .register({
    entity: "Tag",
    table: "tag",
  });
```

### Mappers

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

// Domain Mapper
class UserToDomainMapper extends Mapper<UserRecord, User> {
  build(record: UserRecord): User {
    return new User({
      id: Id.from(record.id),
      name: record.name,
      email: record.email,
      posts: record.posts.map(
        (p) => new Post({
          id: Id.from(p.id),
          title: p.title,
          content: p.content,
          published: p.published,
        })
      ),
      tags: record.tags.map(
        (t) => new Tag({ id: Id.from(t.id), name: t.name })
      ),
    });
  }
}

// Persistence Mapper
class UserToPersistenceMapper extends PrismaToPersistence<User> {
  protected readonly registry = schemaRegistry;

  protected async onCreate(user: User): Promise<void> {
    await this.context.user.create({
      data: {
        id: user.id.value,
        name: user.name,
        email: user.email,
        posts: {
          createMany: {
            data: user.posts.map((p) => ({
              id: p.id.value,
              title: p.title,
              content: p.content,
              published: p.published,
              authorId: user.id.value,
            })),
          },
        },
        tags: {
          connect: user.tags.map((t) => ({ id: t.id.value })),
        },
      },
    });
  }

  // onUpdate uses PrismaBatchExecutor by default — override only if needed
}
```

### Repository

```typescript theme={null}
import {
  PrismaRepository,
  PrismaUnitOfWork,
  PrismaOutboxStore,
} from "@woltz/rich-domain-prisma";

class UserRepository extends PrismaRepository<User, UserRecord> {
  protected readonly model = "user";
  protected readonly includes = { posts: true, tags: true };

  constructor(prisma: PrismaClient, uow: PrismaUnitOfWork) {
    super(
      new UserToPersistenceMapper(prisma, uow),
      new UserToDomainMapper(),
      prisma,
      uow
    );
  }

  protected generateSearchQuery(search: string): any[] {
    return [
      { name: { contains: search, mode: "insensitive" } },
      { email: { contains: search, mode: "insensitive" } },
    ];
  }

  async findByEmail(email: string): Promise<User | null> {
    const data = await this.context.user.findUnique({
      where: { email },
      include: this.includes,
    });
    return data ? this.toDomainMapper.build(data) : null;
  }
}
```

### Use Case

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

class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly uow: PrismaUnitOfWork
  ) {}

  @Transactional()
  async addTagToUser(userId: string, tag: Tag): Promise<User> {
    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new EntityNotFoundError("User", userId);
    }

    user.addTag(tag); // N:N reference
    await this.userRepository.save(user); // Uses connect

    return user;
  }

  @Transactional()
  async removeTagFromUser(userId: string, tagId: string): Promise<User> {
    const user = await this.userRepository.findById(userId);
    if (!user) {
      throw new EntityNotFoundError("User", userId);
    }

    user.removeTag(Id.from(tagId)); // N:N reference
    await this.userRepository.save(user); // Uses disconnect

    return user;
  }
}
```

***

## Error Handling

The adapter provides specific error types:

```typescript theme={null}
import {
  PrismaRepositoryError,
  ModelNotFoundError,
  TableNotFoundError,
  NoRecordsAffectedError,
  BatchOperationError,
} from "@woltz/rich-domain-prisma";

try {
  await userRepository.save(user);
} catch (error) {
  if (error instanceof ModelNotFoundError) {
    // Model not in Prisma schema
  } else if (error instanceof TableNotFoundError) {
    // Entity not in registry
  } else if (error instanceof BatchOperationError) {
    // Batch operation failed
  }
}
```

***

## Complete Example

See the [fastify-with-prisma example](https://github.com/tarcisioandrade/rich-domain/tree/main/examples/backend/fastify-with-prisma) for a complete working application demonstrating:

* User aggregate with Posts (1:N owned)
* Post with Tags (N:N reference via junction table)
* Case-insensitive search
* Transaction management
* CRUD operations
* Domain events with BullMQ

***

## API Reference

### Exports

```typescript theme={null}
// Unit of Work
export { PrismaUnitOfWork, Transactional, getCurrentPrismaContext };
export type { PrismaTransactionContext, PrismaClientLike, PrismaTransactionClient };

// Repository
export { PrismaRepository };
export type { PrismaRepositoryConfig };

// Outbox
export { PrismaOutboxStore, PRISMA_OUTBOX_SCHEMA };

// Mapper
export { PrismaToPersistence };

// Batch Executor
export { PrismaBatchExecutor, executeBatch };
export type { BatchExecutorConfig };

// Errors
export {
  PrismaRepositoryError,
  ModelNotFoundError,
  TableNotFoundError,
  NoRecordsAffectedError,
  BatchOperationError,
  OutboxStoreError,
};
```

### PrismaRepositoryConfig

```typescript theme={null}
interface PrismaRepositoryConfig {
  prisma: PrismaClientLike;
  uow: PrismaUnitOfWork;
  /** Optional — auto-save domain events on `save()`. See [Transactional Outbox](/integrations/outbox). */
  outboxStore?: PrismaOutboxStore;
}
```

The repository constructor accepts the same options positionally: mappers, `prisma`, `uow`, and optional `outboxStore`.

### PrismaUnitOfWork Methods

| Method                | Returns                            | Description                 |
| --------------------- | ---------------------------------- | --------------------------- |
| `transaction(work)`   | `Promise<T>`                       | Execute work in transaction |
| `isInTransaction()`   | `boolean`                          | Check if in transaction     |
| `getCurrentContext()` | `PrismaTransactionContext \| null` | Get current context         |

### PrismaRepository Methods

| Method               | Returns                       | Description            |
| -------------------- | ----------------------------- | ---------------------- |
| `find(criteria)`     | `Promise<PaginatedResult<T>>` | Find with criteria     |
| `findById(id)`       | `Promise<T \| null>`          | Find by ID             |
| `findManyByIds(ids)` | `Promise<T[]>`                | Find multiple by IDs   |
| `count(criteria?)`   | `Promise<number>`             | Count entities         |
| `exists(id)`         | `Promise<boolean>`            | Check if exists        |
| `save(entity)`       | `Promise<void>`               | Create or update       |
| `delete(entity)`     | `Promise<void>`               | Delete entity          |
| `deleteById(id)`     | `Promise<void>`               | Delete by ID           |
| `transaction(work)`  | `Promise<T>`                  | Execute in transaction |
