@archtx/core
v6.2.0
Published
Core types and utils for @archtx
Downloads
34
Readme
@archtx/core
Core building blocks for clean architecture TypeScript applications. Provides foundational abstractions for domain entities, use cases, scheduled processes, and a structured error hierarchy.
Installation
npm install @archtx/coreEntity
The Entity class is the core abstraction for domain entities with immutable props and built-in validation.
Basic Usage
import { Entity } from '@archtx/core'
interface UserSchema {
id: string
name: string
email: string
age?: number
}
class User extends Entity<UserSchema> {
// Use static factory methods - constructor is protected
static create(props: UserSchema) {
const user = new User(props, { newEntity: true })
user.validate(props)
return user
}
static fromPersistence(props: UserSchema) {
return new User(props, { newEntity: false })
}
validate(props: UserSchema) {
if (!props.name || props.name.length === 0) {
throw new ValidationError('Name is required')
}
if (!props.email.includes('@')) {
throw new ValidationError('Invalid email format')
}
}
}
const user = User.create({ id: '123', name: 'John', email: '[email protected]' })
console.log(user.props.name) // 'John'Key Features
Immutable Props
Entity props are deeply frozen and cloned on construction. Direct modification attempts throw errors:
const user = User.create({ id: '123', name: 'John', email: '[email protected]' })
// This throws - props are frozen
user.props.name = 'Jane' // TypeError: Cannot assign to read only property
// Props are deeply frozen
user.props.metadata.nested = 'value' // TypeErrorThe set() Method
Update entity props safely through the set() method. It validates before applying changes and preserves immutability:
// Update a single property with a value
user.set('name', 'Jane')
// Update a single property with a function
user.set('age', (current) => (current ?? 0) + 1)
// Update multiple properties at once
user.set((draft) => {
draft.name = 'Jane Doe'
draft.age = 30
})The set() method:
- Clones props before modification
- Validates the new props via the
validate()method - Rejects invalid updates (original props are preserved)
- Deep freezes the result
- Prevents changing the
idproperty
// Validation errors prevent updates
user.set('name', '') // throws ValidationError, props unchanged
// Cannot change entity ID
user.set('id', 'new-id') // throws EntityErrorPrivate Setters
Exclude properties from the public set() method using the second generic parameter:
interface PostSchema {
id: string
title: string
createdAt: Date
updatedAt: Date
}
// createdAt and updatedAt cannot be set via the public set() method
class Post extends Entity<PostSchema, 'createdAt' | 'updatedAt'> {
static create(props: Omit<PostSchema, 'createdAt' | 'updatedAt'>) {
const now = new Date()
return new Post(
{ ...props, createdAt: now, updatedAt: now },
{ newEntity: true }
)
}
// Internal method to update timestamp
touch() {
// Use the draft updater to modify protected props
this.set((draft) => {
draft.updatedAt = new Date()
})
}
validate() {}
}
const post = Post.create({ id: '1', title: 'Hello' })
post.set('title', 'Updated') // OK
post.set('createdAt', new Date()) // TypeScript error - not allowedChange Tracking
Track modifications to entities using originalProps and hasChanges:
// New entities always have changes (they need to be persisted)
const newUser = User.create({ id: '123', name: 'John', email: '[email protected]' })
console.log(newUser.hasChanges) // true - new entity needs INSERT
// Existing entities start with no changes
const user = User.fromPersistence({ id: '123', name: 'John', email: '[email protected]' })
console.log(user.originalProps) // null - no changes yet
console.log(user.hasChanges) // false
user.set('name', 'Jane')
console.log(user.originalProps) // { id: '123', name: 'John', email: '[email protected]' }
console.log(user.hasChanges) // true
console.log(user.props.name) // 'Jane'Use hasChanges in repositories to determine persistence action:
newEntity && hasChanges→ INSERT!newEntity && hasChanges→ UPDATE!hasChanges→ skip (no persistence needed)
New Entity Flag
The metadata.newEntity flag distinguishes newly created entities from those loaded from persistence:
const newUser = User.create({ id: '123', name: 'John', email: '[email protected]' })
// newUser.metadata.newEntity === true
const existingUser = User.fromPersistence({ id: '123', name: 'John', email: '[email protected]' })
// existingUser.metadata.newEntity === falseUse this flag in repositories to determine whether to INSERT or UPDATE.
Convert to Plain Object
Use toPOJO() to get a mutable clone of the props:
const data = user.toPOJO()
data.name = 'Modified' // OK - it's a clone
console.log(user.props.name) // Still 'John'UseCase
Abstract base class for application use cases:
import { UseCase, ILogger } from '@archtx/core'
interface UserRepository {
findById(id: string): Promise<User | null>
}
class GetUserUseCase extends UseCase<{ userRepo: UserRepository }> {
async execute(userId: string): Promise<User> {
this.log.info('Fetching user', { userId })
const user = await this.services.userRepo.findById(userId)
if (!user) {
throw new NotFoundError('User not found')
}
return user
}
}
// Usage
const useCase = new GetUserUseCase({
log: console, // or your logger implementation
userRepo: myUserRepository
})
const user = await useCase.execute('123')ScheduledProcess
Abstract base class for CRON-scheduled background jobs:
import { ScheduledProcess } from '@archtx/core'
class CleanupExpiredTokens extends ScheduledProcess<{ tokenRepo: TokenRepository }> {
CronSchedule = '0 0 * * *' // Daily at midnight
async execute() {
const deleted = await this.services.tokenRepo.deleteExpired()
return { deletedCount: deleted }
}
}Error Hierarchy
All errors extend RootError which supports internal messages and user-facing messages:
import {
RootError,
ValidationError,
NotFoundError,
AuthenticationError,
AuthorizationError,
EntityError,
ExpiredError,
ParamError,
RepoError,
ServiceError,
UseCaseError
} from '@archtx/core'
// Internal message for logs, public message for users
throw new ValidationError('Email validation failed: missing @ symbol', {
publicMsg: 'Please enter a valid email address',
field: 'email'
})
// Access error metadata
try {
// ...
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.message) // Internal: 'Email validation failed: missing @ symbol'
console.log(error.meta?.publicMsg) // User-facing: 'Please enter a valid email address'
console.log(error.meta?.field) // 'email'
}
}Error Types
| Error | Purpose |
|-------|---------|
| ValidationError | User input validation failures |
| NotFoundError | Resource not found |
| AuthenticationError | Authentication failures |
| AuthorizationError | Permission/access denied |
| EntityError | Domain entity invariant violations |
| ExpiredError | Token/session expiration |
| ParamError | Invalid parameters |
| RepoError | Repository/data access errors |
| ServiceError | External service failures |
| UseCaseError | Use case execution errors |
Utility Types
import { WithID, DeepReadonly, ISODateString, UUID } from '@archtx/core'
// WithID<T> - Adds id: string to any object type
type UserWithID = WithID<{ name: string }> // { name: string; id: string }
// DeepReadonly<T> - Recursive readonly wrapper
type ReadonlyUser = DeepReadonly<{ profile: { name: string } }>
// { readonly profile: { readonly name: string } }
// Semantic type aliases
const timestamp: ISODateString = '2024-01-15T10:30:00.000Z'
const userId: UUID = '550e8400-e29b-41d4-a716-446655440000'Logger Interface
Implement the ILogger interface for your logging solution:
import { ILogger } from '@archtx/core'
const logger: ILogger = {
debug: (msg, ctx) => console.debug(msg, ctx),
info: (msg, ctx) => console.info(msg, ctx),
warn: (msg, ctx) => console.warn(msg, ctx),
error: (msg, ctx) => console.error(msg, ctx)
}License
MIT
