Skip to main content

Models

VEF models are regular Go structs, but they are usually designed to cooperate with Bun, validation tags, search tags, and the framework's audit conventions.

The Common Pattern​

Most persistent models look like this:

type User struct {
bun.BaseModel `bun:"table:sys_user,alias:su"`
orm.FullAuditedModel

Username string `json:"username" validate:"required,alphanum,max=32" label:"Username"`
Email string `json:"email" validate:"omitempty,email,max=128" label:"Email"`
IsActive bool `json:"isActive"`
}

This combines two different concerns:

  • bun.BaseModel: table metadata for Bun
  • orm.FullAuditedModel: framework-owned base fields including ID, creation and update audit columns

Base Model Types​

VEF exposes five reusable model types through orm. They are designed for anonymous embedding as composable field slices.

Types Without Primary Key​

TypeFieldsUse Case
orm.CreationTrackedModelCreatedAt, CreatedBy, CreatedByNameComposite-PK tables that need creation tracking
orm.FullTrackedModelCreatedAt, CreatedBy, CreatedByName, UpdatedAt, UpdatedBy, UpdatedByNameComposite-PK tables that need full audit tracking

Types With Primary Key​

TypeFieldsUse Case
orm.ModelID onlyReference tables, join tables, minimal records
orm.CreationAuditedModelID, CreatedAt, CreatedBy, CreatedByNameAppend-only records, logs, outbox tables
orm.FullAuditedModelID, CreatedAt, CreatedBy, CreatedByName, UpdatedAt, UpdatedBy, UpdatedByNameStandard mutable entities with full audit trail

Choosing The Right Type​

Use the smallest type that matches your entity's lifecycle:

  • orm.Model: you only need a primary key, no audit tracking at all
  • orm.CreationAuditedModel: append-only records — writes once, never updated
  • orm.FullAuditedModel: the most common choice — standard mutable entities that track both creation and update metadata
  • orm.CreationTrackedModel: same as CreationAuditedModel but without the primary key — useful for composite-PK tables
  • orm.FullTrackedModel: same as FullAuditedModel but without the primary key — useful for composite-PK tables

Conceptually, orm.FullAuditedModel is the pre-composed form of orm.Model + orm.FullTrackedModel.

Internal Field Definitions​

Here is exactly what each type contributes, including all struct tags:

// orm.Model — primary key only
type Model struct {
ID string `json:"id" bun:"id,pk"`
}

// orm.CreationTrackedModel — creation audit without PK
type CreationTrackedModel struct {
CreatedAt timex.DateTime `json:"createdAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP,skipupdate"`
CreatedBy string `json:"createdBy" bun:",notnull,skipupdate" mold:"translate=user?"`
CreatedByName string `json:"createdByName" bun:",scanonly"`
}

// orm.FullTrackedModel — full audit without PK
type FullTrackedModel struct {
CreatedAt timex.DateTime `json:"createdAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP,skipupdate"`
CreatedBy string `json:"createdBy" bun:",notnull,skipupdate" mold:"translate=user?"`
CreatedByName string `json:"createdByName" bun:",scanonly"`
UpdatedAt timex.DateTime `json:"updatedAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP"`
UpdatedBy string `json:"updatedBy" bun:",notnull" mold:"translate=user?"`
UpdatedByName string `json:"updatedByName" bun:",scanonly"`
}

// orm.CreationAuditedModel — PK + creation audit
type CreationAuditedModel struct {
ID string `json:"id" bun:"id,pk"`
CreatedAt timex.DateTime `json:"createdAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP,skipupdate"`
CreatedBy string `json:"createdBy" bun:",notnull,skipupdate" mold:"translate=user?"`
CreatedByName string `json:"createdByName" bun:",scanonly"`
}

