Entity
Rich domain models that can access services without dependency injection
Rich domain models that can access services without dependency injection.
The problem
Domain logic tends to scatter across the codebase. A User type ends up with utility functions spread across multiple files:
interface User {
id: string
firstName: string
lastName: string
email: string
}
// In one file...
function getFullName(user: User) {
return `${user.firstName} ${user.lastName}`
}
// In another file...
function getUserOrders(user: User, db: Database) {
return db.query("SELECT * FROM orders WHERE user_id = $1", [user.id])
}
// In yet another file...
function formatUserEmail(user: User) {
return `${getFullName(user)} <${user.email}>`
}Logic that belongs to User is spread across your codebase. And functions like getUserOrders need dependencies passed in manually.
The solution
Define.Entity lets you colocate logic on the model. Because services are ALS-based, entity methods can access services without DI:
import { Define } from "within-ts"
class User extends Define.Entity<{
id: string
firstName: string
lastName: string
email: string
}>() {
get fullName() {
return `${this.entity.firstName} ${this.entity.lastName}`
}
get formattedEmail() {
return `${this.fullName} <${this.entity.email}>`
}
async orders() {
const db = new Database() // reads from ALS — no DI needed
return db.query("SELECT * FROM orders WHERE user_id = $1", [this.entity.id])
}
async save() {
const db = new Database()
Logger.info({ message: "saving user", data: { id: this.entity.id } })
await db.update("users", this.entity)
}
}
// Usage
const user = new User({ id: "1", firstName: "John", lastName: "Doe", email: "[email protected]" })
user.entity.firstName // "John"
user.entity.firstName = "Jane" // simple mutation
user.entity // grab the whole data object
user.fullName // "Jane Doe"
await user.orders() // queries via ALS-provided Database
await user.save() // passes this.entity to dbEntity methods can call new Database(), new Logger(), etc. without being passed them as arguments. AsyncLocalStorage makes the entity pattern work without DI boilerplate. this.entity keeps all data grouped — straightforward to access, update, or pass to other functions.