npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@woltz/rich-domain-prisma

v0.7.7

Published

Prisma integration for Rich Domain Library

Downloads

238

Readme

@woltz/rich-domain-prisma

Prisma adapter for @woltz/rich-domain. Provides plug and play integration between rich-domain and Prisma ORM.

Installation

npm install @woltz/rich-domain @woltz/rich-domain-prisma

Features

  • Unit of Work with AsyncLocalStorage (request isolation)
  • PrismaRepository base class with Criteria support
  • PrismaToPersistence base class with change tracking
  • @Transactional decorator for automatic transactions
  • BatchExecutor for batch change operations
  • Zero config - works out of the box

Quick Start

1. Setup

import { PrismaClient } from "@prisma/client";
import { PrismaUnitOfWork } from "@woltz/rich-domain-prisma";

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

2. Create Repository

import { PrismaRepository } from "@woltz/rich-domain-prisma";

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

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

3. Use It

const userRepository = new UserRepository(prisma, uow);

// Create
const user = User.create({ name: "John", email: "[email protected]", posts: [] });
await userRepository.save(user);

// Find by ID
const found = await userRepository.findById(user.id.value);

// 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 (automatic change tracking)
found.updateName("John Updated");
found.addPost(new Post({ ... }));
await userRepository.save(found); // Detects and applies only what changed

// Delete
await userRepository.delete(found);

API Reference

PrismaUnitOfWork

Manages transactions with per-request isolation using AsyncLocalStorage.

const uow = new PrismaUnitOfWork(prisma);

// Execute in transaction
await uow.transaction(async () => {
  await userRepository.save(user);
  await orderRepository.save(order);
  // All or nothing - rolls back on failure
});

// Check if in transaction
uow.isInTransaction(); // boolean

// Get current context
uow.getCurrentContext(); // PrismaTransactionContext | null

Request Isolation

AsyncLocalStorage ensures each HTTP request has its own transaction context:

// Request 1: starts transaction
await uow.transaction(async () => {
  // ...
});

// Request 2: NOT affected by Request 1's transaction
await userRepository.findById(id); // Uses normal connection

PrismaRepository

Base class for repositories with Criteria support.

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

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

  // Available methods
  async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
  async findById(id: string): Promise<TDomain | null>;
  async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null>;
  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 Example

import { PrismaRepository, PrismaUnitOfWork } from "@woltz/rich-domain-prisma";
import { Criteria } from "@woltz/rich-domain";

class UserRepository extends PrismaRepository<User, UserPersistence> {
  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
    );
  }

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

  async findActiveUsers(): Promise<User[]> {
    const criteria = Criteria.create<User>()
      .where("status", "equals", "active")
      .orderBy("createdAt", "desc");

    const result = await this.find(criteria);
    return result.data;
  }
}

PrismaToPersistence

Base class for persistence mappers with change tracking support.

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

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

  // Required: implement update
  protected abstract onUpdate(
    changes: AggregateChanges,
    entity: TDomain
  ): Promise<void>;

  // Available: current context (transaction or prisma)
  protected get context(): PrismaClient | Transaction;
}

Complete Example

import type { PrismaClient } from "@prisma/client";
import {
  PrismaToPersistence,
  PrismaBatchExecutor,
} from "@woltz/rich-domain-prisma";
import { EntitySchemaRegistry, AggregateChanges } from "@woltz/rich-domain";

const schemaRegistry = new EntitySchemaRegistry()
  .register({
    entity: "User",
    table: "user",
  })
  .register({
    entity: "Post",
    table: "post",
    fields: {
      content: "main_content", // Field with different name in database
    },
    parentFk: {
      field: "authorId",
      parentEntity: "User",
    },
  });

class UserToPersistenceMapper extends PrismaToPersistence<User, PrismaClient> {
  protected readonly registry = schemaRegistry;

  protected async onCreate(user: User): Promise<void> {
    await this.context.user.create({
      data: {
        id: user.id.value,
        email: user.email,
        name: user.name,
        createdAt: user.createdAt,
        updatedAt: user.updatedAt,
        posts: user.posts.length
          ? {
              createMany: {
                data: user.posts.map((post) => ({
                  id: post.id.value,
                  title: post.title,
                  main_content: post.content,
                  published: post.published,
                  authorId: user.id.value,
                  createdAt: post.createdAt,
                  updatedAt: post.updatedAt,
                })),
              },
            }
          : undefined,
      },
    });
  }

  protected async onUpdate(
    changes: AggregateChanges,
    user: User
  ): Promise<void> {
    const executor = new PrismaBatchExecutor(this.context, {
      registry: this.registry,
    });

    await executor.execute(changes);
  }
}

@Transactional Decorator

Decorator that automatically wraps a method in a transaction.

