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

@roastery/beans

v0.1.2

Published

DDD building blocks for the Roastery CMS ecosystem — Entities, Value Objects, DTOs, Schemas, and Mappers.

Readme

@roastery/beans

DDD building blocks for the Roastery CMS ecosystem — base abstractions for Entities, Value Objects, DTOs, Schemas, and Mappers.

Checked with Biome

Overview

beans provides the foundational primitives for building domain models in TypeScript:

  • Entity — Abstract base class with built-in id (UUID v7), createdAt, and updatedAt fields, using Symbol-based metadata to avoid property collisions.
  • ValueObject — Immutable, validated wrapper around a value. Throws InvalidPropertyException on invalid data.
  • Mapper — Bidirectional conversion between Entities and DTOs, with schema validation on output.
  • Collections — Ready-to-use Value Objects, DTOs, and Schemas for common types (UUID, email, slug, datetime, etc.).

Technologies

| Tool | Purpose | |------|---------| | @roastery/terroir | Schema validation, exception hierarchy, and TypeBox re-exports | | TypeBox | Runtime schema validation and TypeScript type inference | | slugify | URL-safe slug generation | | tsup | Bundling to ESM + CJS with .d.ts generation | | Bun | Runtime, test runner, and package manager | | Knip | Unused exports and dependency detection | | Husky + commitlint | Git hooks and conventional commit enforcement |

Installation

Install the package and its peer dependencies:

bun add @roastery/beans @roastery/terroir typescript

Or install them separately:

# Install the library
bun add @roastery/beans

# Install peer dependencies
bun add @roastery/terroir typescript

Local development (link)

If you're developing beans alongside another project, you can link it locally:

# Inside the beans directory
bun run setup  # builds and registers the link

# Inside your consuming project
bun link @roastery/beans

Entity

Abstract base class that every domain entity extends. Provides id, createdAt, and updatedAt out of the box, validated through Value Objects.

import { Entity } from "@roastery/beans";
import { EntitySchema, EntityContext, EntityFactory } from "@roastery/beans/entity/symbols";
import type { EntityDTO } from "@roastery/beans/entity/dtos";
import { makeEntity } from "@roastery/beans/entity/factories";
import { Schema } from "@roastery/terroir/schema";
import { t } from "@roastery/terroir";
import { UuidDTO, DateTimeDTO, StringDTO, SlugDTO } from "@roastery/beans/collections";
import { DefinedStringVO, SlugVO } from "@roastery/beans/collections";

// 1. Define the schema
const PostDTO = t.Object({
  id: UuidDTO,
  createdAt: DateTimeDTO,
  updatedAt: t.Optional(DateTimeDTO),
  title: StringDTO,
  slug: SlugDTO,
});

const PostSchema = Schema.make(PostDTO);
type PostInput = { title: string; slug: string };

// 2. Implement the entity
class Post extends Entity<typeof PostDTO> {
  public readonly [EntitySchema] = PostSchema;

  private _title: DefinedStringVO;
  private _slug: SlugVO;

  public get title(): string {
    return this._title.value;
  }

  public get slug(): string {
    return this._slug.value;
  }

  public constructor(data: EntityDTO & PostInput) {
    super(data, "post");
    this._title = DefinedStringVO.make(data.title, this[EntityContext]("title"));
    this._slug = SlugVO.make(data.slug, this[EntityContext]("slug"));
  }

  public [EntityFactory](data: PostInput, initialProperties?: EntityDTO): this {
    return new Post({ ...(initialProperties ?? makeEntity()), ...data }) as this;
  }
}

The entity-type tag ("post") is passed as the second argument to super(...) and stored under this[EntitySource] by the base class — there is no need for a [EntitySource] class field on the subclass. The [EntitySchema] field is abstract on Entity, so every subclass must bind it to its own Schema.make(...) instance. [EntityFactory] is abstract too — every subclass implements it so generic consumers holding an entity instance (e.g. EntityUpdater) can rebuild it without a separately-wired factory function.

Symbols

| Symbol | Purpose | |--------|---------| | EntitySource | Identifies the entity origin (e.g. "post", "user") | | EntitySchema | Holds the entity's validation Schema instance | | EntityContext | Returns IValueObjectMetadata for a given property name | | EntityStorage | Accessor to the entity's internal key-value store | | EntityFactory | Abstract self-rebuild method: (data, initialProperties?) => this |

EntityStorage

Each entity instance has a built-in key-value store (string → string) accessible via the [EntityStorage] protected getter. Useful for storing transient, non-domain state inside an entity without exposing extra public properties.

