Project

activeitem

0.0
The project is in a healthy, maintained state
A Rails-inspired ORM for DynamoDB with query builder, associations, callbacks, dirty tracking, validations, transactions, and pagination.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.12
~> 1.0
>= 1.10, < 2

Runtime

 Project Readme

ActiveItem

ActiveRecord-like ORM for AWS DynamoDB.

Installation

gem 'activeitem'

To install with signature verification:

gem cert --add <(curl -Ls https://raw.githubusercontent.com/stowzilla/activeitem/master/certs/stowzilla.pem)
gem install activeitem -P MediumSecurity

Configuration

ActiveItem.configure do |config|
  config.table_prefix = 'myapp'
  config.environment = 'production'
  config.logger = Rails.logger # or any Logger-compatible object
end

Table names are generated as {prefix}-{environment}-{model-name-pluralized}.

Usage

class User < ActiveItem::Base
  self.primary_key = :user_id

  attr_accessor :email, :name, :status

  indexes(
    'EmailIndex' => { partition_key: 'email' },
    'StatusIndex' => { partition_key: 'status', sort_key: 'createdAt' }
  )

  validates :email, presence: true
  validates :email, uniqueness: true

  scope :active, -> { where(status: 'active') }

  before_create :set_defaults

  private

  def set_defaults
    self.status ||= 'active'
  end
end

CRUD

user = User.create!(email: 'alice@example.com', name: 'Alice')
user = User.find('user-123')
user.update(name: 'Alice Smith')
user.destroy

Querying

User.where(status: 'active', index: 'StatusIndex')
User.where(email: 'alice@example.com', index: 'EmailIndex').first
User.where.not(status: 'deleted')
User.all.limit(50)
User.count
User.exists?('user-123')

Sort-key conditions on an index go into the key condition, not a filter:

Event.where(customer_id: id, created_at: 1.week.ago..)                 # >=, <=, BETWEEN via Range
Username.where(search_prefix: 'ab', search_name: ActiveItem.begins_with('abe'))  # prefix

For prefix search (autocomplete), add a GSI whose partition key is the first few lowercased characters and whose sort key is the whole lowercased value, then begins_with on the sort key. begins_with on a non-key attribute works too, but it's a filter, so DynamoDB still reads every item in the query or scan.

Associations

class Post < ActiveItem::Base
  belongs_to :user
  has_many :comments, foreign_key: 'post_id', index: 'PostIndex'
end

Transactions

ActiveItem::Base.transaction do |txn|
  txn.put(new_record)
  txn.update(existing_record)
  txn.delete(old_record)
end

Pagination

result = Post.where(user_id: id, index: 'UserIndex').page(cursor, per_page: 25)
result.items          # => [Post, Post, ...]
result.pagination_metadata # => { next_cursor: "...", has_more: true, per_page: 25 }

Composed Of (Value Objects)

class Customer < ActiveItem::Base
  attr_accessor :street, :city, :state, :zip_code

  composed_of :address, class_name: 'Address', mapping: {
    street: :street, city: :city, state: :state, zip_code: :zip_code
  }
end

Custom Attribute Writers

Like ActiveRecord, you can override a generated writer to coerce or normalize a value on assignment. Just route the assignment back through the generated writer — call super, or use write_attribute. Both record the change for dirty tracking, so save (and update) persist it. Assigning @ivar directly bypasses dirty tracking and the change is silently dropped on update.

class Article < ActiveItem::Base
  attr_accessor :tags, :payload

  # Coerce to an Array, Rails-style: override and call super.
  def tags=(value)
    super(Array(value))
  end

  # Or be explicit with write_attribute.
  def payload=(value)
    write_attribute(:payload, value.is_a?(String) ? value : JSON.generate(value))
  end
end

read_attribute(:name) is available too, for custom readers:

def display_name
  read_attribute(:name).to_s.strip
end

License

MIT