---
layout: 'page'
uri: '/framework/domain/entities'
position: 1
slug: 'framework-domain-entities'
parent: 'framework-domain'
navTitle: 'Entity & Value Objects'
title: 'Entity & Value Objects'
description: 'Doménové entity (User, RefreshToken) a value objects (Nickname, Role, Email, Password).'
---

# Entity & Value Objects


## Proč

Entity reprezentují doménové objekty s identitou. Value objects zajišťují, že data jsou validní už při vytvoření -- nelze konstruovat objekt v neplatném stavu. Validace formátu a povinných polí žije ve value objects, business pravidla s I/O (např. unique nickname) v command handlerech.


## Jak

### User entity

Žije v `domain/user/user.go`. Struct používá `db:"..."` tagy pro sqlx scanning.

```go
package user

type User struct {
    ID           string    `db:"id"`
    Nickname     string    `db:"nickname"`
    PasswordHash string    `db:"password_hash"`
    Email        string    `db:"email"`
    Role         string    `db:"role"`
    Active       bool      `db:"active"`
    CreatedAt    time.Time `db:"created_at"`
    UpdatedAt    time.Time `db:"updated_at"`
}

func NewUser(nickname Nickname, passwordHash string, email Email, role Role) *User {
    return &User{
        ID:           uuid.New().String(),
        Nickname:     string(nickname),
        PasswordHash: passwordHash,
        Email:        string(email),
        Role:         string(role),
        Active:       true,
        CreatedAt:    time.Now(),
        UpdatedAt:    time.Now(),
    }
}
```

Factory `NewUser` přijímá value objects (`Nickname`, `Email`, `Role`) -- pokud se caller dostal až sem, data jsou validní. `passwordHash` je odvozený stav (produkt `PasswordHasher`), ne doménový koncept -- raw heslo se validuje přes `Password` VO těsně před hashováním.


### RefreshToken entity

Žije v `domain/token/refresh_token.go`.

```go
package token

type RefreshToken struct {
    ID        string     `db:"id"`
    UserID    string     `db:"user_id"`
    TokenHash string     `db:"token_hash"`
    ExpiresAt time.Time  `db:"expires_at"`
    CreatedAt time.Time  `db:"created_at"`
    UsedAt    *time.Time `db:"used_at"` // marker pro theft detection (rotace + zneužití)
}
```


### Nickname value object

Žije v `domain/user/nickname.go`.

```go
package user

type Nickname string

func NewNickname(s string) (Nickname, error) {
    if s == "" {
        return "", &shared.ValidationError{Field: "nickname", Message: "nickname is required"}
    }
    if len(s) > 50 {
        return "", &shared.ValidationError{Field: "nickname", Message: "nickname must be at most 50 characters"}
    }
    return Nickname(s), nil
}
```


### Role value object

Žije v `domain/user/role.go`.

```go
package user

type Role string

const (
    RoleAdmin Role = "admin"
    RoleUser  Role = "user"
)

func NewRole(s string) (Role, error) {
    switch Role(s) {
    case RoleAdmin, RoleUser:
        return Role(s), nil
    default:
        return "", &shared.ValidationError{Field: "role", Message: "invalid role"}
    }
}
```


### Email value object

Žije v `domain/user/email.go`. Email je **nepovinný** -- prázdný řetězec projde jako prázdný `Email`. Když je hodnota neprázdná, validuje se maximální délka a přítomnost `@`. Striktnější kontrolu (regex, DNS MX lookup) záměrně nedělá -- uživatel přijde na řadu při prvním odeslání mailu.

```go
package user

type Email string

// NewEmail validates the email. Empty string is allowed (email is optional).
func NewEmail(s string) (Email, error) {
    if s == "" {
        return "", nil
    }
    if len(s) > 254 {
        return "", &shared.ValidationError{
            Field:   "email",
            Message: "email must be at most 254 characters",
        }
    }
    if !strings.Contains(s, "@") {
        return "", &shared.ValidationError{Field: "email", Message: "invalid email format"}
    }
    return Email(s), nil
}
```


### Password value object

Žije v `domain/user/password.go`. Validuje **raw** heslo před hashingem -- na už uložený hash se nevztahuje (login jen porovnává).

```go
package user

type Password string

func NewPassword(s string) (Password, error) {
    if s == "" {
        return "", &shared.ValidationError{Field: "password", Message: "password is required"}
    }
    if len(s) < 8 {
        return "", &shared.ValidationError{
            Field:   "password",
            Message: "password must be at least 8 characters",
        }
    }
    if len(s) > 128 {
        return "", &shared.ValidationError{
            Field:   "password",
            Message: "password must be at most 128 characters",
        }
    }
    return Password(s), nil
}
```

Používá se v `CreateUserCommand` (při registraci) a `ChangePasswordCommand` (při změně hesla). `LoginCommand` ho **nepoužívá** -- ten jen porovnává se stored hashem, nevaliduje pravidla (jinak by změna pravidel zamkla existující účty).


## Detaily

### Kde žije validace

| Typ | Kde | Příklad |
|---|---|---|
| Formát, povinná pole | Value objects (`NewNickname`, `NewRole`, `NewEmail`, `NewPassword`) | `NewNickname("")` -> `ValidationError` |
| Business pravidla s I/O | Command handler | Unique nickname (repo lookup) |
| Oprávnění | Bus `AuthorizeMiddleware` | `Permissioned` interface |
| Záchranná síť | SQL constraints | `UNIQUE`, `CHECK` |

### Konvence

- Každá entita žije ve vlastním subdoménovém balíčku (`user/`, `token/`).
- Entity struct má `db:"..."` tagy -- používané `sqlx` pro automatický scanning.
- Value objects vrací `*shared.ValidationError` při nevalidním vstupu.
- Factory funkce (např. `NewUser`) přijímají value objects, ne raw stringy.
- Entity nemá metody s side-effecty (žádné Save, Load) -- to je zodpovědnost repository.

---

[← Domain](/framework/domain.md) | [Interfaces →](/framework/domain/interfaces.md)
