@contract-kit/domain
v0.1.1
Published
Domain modeling helpers for contract-kit - value objects, entities, and domain events
Downloads
227
Maintainers
Readme
@contract-kit/domain
Domain modeling helpers for Contract Kit - value objects, entities, and domain events
This package provides small, framework-agnostic helpers for domain-driven design patterns. Works with any Standard Schema library (Zod, Valibot, ArkType, etc.).
Installation
npm install @contract-kit/domain
# Use with your preferred Standard Schema library
npm install zodTypeScript Requirements
This package requires TypeScript 5.0 or higher for proper type inference.
Value Objects
Value objects are immutable, validated types that represent domain concepts:
import { valueObject } from "@contract-kit/domain";
import { z } from "zod";
// Simple value object
const Email = valueObject("Email")
.schema(z.string().email())
.brand();
type Email = typeof Email.Type;
// Create with validation
const email = await Email.create("[email protected]"); // Returns branded Email type
// Check validity without throwing
const isValid = await Email.isValid("invalid"); // false
// Raw value access
console.log(email); // "[email protected]"Complex Value Objects
const Money = valueObject("Money")
.schema(z.object({
amount: z.number().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
}))
.brand();
type Money = typeof Money.Type;
const price = await Money.create({ amount: 99.99, currency: "USD" });Value Object with Transformation
const Slug = valueObject("Slug")
.schema(
z.string()
.min(1)
.transform((s) => s.toLowerCase().replace(/\s+/g, "-"))
)
.brand();
const slug = await Slug.create("Hello World"); // "hello-world"Entities
Entities are domain objects with identity and behavior:
import { entity } from "@contract-kit/domain";
import { z } from "zod";
const Todo = entity("Todo")
.props(z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
completed: z.boolean().default(false),
createdAt: z.date().default(() => new Date()),
}))
.methods((self) => ({
complete() {
return self.with({ completed: true });
},
rename(title: string) {
return self.with({ title });
},
isOverdue(now: Date) {
// Read-only method
return !self.props.completed && self.props.createdAt < now;
},
}))
.build();
type Todo = typeof Todo.Type;Using Entities
// Create a new entity
const todo = Todo.create({
id: "1",
title: "Learn Contract Kit",
});
// Access properties
console.log(todo.id); // "1"
console.log(todo.title); // "Learn Contract Kit"
console.log(todo.completed); // false
// Call methods (returns new instance - immutable)
const completed = todo.complete();
console.log(todo.completed); // false (original unchanged)
console.log(completed.completed); // true
// Chain method calls
const renamed = todo
.rename("Master Contract Kit")
.complete();Entity with Validation
const User = entity("User")
.props(z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
role: z.enum(["user", "admin"]).default("user"),
}))
.methods((self) => ({
promote() {
if (self.props.role === "admin") {
throw new Error("Already an admin");
}
return self.with({ role: "admin" as const });
},
}))
.build();Domain Events
Domain events represent something that happened in your domain:
import { domainEvent } from "@contract-kit/domain";
import { z } from "zod";
const TodoCompleted = domainEvent(
"todo.completed",
z.object({
todoId: z.string(),
completedAt: z.string().datetime(),
completedBy: z.string(),
})
);
type TodoCompleted = typeof TodoCompleted;
// Create an event instance
const event = {
type: "todo.completed",
payload: {
todoId: "123",
completedAt: new Date().toISOString(),
completedBy: "user-1",
},
};Using Domain Events with Use Cases
import { createUseCaseFactory } from "@contract-kit/application";
import { TodoCompleted } from "@/domain/events";
const completeTodo = useCase
.command("todos.complete")
.input(z.object({ id: z.string() }))
.output(z.object({ success: z.boolean() }))
.emits([TodoCompleted]) // Declares event emission
.run(async ({ ctx, input }) => {
const todo = await ctx.ports.db.todos.complete(input.id);
await ctx.ports.eventBus.publish({
type: "todo.completed",
payload: {
todoId: todo.id,
completedAt: new Date().toISOString(),
completedBy: ctx.user.id,
},
});
return { success: true };
});API Reference
valueObject(name)
Creates a value object builder.
valueObject(name: string)
.schema(schema: StandardSchema)
.brand() // Returns the value object definitionValue Object Definition
type ValueObjectDef = {
name: string;
Type: BrandedType;
create: (value) => Promise<BrandedType>;
isValid: (value) => Promise<boolean>;
};entity(name)
Creates an entity builder.
entity(name: string)
.props(schema: StandardSchema)
.methods((self) => ({
methodName() { return self.with({ ... }); },
}))
.build() // Returns the entity definitionEntity Instance
type EntityInstance = {
// All props are accessible as properties
[key: string]: value;
// Methods defined in .methods()
methodName(): EntityInstance;
// Built-in method for updates
with(updates: Partial<Props>): EntityInstance;
};domainEvent(type, payloadSchema)
Creates a domain event definition.
const Event = domainEvent(
type: string,
payloadSchema: StandardSchema
);Domain Event Definition
type DomainEventDef = {
name: string; // Event type name
payload: Schema; // Payload schema for validation
};Patterns
Aggregate Roots
Use entities as aggregate roots with child entities:
const Order = entity("Order")
.props(z.object({
id: z.string(),
customerId: z.string(),
items: z.array(OrderItem.schema),
status: z.enum(["draft", "placed", "shipped", "delivered"]),
}))
.methods((self) => ({
addItem(item: OrderItem) {
return self.with({
items: [...self.props.items, item],
});
},
place() {
if (self.props.items.length === 0) {
throw new Error("Cannot place empty order");
}
return self.with({ status: "placed" });
},
}))
.build();Rich Domain Model
Encapsulate business logic in entities:
const Account = entity("Account")
.props(z.object({
id: z.string(),
balance: z.number(),
status: z.enum(["active", "frozen", "closed"]),
}))
.methods((self) => ({
deposit(amount: number) {
if (self.props.status !== "active") {
throw new Error("Account is not active");
}
return self.with({
balance: self.props.balance + amount,
});
},
withdraw(amount: number) {
if (self.props.status !== "active") {
throw new Error("Account is not active");
}
if (self.props.balance < amount) {
throw new Error("Insufficient funds");
}
return self.with({
balance: self.props.balance - amount,
});
},
freeze() {
return self.with({ status: "frozen" });
},
}))
.build();Related Packages
@contract-kit/core- Core contract definitions@contract-kit/application- Use case definitions@contract-kit/ports- Ports & Adapters pattern@contract-kit/server- Hexagonal framework
License
MIT
