npm package: @archtx/core
Description: Core types and utils for @archtx
Author: coryrobinson42@gmail.com
Latest version: 6.2.0

# @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

```bash
npm install @archtx/core
```

## Entity

The `Entity` class is the core abstraction for domain entities with immutable props and built-in validation.

### Basic Usage

```typescript
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: 'john@example.com' })
console.log(user.props.name) // 'John'
```

### Key Features

#### Immutable Props

Entity props are deeply frozen and cloned on construction. Direct modification attempts throw errors:

```typescript
const user = User.create({ id: '123', name: 'John', email: 'john@example.com' })

// 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' // TypeError
```

#### The `set()` Method

Update entity props safely through the `set()` method. It validates before applying changes and preserves immutability:

```typescript
// 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 `id` property

```typescript
// Validation errors prevent updates
user.set('name', '') // throws ValidationError, props unchanged

// Cannot change entity ID
user.set('id', 'new-id') // throws EntityError
```

#### Private Setters

Exclude properties from the public `set()` method using the second generic parameter:

```typescript
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 allowed
```

#### Change Tracking

Track modifications to entities using `originalProps` and `hasChanges`:

```typescript
// New entities always have changes (they need to be persisted)
const newUser = User.create({ id: '123', name: 'John', email: 'john@example.com' })
console.log(newUser.hasChanges) // true - new entity needs INSERT

// Existing entities start with no changes
const user = User.fromPersistence({ id: '123', name: 'John', email: 'john@example.com' })
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: 'john@example.com' }
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:

```typescript
const newUser = User.create({ id: '123', name: 'John', email: 'john@example.com' })
// newUser.metadata.newEntity === true

const existingUser = User.fromPersistence({ id: '123', name: 'John', email: 'john@example.com' })
// existingUser.metadata.newEntity === false
```

Use 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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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:

```typescript
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