Anar Framework Models Documentation#

Introduction#

The Anar framework provides a robust model system built on top of GORM (Go Object Relational Mapping) that simplifies database interactions and provides type-safe operations. The framework includes custom field types that extend basic Go types with additional functionality for JSON serialization, database storage, and validation.

Model Structure#

Basic Model Definition#

Models in Anar are Go structs that embed gorm.Model and define database table structure:

type User struct {
    gorm.Model

    ID        uint   `json:"id" gorm:"primaryKey"`
    Name      string `json:"name" gorm:"unique" validate:"required,min=3,max=20"`
    Pass      string `json:"pass" validate:"required,min=6"`
    Is_admin  bool   `json:"is_admin"`
    Email     string `json:"email" gorm:"unique" validate:"required,email"`
    Confirmed bool   `json:"confirmed" gorm:"default:false"`
    Token     string `json:"token"`
}

Model Tags#

Anar models use three types of struct tags:

Base Model Components#

gorm.Model Embedding#

All Anar models should embed gorm.Model which provides:

type MyModel struct {
    gorm.Model
    // Your fields here
}

Primary Key Definition#

While gorm.Model provides an ID field, you can override it:

ID uint `json:"id" gorm:"primaryKey"`

Custom Field Types#

The Anar framework provides several custom field types in the types package that extend basic Go types with enhanced functionality.

1. FileField#

Purpose: Stores file paths and automatically expands them to full URLs in JSON responses.

Usage:

type Product struct {
    gorm.Model
    Name  string             `json:"name"`
    Image types.FileField    `json:"image"`
}

Features: - Stores relative paths in database - Automatically prepends app URL when marshaling to JSON - Handles absolute URLs (http/https) without modification - GORM compatible with Scan() and Value() methods

JSON Output Example:

// Database value: "uploads/product.jpg"
// JSON output: "https://myapp.com/uploads/product.jpg"

2. JSONField#

Purpose: Stores arbitrary JSON objects in the database as text.

Usage:

type Settings struct {
    gorm.Model
    UserID     uint              `json:"user_id"`
    Preferences types.JSONField  `json:"preferences"`
}

Features: - Stores map[string]interface{} as JSON text in database - Automatic marshaling/unmarshaling - Handles null values gracefully - Type-safe JSON operations

Example:

settings := Settings{
    Preferences: types.JSONField{
        "theme": "dark",
        "notifications": true,
        "language": "en",
    },
}

3. StringArray#

Purpose: Stores arrays of strings as JSON in the database.

Usage:

type Article struct {
    gorm.Model
    Title string              `json:"title"`
    Tags  types.StringArray   `json:"tags"`
}

Features: - Stores []string as JSON array in database - Maintains order of elements - Handles empty arrays gracefully

Example:

article := Article{
    Title: "Go Programming",
    Tags: types.StringArray{"go", "programming", "tutorial"},
}

4. TimeField#

Purpose: Nullable timestamp field with ISO8601 JSON serialization.

Usage:

type Event struct {
    gorm.Model
    Name      string           `json:"name"`
    StartTime types.TimeField  `json:"start_time"`
    EndTime   types.TimeField  `json:"end_time"`
}

Features: - Nullable timestamp (can be null/nil) - ISO8601 format for JSON (RFC3339) - GORM compatible - Helper constructor: types.NewTimeField(time.Now())

JSON Format:

{
    "start_time": "2023-12-25T10:00:00Z",
    "end_time": null
}

5. EmailField#

Purpose: Email field with strict RFC5322 validation.

Usage:

type Contact struct {
    gorm.Model
    Name  string            `json:"name"`
    Email types.EmailField  `json:"email"`
}

Features: - Strict email validation on database operations - RFC5322 compliant regex validation - Handles empty values gracefully - Validation occurs during Scan() and Value() operations

Model Definition Examples#

Basic User Model#

package models

import (
    "gorm.io/gorm"
    "codeberg.org/anarproject/anar/types"
)

type User struct {
    gorm.Model

    Username    string            `json:"username" gorm:"unique;not null" validate:"required,min=3,max=20"`
    Email       types.EmailField  `json:"email" gorm:"unique;not null" validate:"required"`
    Password    string            `json:"-" validate:"required,min=8"` // Hidden from JSON
    IsActive    bool              `json:"is_active" gorm:"default:true"`
    Avatar      types.FileField   `json:"avatar"`
    LastLogin   types.TimeField   `json:"last_login"`
    Preferences types.JSONField   `json:"preferences"`
}

Blog Post Model#