import { EntityStorage } from "@roastery/beans/entity/symbols";

class Post extends Entity<typeof PostDTO> {
  // ...

  public addTag(tag: string): void {
    const current = this[EntityStorage].get("tags") ?? "";
    this[EntityStorage].set("tags", current ? `${current},${tag}` : tag);
  }

  public getTags(): string[] {
    return (this[EntityStorage].get("tags") ?? "").split(",").filter(Boolean);
  }
}

The storage API is intentionally minimal:

| Method | Description | |--------|-------------| | get(key) | Returns the value or null if the key does not exist | | set(key, value) | Stores a value under the given key | | del(key) | Removes the entry for the given key |

AutoUpdate decorator

Automatically calls entity.update() after a method executes, setting updatedAt to the current timestamp.

import { AutoUpdate } from "@roastery/beans/entity/decorators";

class Post extends Entity<typeof PostDTO> {
  // ...

  @AutoUpdate
  public rename(title: string): void {
    this._title = DefinedStringVO.make(title, this[EntityContext]("title"));
    this._slug = SlugVO.make(title, this[EntityContext]("slug"));
  }
}

Entity factory

Generates fresh entity base data for testing:

import { makeEntity } from "@roastery/beans/entity/factories";

const data = makeEntity();
// { id: "<uuid-v7>", createdAt: "<iso-string>", updatedAt: undefined }

EntityUpdater

Applies field-level updates by round-tripping the entity through the Mapper: serialize to DTO, mutate the requested field, validate against the entity's schema, stamp updatedAt, and rebuild via the entity's own [EntityFactory] method. Rebuilding (instead of mutating in place) keeps every update flowing through the same value-object validation as initial construction. Successive updates accumulate — each effective call replaces the managed instance, exposed via updater.current.

import { EntityUpdater } from "@roastery/beans/entity";

// Post implements `[EntityFactory]` — see the Entity section above.
const updater = new EntityUpdater(post);
const renamed = updater.run("title", "New title");
// renamed.updatedAt is freshly stamped; id/createdAt are preserved.
updater.current === renamed; // true

Guarantees:

  • No-op updates don't stamp. A value that is structurally equal to the current one — or that a value-object normalises back into it (e.g. SlugVO turning "Hello World" into the current "hello-world") — returns the same instance with updatedAt untouched.
  • Invalid values are rejected before [EntityFactory] runs. The mutated DTO is matched against the entity's [EntitySchema]; a mismatch throws InvalidDomainDataException.
  • Identity is enforced. run only accepts domain fields (id/createdAt/updatedAt are excluded at the type level and rejected at runtime), and an [EntityFactory] implementation that ignores its initialProperties argument — regenerating id/createdAt — makes run throw OperationFailedException.

Limitation. Rebuilding goes through the DTO, so transient state the mapper excludes — [EntityStorage] contents and __-prefixed fields — resets to constructor defaults on every effective update.


ValueObject

Immutable wrapper around a value with schema validation. All Value Objects follow the same pattern: extend ValueObject, implement schema, and expose a static make() factory.

import { ValueObject } from "@roastery/beans";
import type { IValueObjectMetadata } from "@roastery/beans/value-object";
import { Schema } from "@roastery/terroir/schema";
import { StringDTO } from "@roastery/beans/collections";
import { StringSchema } from "@roastery/beans/collections";

class FullName extends ValueObject<string, typeof StringDTO> {
  protected override schema = StringSchema;

  public static make(value: string, info: IValueObjectMetadata): FullName {
    const vo = new FullName(value, info);
    vo.validate(); // throws InvalidPropertyException if invalid
    return vo;
  }
}

Mapper

Bidirectional conversion between Entities and DTOs. toDTO validates the output against the entity's schema; toDomain splits the DTO into entity props and domain data.

import { Mapper } from "@roastery/beans/mapper";

// Entity -> DTO (validated against schema)
const dto = Mapper.toDTO(post);
// { id: "...", createdAt: "...", title: "My Post", slug: "my-post" }

// DTO -> Entity (factory receives domain data + entity props separately)
const entity = Mapper.toDomain<typeof PostDTO>(dto, (data, entityProps) => {
  return new Post({ ...entityProps, title: data.title, slug: data.slug });
});

Mapping conventions

| Convention | Behavior | |------------|----------| | _property | Stripped to property in DTO output | | __property | Ignored entirely (internal metadata) | | ValueObject instances | Extracted to .value | | Schema instances | Replaced with .toString() | | Nested Entity instances | Recursively converted | | Objects with toDTO() | Calls .toDTO() recursively | | Arrays | Each element processed recursively | | null / undefined / primitives | Passed through unchanged |

