Models
Belt uses ActiveItem as its ORM for DynamoDB. Models inherit from
ActiveItem::Base (typically via an ApplicationRecord base class).
Basic Model
class Post < ApplicationRecord
attr_accessor :id, :user_id, :title, :body, :status, :created_at, :updated_at
validates :title, presence: true
validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
before_create { self.id ||= SecureRandom.uuid }
before_save { self.updated_at = Time.now.iso8601 }
end
Table Naming
Table names are derived from the environment:
{APP_NAME}-{ENVIRONMENT}-{pluralized_model}
Example: app "blog", environment "prod", model "Post" → blog-prod-posts
Schema Definition
Define table structure in infrastructure/schema.tf.rb:
Belt.application.schema.define do
model :post do
partition_key :id, :string
global_secondary_index :UserIndex, partition_key: :user_id
global_secondary_index :StatusIndex, partition_key: :status, sort_key: :created_at
end
end
CRUD Operations
Create
post = Post.create!(title: "Hello", body: "World", user_id: "u-123")
# or
post = Post.new(title: "Hello")
post.save!
Read
post = Post.find("post-id-123") # by primary key
posts = Post.all # scan (use sparingly)
posts = Post.where(user_id: "u-123", index: "UserIndex")
post = Post.find_by(user_id: "u-123", index: "UserIndex") # first match
Update
post = Post.find("post-id-123")
post.update(title: "New Title")
# or
post.title = "New Title"
post.save!
Delete
post = Post.find("post-id-123")
post.destroy
Query Patterns
Using Indexes
# Query a GSI
Post.where(user_id: "u-123", index: "UserIndex")
# With sort key conditions
Post.where(
status: "published",
created_at: { gte: "2024-01-01" },
index: "StatusIndex"
)
Count and Existence
Post.count # total items (scan)
Post.exists?("post-id-123") # check by primary key
Validations
ActiveItem supports ActiveModel-style validations:
validates :title, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :status, inclusion: { in: %w[active inactive] }
validates :age, numericality: { greater_than: 0 }
Callbacks
before_create { self.id ||= SecureRandom.uuid }
before_save { self.updated_at = Time.now.iso8601 }
after_create { notify_subscribers }
before_destroy { cleanup_associations }
Associations (Manual)
DynamoDB doesn't have joins. Model relationships manually:
class Post < ApplicationRecord
def comments
Comment.where(post_id: id, index: "PostIndex")
end
def author
User.find(user_id)
end
end
Transactions
ActiveItem::Transaction.write do |tx|
tx.put(post)
tx.put(comment)
tx.delete(draft)
end
Generating Models
belt generate model post title:string body:text user_id:string
belt generate resource comment body:text author:string post_id:string
The resource generator creates model + controller + routes + schema entry.
See Also
belt explain controllers— using models in controllersbelt explain deployment— how schema becomes infrastructurebelt explain queries— advanced DynamoDB query patterns