type Post struct {
    gorm.Model

    Title       string              `json:"title" gorm:"not null" validate:"required,max=200"`
    Slug        string              `json:"slug" gorm:"unique;not null"`
    Content     string              `json:"content" gorm:"type:text"`
    Published   bool                `json:"published" gorm:"default:false"`
    Tags        types.StringArray   `json:"tags"`
    FeaturedImg types.FileField     `json:"featured_image"`
    PublishedAt types.TimeField     `json:"published_at"`
    AuthorID    uint                `json:"author_id" gorm:"not null"`
    Author      User                `json:"author" gorm:"foreignKey:AuthorID"`
    Metadata    types.JSONField     `json:"metadata"`
}

E-commerce Product Model#

type Product struct {
    gorm.Model

    Name        string              `json:"name" gorm:"not null" validate:"required,max=100"`
    Description string              `json:"description" gorm:"type:text"`
    Price       float64             `json:"price" gorm:"not null" validate:"gte=0"`
    SKU         string              `json:"sku" gorm:"unique;not null"`
    Images      types.StringArray   `json:"images"`
    MainImage   types.FileField     `json:"main_image"`
    InStock     bool                `json:"in_stock" gorm:"default:true"`
    Attributes  types.JSONField     `json:"attributes"`
    CategoryID  uint                `json:"category_id"`
    Category    Category            `json:"category" gorm:"foreignKey:CategoryID"`
}

type Category struct {
    gorm.Model

    Name     string `json:"name" gorm:"unique;not null"`
    ParentID *uint  `json:"parent_id"`
    Parent   *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
}

Field Validation#

Validation Tags#

Anar checks the validate tag (go-playground/validator syntax) on every create and update that goes through the generated CRUD handlers:

type User struct {
    Name     string `validate:"required,min=3,max=50"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"min=18,max=120"`
    Website  string `validate:"omitempty,url"`
}

Common Validation Rules#

required rejects the zero value, so don't put it on a number that may be 0 or a bool that may be false. Use gte=0, or a pointer type, instead.

How Validation Runs#

A request that breaks a rule is rejected with 422 Unprocessable Entity before anything is written. The fields are listed by their JSON names:

{
  "error": {
    "code": "validation_failed",
    "message": "Validation failed",
    "fields": {
      "name": "must be at least 3 characters long",
      "price": "must be greater than 0",
      "category": "is required"
    }
  }
}

Hand-written handlers that bind their own request structs should use Gin's binding:"..." tag instead.

Database Integration#

GORM Tags#

Common GORM tags for database schema:

type Model struct {
    ID          uint   `gorm:"primaryKey"`
    Name        string `gorm:"unique;not null;size:100"`
    Description string `gorm:"type:text"`
    Price       float64 `gorm:"precision:10;scale:2"`
    CreatedBy   uint   `gorm:"index"`
    IsActive    bool   `gorm:"default:true"`
}

Relationships#

One-to-Many#

type User struct {
    gorm.Model
    Posts []Post `json:"posts" gorm:"foreignKey:UserID"`
}

type Post struct {
    gorm.Model
    UserID uint `json:"user_id"`
    User   User `json:"user" gorm:"foreignKey:UserID"`
}

Many-to-Many#

type User struct {
    gorm.Model
    Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
}

type Role struct {
    gorm.Model
    Users []User `json:"users" gorm:"many2many:user_roles;"`
}

Database Hooks#

GORM provides hooks for model lifecycle events:

func (u *User) BeforeCreate(tx *gorm.DB) error {
    // Hash password before saving
    if u.Password != "" {
        hashedPassword, err := hashPassword(u.Password)
        if err != nil {
            return err
        }
        u.Password = hashedPassword
    }
    return nil
}

func (u *User) AfterFind(tx *gorm.DB) error {
    // Load additional data after finding
    return nil
}

Best Practices#

1. Field Naming#

2. JSON Handling#

type User struct {
    ID       uint   `json:"id"`
    Password string `json:"-"`          // Hidden from JSON
    Email    string `json:"email,omitempty"` // Omit if empty
}

3. Custom Methods#

func (u *User) IsAdmin() bool {
    return u.Is_admin
}

func (u *User) FullName() string {
    return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}

func (u *User) HasRole(role string) bool {
    for _, r := range u.Roles {
        if r.Name == role {
            return true
        }
    }
    return false
}

4. Validation#

5. Performance Considerations#

6. Security#

8. Migration Considerations#

// Use consistent field types
// Consider backward compatibility
// Use migrations for schema changes
type User struct {
    gorm.Model
    // Add new fields with appropriate defaults
    NewField string `json:"new_field" gorm:"default:'default_value'"`
}

This documentation provides a comprehensive guide to working with models in the Anar framework, covering all the custom field types, validation, relationships, and best practices for building robust database-backed applications.