Symbol-keyed properties ([EntitySource], [EntitySchema], [EntityContext], [EntityStorage]) never appear in the produced DTO because the underlying walk only iterates string keys.


Collections

Value Objects

| Class | Value type | Description | |-------|------------|-------------| | UuidVO | string | UUID with generate() for new v7 IDs | | DefinedStringVO | string | Non-empty string | | SlugVO | string | Auto-slugified string | | UrlVO | string | HTTP/HTTPS URL | | DateTimeVO | string | ISO 8601 datetime with now() factory | | BooleanVO | boolean | Boolean with truthy(), falsy(), from() helpers | | StringArrayVO | string[] | Array of strings | | UuidArrayVO | string[] | Array of UUIDs |

import { UuidVO, SlugVO, DateTimeVO, BooleanVO } from "@roastery/beans/collections";

const id = UuidVO.generate(info);       // new UUID v7
const slug = SlugVO.make("My Post", info); // "my-post"
const now = DateTimeVO.now(info);        // current ISO timestamp
const flag = BooleanVO.from(1, info);    // true

DTOs

Pre-built TypeBox definitions for common types:

| DTO | Description | |-----|-------------| | StringDTO | Non-empty string | | NumberDTO | Non-negative number | | BooleanDTO | Boolean | | DateTimeDTO | ISO 8601 date-time | | UuidDTO | UUID | | SlugDTO | URL slug | | UrlDTO | HTTP/HTTPS URL | | SimpleUrlDTO | URL with any protocol | | EmailDTO | Email address | | PasswordDTO | Password (min 7 chars, uppercase, lowercase, digit, special) | | StringArrayDTO | Array of strings | | UuidArrayDTO | Array of UUIDs | | IdObjectDTO | { id: UUID } | | SlugObjectDTO | { slug: Slug } |

import { EmailDTO, PasswordDTO, UuidDTO } from "@roastery/beans/collections";

Schemas

Each DTO has a corresponding Schema instance for runtime validation:

import { EmailSchema, PasswordSchema, UuidSchema } from "@roastery/beans/collections";

EmailSchema.match("[email protected]"); // true
EmailSchema.match("invalid");          // false

Exports reference

// Top-level pillars
import { Entity, Mapper, ValueObject } from "@roastery/beans";

// Entity subpaths
import { EntityUpdater } from "@roastery/beans/entity";
import { EntitySource, EntitySchema, EntityContext, EntityStorage, EntityFactory as EntityFactorySymbol } from "@roastery/beans/entity/symbols";
import { AutoUpdate } from "@roastery/beans/entity/decorators";
import { makeEntity } from "@roastery/beans/entity/factories";
import { deepEquals, generateUUID, slugify } from "@roastery/beans/entity/helpers";
import { ParseEntityToDTOService } from "@roastery/beans/entity/services";
import { EntitySchema as BaseEntitySchema } from "@roastery/beans/entity/schemas"; // runtime Schema for the base EntityDTO
import { EntityDTO } from "@roastery/beans/entity/dtos";
import type { EntityFactory, EntityUpdaterInput, IEntity, IRawEntity } from "@roastery/beans/entity/types";

// ValueObject metadata
import type { IValueObjectMetadata } from "@roastery/beans/value-object/types";

// Collections (single barrel for DTOs, Schemas and Value Objects)
import { UuidVO, SlugVO } from "@roastery/beans/collections";       // Value Objects
import { UuidDTO, EmailDTO } from "@roastery/beans/collections";    // DTOs
import { UuidSchema, EmailSchema } from "@roastery/beans/collections"; // Schemas

Naming note. EntitySchema exists in two forms: the symbol (under /entity/symbols) is the property key on Entity, while the homonymous runtime instance (under /entity/schemas) is Schema.make(EntityDTO) validating the base shape. The same applies to EntityStorage — symbol vs runtime class — and to EntityFactory — symbol (under /entity/symbols, keys the entity's abstract self-rebuild method) vs type (under /entity/types, the free-standing factory signature EntityUpdater used to accept before it started calling the symbol directly). The pairing is intentional; alias one when you need both in scope.


Development

# Run tests
bun run test:unit

# Run tests with coverage
bun run test:coverage

# Build for distribution
bun run build

# Check for unused exports and dependencies
bun run knip

# Full setup (build + bun link)
bun run setup

License

MIT