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

yuow

v0.1.0

Published

Generic implementation of Unit of Work and Repository patterns to build isolated domain model

Readme

Yuow

codecov npm version npm downloads/month npm downloads license

Yuow is a generic implementation of Unit of Work and Repository patterns. It's not a replacement for your current ORM, but a great addition to it especially if you use tactical DDD patterns.

With Yuow, you can build a truly isolated domain model.

  1. Quick Start
  2. Repository
  3. Run Options
  4. Versioning
  5. Engine
  6. License

Quick Start

npm install yuow

Yuow supports Knex out of the box, but you can integrate with any other database driver or ORM. In order to get started, you should implement Repository for each of your models.

This is an example of how to use Yuow with Knex:

import { Uow, UowContext } from 'yuow/core';
import { KnexEngine } from 'yuow/knex';

const uow = new Uow(new KnexEngine(/* put knex instance here */));

app.use((next) => {
  return UowContext.create(uow, next);
});

Then, in your application code, initialize the repository and use it to store your domain entity:

import { Transactional, Context } from 'yuow/core';

class OrderController {
  @Post('/orders')
  @Transactional()
  createOrder() {
    const repository = Context.getRepository(OrderRepository);

    const order = new Order({
      id: crypto.randomUUID(),
    });

    repository.add(order);
  }

  @Post('/orders/:id/cancel')
  @Transactional()
  cancelOrder(@Param('id') id: string) {
    const repository = Context.getRepository(OrderRepository);
    const order = await repository.findById(id);

    order.cancel();
  }
}

The example above assumes that you already have the Order entity and OrderRepository implemented. See the Repository section for more details on how to implement a repository.

Repository

A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection

– Martin Fowler

In Yuow, data mapper and repository responsibilities are merged together for simplicity. However, you are free to encapsulate data mapping logic into a separate class.

To create a repository, you have to extend abstract Repository class and implement 4 methods extractIdentity, flushInsert, flushUpdate and flushDelete. Also, even though it's not required, you should write your own selection methods:

import { Repository } from 'yuow/core';
import { type KnexTransaction } from 'yuow/knex';

export class OrderRepository extends Repository<Order, KnexTransaction> {
  async find(id: string) {
    // Implement
  }

  protected extractIdentity(order: Order) {
    // Implement
  }

  protected flushInsert(order: Order) {
    // Implement
  }

  protected flushUpdate(order: Order) {
    // Implement
  }

  protected flushDelete(order: Order) {
    // Implement
  }
}

extractIdentity

To emulate a collection-like behavior, a repository uses the Identity Map pattern to keep identity <–> entity references. Since, with Yuow, your domain model can live truly isolated, it's necessary to provide the repository with information on how to extract identity from your entity:

protected extractIdentity(order: Order) {
  return order.id;
}

Selection

To load an entity from database, you should create a method that hydrates your entity and returns it. Usually, it's enough to have a single method that returns an entity by its identity, but you can implement any selection methods you need.

In this example, we create a find method that returns an Order entity or undefined:

async find(id: string): Promise<Order | undefined> {
  // 1. Request a record from database
  const record = await this.knex
    .select('*')
    .from('orders')
    .where('id', id)
    .first();

  // 2. Return undefined if a record was not found
  if (!record) {
    return;
  }

  // 3. Hydrate Order entity
  const order = new Order({
    id: record.id,
    name: record.name,
  });

  // 4. Store entity in identity map and return
  return this.changeTracker.getTrackedOrTrack(order, EntityState.LOADED);
}

flushInsert, flushDelete, flushUpdate

Insert, delete, and update methods are necessary to persist your domain model state.

These methods are fairly straightforward and structurally similar. The example above lacks concurrency control logic, so it's up to you to implement if needed. You can use database mechanisms like row-level locks or implement versioning.

async flushInsert(order: Order) {
  // 1. Insert
  const result = await this.knex
    .insert({
      id: order.id,
      name: order.name
    })
    .into('orders');

  // 2. Return result
  return (result[0] || 0) > 0;
}

async flushUpdate(order: Order) {
  // 1. Update
  const result = await this.knex('orders')
    .update({
      id: order.id,
      name: order.name,
    })
    .where('orders.id', order.id);

  // 2. Return result
  return result > 0;
}

async flushDelete(order: Order) {
  // 1. Delete
  const result = await this.knex
    .delete()
    .from('orders')
    .where('orders.id', order.id);

  // 2. Return result
  return result > 0;
}

It's necessary to always return a boolean result indicating whether the operation was successful. Depending on the result, Yuow decides whether to throw a PersistenceError and retry the operation.

Run Options

@Transactional({
  retries: 3,
  transaction: { /* transaction options */ }
})
createOrder() { /* ... */ }

retries

retries specifies how many times the unit of work must be retried before it throws an error. Retries are performed only if a PersistenceError is thrown. Check out the Repository section to learn when it's thrown.

This is useful when you use a version field for optimistic concurrency control.

transaction

transaction is an object that contains transaction options. Its structure depends on the engine you use. For example, if you use KnexEngine, it should contain the isolationLevel and global properties.

{
  isolationLevel: 'read committed' | 'repeatable read' | 'serializable',
  global: false
}

Versioning

Yuow provides a simple versioning utility to help you to handle optimistic concurrency control:

import { Repository, WeakVersionTracker } from 'yuow/core';

class OrderRepository extends Repository<Order, KnexTransaction> {
  private readonly versionTracker = new WeakVersionTracker<Order>();

  // ...
  async find(id: string): Promise<Order | undefined> {
    // 1. Request a record from database
    const record = await this.knex
      .select('*')
      .from('orders')
      .where('id', id)
      .first();

    // 2. Return undefined if a record was not found
    if (!record) {
      return;
    }

    // 3. Hydrate Order entity
    const order = new Order({
      id: record.id,
      name: record.name,
    });

    // 4. Remember its version
    this.versionTracker.setVersion(order, record.version);

    // ...
  }

  async flushInsert(order: Order) {
    const result = await this.knex
      .insert({
        id: order.id,
        name: order.name,
        version: 1, // Initial version
      })
      .into('orders');

    return (result[0] || 0) > 0;
  }

  protected flushUpdate(order: Order) {
    const version = this.versionTracker.increaseVersion(order); // Call it in order to get new increased version number

    const result = await this.knex('orders')
      .update({
        id: order.id,
        name: order.name,
        version: version,
      })
      .where('orders.id', order.id)
      .andWhere('version', version - 1);

    return result > 0;
  }

  // ...
}

Engine

An Engine in Yuow abstracts the underlying transaction mechanism, allowing you to plug in different database drivers or ORMs while keeping your domain logic decoupled from persistence details.

The Engine is responsible for creating and managing transactions, which are then used by repositories to persist changes.

Out of the box, Yuow provides a KnexEngine that integrates with Knex.js. You can use it as a starting point to implement your own engine for other database drivers or ORMs.

License

Yuow is MIT licensed.