import { Transactional } from "@woltz/rich-domain-prisma";

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

  @Transactional()
  async execute(input: CreateUserInput): Promise<User> {
    // Everything here runs in a transaction automatically
    const existing = await this.userRepository.findByEmail(input.email);
    if (existing) {
      throw new Error("User already exists");
    }

    const user = User.create({ ...input, posts: [] });
    await this.userRepository.save(user);

    return user;
  }
}

Behavior

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

Requirements

The class must have a uow property of type PrismaUnitOfWork:

class MyService {
  constructor(
    private readonly uow: PrismaUnitOfWork // ✅ Found by decorator
  ) {}

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

PrismaBatchExecutor

Executes batch operations from AggregateChanges.

import { PrismaBatchExecutor } from "@woltz/rich-domain-prisma";

const executor = new PrismaBatchExecutor(context, {
  // Registry for table/field mapping
  registry: schemaRegistry,
});

await executor.execute(changes);

Execution Order

The executor respects the correct order for referential integrity:

  1. Deletes - Leaf → Root (depth DESC)
  2. Creates - Root → Leaf (depth ASC)
  3. Updates - Any order

The executor uses the registry's mapEntity() for creates and mapFields() for updates.


Recipes

Use Case with Transaction

class TransferMoneyUseCase {
  constructor(
    private readonly accountRepository: AccountRepository,
    private readonly uow: PrismaUnitOfWork
  ) {}

  @Transactional()
  async execute(input: { from: string; to: string; amount: number }) {
    const fromAccount = await this.accountRepository.findById(input.from);
    const toAccount = await this.accountRepository.findById(input.to);

    if (!fromAccount || !toAccount) {
      throw new Error("Account not found");
    }

    fromAccount.withdraw(input.amount);
    toAccount.deposit(input.amount);

    await this.accountRepository.save(fromAccount);
    await this.accountRepository.save(toAccount);

    // If any operation fails, everything is rolled back
  }
}

Repository with Custom Methods

class OrderRepository extends PrismaRepository<Order> {
  protected readonly model = "order";
  protected readonly includes = {
    items: true,
    customer: true,
  };

  async findByCustomerId(customerId: string): Promise<Order[]> {
    const data = await this.modelAccessor.findMany({
      where: { customerId },
      include: this.includes,
      orderBy: { createdAt: "desc" },
    });

    return data.map((item) => this.mapperToDomain.build(item));
  }

  async findPendingOrders(): Promise<Order[]> {
    const criteria = Criteria.create<Order>()
      .where("status", "equals", "pending")
      .where("createdAt", "greaterThan", subDays(new Date(), 7));

    const result = await this.find(criteria);
    return result.data;
  }
}

Mapper with Complex Relations

class OrderToPersistenceMapper extends PrismaToPersistence<
  Order,
  PrismaClient
> {
  protected readonly registry = new EntitySchemaRegistry()
    .register({ entity: "Order", table: "order" })
    .register({
      entity: "OrderItem",
      table: "orderItem",
      parentFk: { field: "orderId", parentEntity: "Order" },
    })
    .register({
      entity: "OrderItemAddon",
      table: "orderItemAddon",
      parentFk: { field: "orderItemId", parentEntity: "OrderItem" },
    });

  protected async onCreate(order: Order): Promise<void> {
    await this.context.order.create({
      data: {
        id: order.id.value,
        customerId: order.customerId,
        status: order.status,
        total: order.total,
        items: {
          create: order.items.map((item) => ({
            id: item.id.value,
            productId: item.productId,
            quantity: item.quantity,
            price: item.price,
            addons: {
              create: item.addons.map((addon) => ({
                id: addon.id.value,
                name: addon.name,
                price: addon.price,
              })),
            },
          })),
        },
      },
    });
  }

  protected async onUpdate(
    changes: AggregateChanges,
    order: Order
  ): Promise<void> {
    const executor = new PrismaBatchExecutor(this.context, {
      registry: this.registry,
    });

    await executor.execute(changes);
  }
}

Testing

Mocking the UnitOfWork

import { PrismaUnitOfWork } from "@woltz/rich-domain-prisma";

describe("CreateUserUseCase", () => {
  let mockUow: jest.Mocked<PrismaUnitOfWork>;
  let mockUserRepository: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockUow = {
      transaction: jest.fn((work) => work()),
      getCurrentContext: jest.fn(),
      isInTransaction: jest.fn(),
    } as any;

    mockUserRepository = {
      save: jest.fn(),
      findByEmail: jest.fn(),
    } as any;
  });

  it("should create user", async () => {
    mockUserRepository.findByEmail.mockResolvedValue(null);

    const useCase = new CreateUserUseCase(mockUserRepository, mockUow);
    const user = await useCase.execute({
      name: "John",
      email: "[email protected]",
    });

    expect(user.name).toBe("John");
    expect(mockUserRepository.save).toHaveBeenCalled();
  });
});

License

MIT