// orm.FullAuditedModel — PK + full audit
type FullAuditedModel struct {
ID string `json:"id" bun:"id,pk"`
CreatedAt timex.DateTime `json:"createdAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP,skipupdate"`
CreatedBy string `json:"createdBy" bun:",notnull,skipupdate" mold:"translate=user?"`
CreatedByName string `json:"createdByName" bun:",scanonly"`
UpdatedAt timex.DateTime `json:"updatedAt" bun:",notnull,type:timestamp,default:CURRENT_TIMESTAMP"`
UpdatedBy string `json:"updatedBy" bun:",notnull" mold:"translate=user?"`
UpdatedByName string `json:"updatedByName" bun:",scanonly"`
}

Key Tag Details​

  • bun:",skipupdate": created_at and created_by are set on insert only and never overwritten on update
  • bun:",scanonly": created_by_name and updated_by_name are read-side convenience fields — they are populated from JOIN queries but are not persisted as separate columns
  • mold:"translate=user?": an optional hook for a host-registered mold.Translator that turns user IDs into display names, populating the *ByName fields. The framework's only built-in translator handles codes:<codeSet> kinds (see Mold); it does not serve user. The trailing ? means "skip silently when no registered translator supports this kind" — without a custom user translator the fields are simply left untouched
  • timex.DateTime: the framework's custom timestamp type (see Timex) — not time.Time

Embedding And Composition​

The smaller base model types are useful when an entity does not need the full orm.FullAuditedModel field set.

// Minimal: just a primary key
type Tag struct {
bun.BaseModel `bun:"table:tag,alias:t"`
orm.Model

Name string `json:"name" bun:"name,notnull"`
}

// Append-only: PK + creation audit
type ActionLog struct {
bun.BaseModel `bun:"table:apv_action_log,alias:aal"`
orm.Model
orm.CreationTrackedModel

InstanceID string `json:"instanceId" bun:"instance_id"`
Action string `json:"action" bun:"action"`
}

// Standard mutable entity: PK + full audit
type Role struct {
bun.BaseModel `bun:"table:sys_role,alias:sr"`
orm.FullAuditedModel

Name string `json:"name" bun:"name,notnull"`
IsActive bool `json:"isActive" bun:"is_active"`
}

// Composite PK: creation tracking only, PK columns declared on the fields
// themselves (orm.Model is deliberately absent — it would add a single `id`
// primary key).
type UserRole struct {
bun.BaseModel `bun:"table:sys_user_role,alias:sur"`
orm.CreationTrackedModel

UserID string `json:"userId" bun:"user_id,pk"`
RoleID string `json:"roleId" bun:"role_id,pk"`
}

Typical choices:

  • orm.Model: reference tables, join tables, or records with no audit columns
  • orm.Model + orm.CreationTrackedModel: append-only records, snapshots, logs, outbox tables
  • orm.FullAuditedModel: standard mutable entities that track both creation and update metadata
  • orm.FullTrackedModel: entities with composite primary keys that still want full audit tracking

Bun Model Hooks​

VEF re-exports Bun's model lifecycle hook interfaces through orm:

Hook InterfaceWhen Called
orm.BeforeSelectHookBefore a SELECT query executes
orm.AfterSelectHookAfter a SELECT query executes
orm.BeforeInsertHookBefore an INSERT query executes
orm.AfterInsertHookAfter an INSERT query executes
orm.BeforeUpdateHookBefore an UPDATE query executes
orm.AfterUpdateHookAfter an UPDATE query executes
orm.BeforeDeleteHookBefore a DELETE query executes
orm.AfterDeleteHookAfter a DELETE query executes
orm.BeforeScanRowHookBefore scanning a row
orm.AfterScanRowHookAfter scanning a row

Implement any of these on your model struct to add lifecycle behavior:

func (u *User) BeforeInsert(ctx context.Context, query *orm.BunInsertQuery) error {
// Set defaults, validate, or log before insert
return nil
}

Audit Fields​

The framework standardizes these common audit columns:

