Shared utilities, use cases, DTOs, and domain abstractions for the Roastery CMS ecosystem — reusable building blocks for entity lookup, pagination, slug validation, and plugin extensibility.
seedbed provides cross-cutting application concerns shared across Roastery services:
- FindEntityByTypeUseCase — Generic use case that resolves an entity by UUID or slug, auto-detecting the input type.
- GetNumberOfPagesService — Pagination calculator with configurable items per page.
- SlugUniquenessCheckerService — Domain service to verify slug availability.
- DTOs — Schema-validated Data Transfer Objects for common query parameters (ID/slug lookup, pagination).
- Plugin interface — Generic contract for extending services with plugins.
- Constants — Shared configuration values for pagination limits and cache expiration.
| Tool | Purpose |
|---|---|
| @roastery/terroir | Schema validation, exception hierarchy, and TypeBox re-exports |
| @roastery/beans | Entity abstractions, UUID utilities, and collection schemas |
| 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 |
Install the package and its peer dependencies:
bun add @roastery/seedbed @roastery/terroir @roastery/beans typescriptIf you're developing seedbed alongside another project, you can link it locally:
# Inside the seedbed directory
bun run setup # builds and registers the link
# Inside your consuming project
bun link @roastery/seedbedGeneric use case that resolves an entity by its identifier. It auto-detects whether the input is a UUID or slug and delegates to the appropriate repository method.
import { FindEntityByTypeUseCase } from "@roastery/seedbed/application/use-cases";
import type { ICanReadId, ICanReadSlug } from "@roastery/seedbed/domain/types/repositories";
class PostRepository implements ICanReadId<PostDTO, Post>, ICanReadSlug<PostDTO, Post> {
async findById(id: string): Promise<Post | null> { /* ... */ }
async findBySlug(slug: string): Promise<Post | null> { /* ... */ }
}
const repository = new PostRepository();
const findPost = new FindEntityByTypeUseCase(repository);
// Resolves by UUID
const post = await findPost.run("550e8400-e29b-41d4-a716-446655440000", "Post");
// Resolves by slug
const post = await findPost.run("my-first-post", "Post");
// Throws ResourceNotFoundException if not foundCalculates the total number of pages for a paginated result set.
import { GetNumberOfPagesService } from "@roastery/seedbed/application/services";
GetNumberOfPagesService.run(100, 10); // 10
GetNumberOfPagesService.run(101, 10); // 11
GetNumberOfPagesService.run(100); // 8 (default: 14 items per page)
GetNumberOfPagesService.run(0, 10); // 0Domain service that checks whether a slug is available for use.
import { SlugUniquenessCheckerService } from "@roastery/seedbed/domain/services";
const checker = new SlugUniquenessCheckerService(repository);
await checker.run("my-post"); // true (available)
await checker.run("existing-post"); // false (taken)Schema-validated Data Transfer Objects for common query parameters.
Accepts either a UUID or a slug as a path/query parameter:
import { IdOrSlugDTO } from "@roastery/seedbed/presentation/dtos";
// { "id-or-slug": "550e8400-e29b-41d4-a716-446655440000" }
// { "id-or-slug": "my-cool-post" }Pagination parameter with a minimum value of 1:
import { PaginationDTO } from "@roastery/seedbed/presentation/dtos";
// { page: 1 }Generic contract for building extensible plugin systems:
import type { IPlugin } from "@roastery/seedbed/presentation/types";
interface MyPluginValue {
handler: () => void;
}
const myPlugin: IPlugin<MyPluginValue> = {
name: "my-plugin",
value: { handler: () => console.log("Hello!") },
};import { MAX_ITEMS_PER_QUERY, CACHE_EXPIRATION_TIME } from "@roastery/seedbed/constants";| Constant | Value | Description |
|---|---|---|
MAX_ITEMS_PER_QUERY |
14 |
Default items per page for pagination |
CACHE_EXPIRATION_TIME.SAFE |
3600 |
1 hour (seconds) |
CACHE_EXPIRATION_TIME.LOW_UPDATES |
86400 |
24 hours (seconds) |
CACHE_EXPIRATION_TIME.HIGH_UPDATES |
900 |
15 minutes (seconds) |
Detects whether a string is a UUID or a slug:
import { detectEntry } from "@roastery/seedbed/application/utils";
detectEntry("550e8400-e29b-41d4-a716-446655440000"); // "UUID"
detectEntry("my-cool-post"); // "SLUG"Granular interfaces for read operations, allowing repositories to implement only what they need:
import type { ICanReadId, ICanReadSlug } from "@roastery/seedbed/domain/types/repositories";
// Find by UUID
interface ICanReadId<SchemaType, EntityType> {
findById(id: string): Promise<EntityType | null>;
}
// Find by slug
interface ICanReadSlug<SchemaType, EntityType> {
findBySlug(slug: string): Promise<EntityType | null>;
}// Application — Use Cases
import { FindEntityByTypeUseCase } from "@roastery/seedbed/application/use-cases";
// Application — Services
import { GetNumberOfPagesService } from "@roastery/seedbed/application/services";
// Application — Utilities
import { detectEntry } from "@roastery/seedbed/application/utils";
// Application — Types
import type { EntryActions, ValueTypes, ICountItems } from "@roastery/seedbed/application/types";
// Domain — Repository Interfaces
import type { ICanReadId, ICanReadSlug } from "@roastery/seedbed/domain/types/repositories";
// Domain — Services
import { SlugUniquenessCheckerService } from "@roastery/seedbed/domain/services";
// Presentation — DTOs
import { IdOrSlugDTO, PaginationDTO } from "@roastery/seedbed/presentation/dtos";
// Presentation — Types
import type { IPlugin } from "@roastery/seedbed/presentation/types";
// Constants
import { MAX_ITEMS_PER_QUERY, CACHE_EXPIRATION_TIME } from "@roastery/seedbed/constants";# 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 setupMIT