ColumnJSON NamePersistedPurpose
idid✅Primary key
created_atcreatedAt✅Creation timestamp
created_bycreatedBy✅Creator user ID
created_by_namecreatedByName❌ scanonlyCreator display name (populated by mold or JOIN)
updated_atupdatedAt✅Last update timestamp
updated_byupdatedBy✅Last updater user ID
updated_by_nameupdatedByName❌ scanonlyUpdater display name (populated by mold or JOIN)

Not every model embeds all of these fields. orm.CreationTrackedModel contributes the created_* subset. orm.FullTrackedModel and orm.FullAuditedModel contribute both subsets.

The framework stamps updated_at and updated_by on every UPDATE shape — whether the query is model-based, uses Set/SetExpr, has a column whitelist, or is a bulk update. created_at and created_by are excluded from UPDATE automatically (they carry skipupdate and are skipped by the framework's beforeUpdate hook). The one exception is an explicit Set("updated_at", ...) or Set("updated_by", ...) — the framework respects caller intent and does not overwrite an explicitly set audit column.

The framework also exports audit column and field name constants:

orm.ColumnID // "id"
orm.ColumnCreatedAt // "created_at"
orm.ColumnUpdatedAt // "updated_at"
orm.ColumnCreatedBy // "created_by"
orm.ColumnUpdatedBy // "updated_by"
orm.ColumnCreatedByName // "created_by_name"
orm.ColumnUpdatedByName // "updated_by_name"

orm.FieldID // "ID"
orm.FieldCreatedAt // "CreatedAt"
// ... and so on

System operator constants for created_by / updated_by:

orm.OperatorSystem // "system" — used by system initialization
orm.OperatorCronJob // "cron_job" — used by scheduled tasks
orm.OperatorAnonymous // "anonymous" — used by unauthenticated operations

Tags You Will Use Most Often​

bun​

Controls table name, alias, primary key rules, null behavior, and relations.

json​

Controls request and response payload field names. In practice, VEF projects usually use camelCase JSON names even when database columns stay snake_case.

validate​

Used by automatic request validation when params or meta are decoded into structs.

label / label_i18n​

Used by the validator to generate readable field names in error messages.

Used by the search parser and CRUD query builders to translate search payloads into SQL conditions.

meta​

Used by the storage.Files / FilesFor[T] lifecycle facade to detect uploaded file fields, rich text, and markdown content that participate in the upload-claim / pending-delete lifecycle. See Storage for the tag values and how the reconciliation runs.

mold​

Used by the struct transformer for field-level data transformation. The most common built-in usage is mold:"translate=user?" on *ByName fields.

Search Models Are Usually Separate​

Do not overload your persistence model with search semantics. Instead, define a dedicated search struct:

type UserSearch struct {
api.P

Keyword string `json:"keyword" search:"contains,column=username|email"`
IsActive *bool `json:"isActive" search:"eq,column=is_active"`
}

This keeps search rules explicit and prevents your write model from becoming a query DSL.

Pagination And Sorting Metadata​

For paging endpoints, metadata often comes through dedicated structs such as:

type UserSearch struct {
api.P
Keyword string `json:"keyword" search:"contains,column=username|email"`
}

type UserMeta struct {
api.M
page.Pageable
crud.Sortable
}

page.Pageable and crud.Sortable are metadata helpers, not persistence models.

Practical Advice​

  • keep database models small and explicit
  • use dedicated params structs for writes
  • use dedicated search structs for reads
  • compose from smaller embedded base models when an entity does not need the full orm.FullAuditedModel shape
  • embed orm.FullAuditedModel only when you want the framework's standard audit behavior
  • keep database tags, validation tags, and search tags close to the fields they govern
  • remember that *ByName fields are scan-only — they are never written to the database

Next Step​

Read Generic CRUD to see how these models plug into typed operation builders, or read the ORM SQL Builder for a comprehensive reference on constructing SQL queries.