dynamoid 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Dynamoid.gemspec +116 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +58 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +86 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/dynamoid.rb +32 -0
- data/lib/dynamoid/adapter.rb +24 -0
- data/lib/dynamoid/adapter/aws_sdk.rb +100 -0
- data/lib/dynamoid/adapter/local.rb +77 -0
- data/lib/dynamoid/associations.rb +54 -0
- data/lib/dynamoid/associations/association.rb +80 -0
- data/lib/dynamoid/associations/belongs_to.rb +39 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +34 -0
- data/lib/dynamoid/associations/has_many.rb +33 -0
- data/lib/dynamoid/associations/has_one.rb +36 -0
- data/lib/dynamoid/attributes.rb +37 -0
- data/lib/dynamoid/components.rb +25 -0
- data/lib/dynamoid/config.rb +19 -0
- data/lib/dynamoid/config/options.rb +74 -0
- data/lib/dynamoid/document.rb +35 -0
- data/lib/dynamoid/errors.rb +8 -0
- data/lib/dynamoid/fields.rb +32 -0
- data/lib/dynamoid/finders.rb +62 -0
- data/lib/dynamoid/indexes.rb +59 -0
- data/lib/dynamoid/persistence.rb +35 -0
- data/lib/dynamoid/relations.rb +21 -0
- data/spec/app/models/address.rb +5 -0
- data/spec/app/models/magazine.rb +6 -0
- data/spec/app/models/sponsor.rb +6 -0
- data/spec/app/models/subscription.rb +6 -0
- data/spec/app/models/user.rb +13 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +123 -0
- data/spec/dynamoid/adapter/local_spec.rb +150 -0
- data/spec/dynamoid/adapter_spec.rb +13 -0
- data/spec/dynamoid/associations/association_spec.rb +71 -0
- data/spec/dynamoid/associations/belongs_to_spec.rb +50 -0
- data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +30 -0
- data/spec/dynamoid/associations/has_many_spec.rb +28 -0
- data/spec/dynamoid/associations/has_one_spec.rb +37 -0
- data/spec/dynamoid/associations_spec.rb +16 -0
- data/spec/dynamoid/attributes_spec.rb +43 -0
- data/spec/dynamoid/document_spec.rb +38 -0
- data/spec/dynamoid/fields_spec.rb +26 -0
- data/spec/dynamoid/finders_spec.rb +114 -0
- data/spec/dynamoid/indexes_spec.rb +54 -0
- data/spec/dynamoid/persistence_spec.rb +55 -0
- data/spec/dynamoid/relations_spec.rb +6 -0
- data/spec/dynamoid_spec.rb +5 -0
- data/spec/spec_helper.rb +52 -0
- metadata +204 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc
|
3
|
+
module Config
|
4
|
+
|
5
|
+
# Encapsulates logic for setting options.
|
6
|
+
module Options
|
7
|
+
|
8
|
+
# Get the defaults or initialize a new empty hash.
|
9
|
+
#
|
10
|
+
# @example Get the defaults.
|
11
|
+
# options.defaults
|
12
|
+
#
|
13
|
+
# @return [ Hash ] The default options.
|
14
|
+
#
|
15
|
+
# @since 2.3.0
|
16
|
+
def defaults
|
17
|
+
@defaults ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Define a configuration option with a default.
|
21
|
+
#
|
22
|
+
# @example Define the option.
|
23
|
+
# Options.option(:persist_in_safe_mode, :default => false)
|
24
|
+
#
|
25
|
+
# @param [ Symbol ] name The name of the configuration option.
|
26
|
+
# @param [ Hash ] options Extras for the option.
|
27
|
+
#
|
28
|
+
# @option options [ Object ] :default The default value.
|
29
|
+
#
|
30
|
+
# @since 2.0.0.rc.1
|
31
|
+
def option(name, options = {})
|
32
|
+
defaults[name] = settings[name] = options[:default]
|
33
|
+
|
34
|
+
class_eval <<-RUBY
|
35
|
+
def #{name}
|
36
|
+
settings[#{name.inspect}]
|
37
|
+
end
|
38
|
+
|
39
|
+
def #{name}=(value)
|
40
|
+
settings[#{name.inspect}] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def #{name}?
|
44
|
+
#{name}
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
# Reset the configuration options to the defaults.
|
50
|
+
#
|
51
|
+
# @example Reset the configuration options.
|
52
|
+
# config.reset
|
53
|
+
#
|
54
|
+
# @return [ Hash ] The defaults.
|
55
|
+
#
|
56
|
+
# @since 2.3.0
|
57
|
+
def reset
|
58
|
+
settings.replace(defaults)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the settings or initialize a new empty hash.
|
62
|
+
#
|
63
|
+
# @example Get the settings.
|
64
|
+
# options.settings
|
65
|
+
#
|
66
|
+
# @return [ Hash ] The setting options.
|
67
|
+
#
|
68
|
+
# @since 2.3.0
|
69
|
+
def settings
|
70
|
+
@settings ||= {}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
|
4
|
+
# This is the base module for all domain objects that need to be persisted to
|
5
|
+
# the database as documents.
|
6
|
+
module Document
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include Dynamoid::Components
|
9
|
+
|
10
|
+
attr_accessor :new_record
|
11
|
+
|
12
|
+
def initialize(attrs = {})
|
13
|
+
@new_record = true
|
14
|
+
@attributes ||= {}
|
15
|
+
self.class.attributes.each {|att| write_attribute(att, attrs[att])}
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
other.respond_to?(:id) && other.id == self.id
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
def create(attrs = {})
|
24
|
+
obj = self.new(attrs)
|
25
|
+
obj.save && obj.new_record = false
|
26
|
+
obj
|
27
|
+
end
|
28
|
+
|
29
|
+
def build(attrs = {})
|
30
|
+
self.new(attrs)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
|
4
|
+
module Fields
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :fields
|
9
|
+
|
10
|
+
self.fields = []
|
11
|
+
field :id
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def field(name, options = {})
|
16
|
+
named = name.to_s
|
17
|
+
self.fields << name
|
18
|
+
define_method(named) do
|
19
|
+
read_attribute(named)
|
20
|
+
end
|
21
|
+
define_method("#{named}=") do |value|
|
22
|
+
write_attribute(named, value)
|
23
|
+
end
|
24
|
+
define_method("#{named}?") do
|
25
|
+
!read_attribute(named).nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
|
4
|
+
# This module defines the finder methods that hang off the document at the
|
5
|
+
# class level.
|
6
|
+
module Finders
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def find(*id)
|
11
|
+
id = Array(id.flatten.uniq)
|
12
|
+
if id.count == 1
|
13
|
+
self.find_by_id(id.first)
|
14
|
+
else
|
15
|
+
items = Dynamoid::Adapter.batch_get_item(self.table_name => id)
|
16
|
+
items[self.table_name].collect{|i| o = self.build(i); o.new_record = false; o}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_by_id(id)
|
21
|
+
if item = Dynamoid::Adapter.get_item(self.table_name, id)
|
22
|
+
obj = self.new(Dynamoid::Adapter.get_item(self.table_name, id))
|
23
|
+
obj.new_record = false
|
24
|
+
return obj
|
25
|
+
else
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method, *args)
|
31
|
+
if method =~ /find/
|
32
|
+
finder = method.to_s.split('_by_').first
|
33
|
+
attributes = method.to_s.split('_by_').last.split('_and_')
|
34
|
+
|
35
|
+
results = if index = self.indexes.find {|i| i == attributes.sort.collect(&:to_sym)}
|
36
|
+
ids = Dynamoid::Adapter.get_item(self.index_table_name(index), self.key_for_index(index, args))
|
37
|
+
if ids.nil? || ids.empty?
|
38
|
+
[]
|
39
|
+
else
|
40
|
+
self.find(ids[:ids].to_a)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
if Dynamoid::Config.warn_on_scan
|
44
|
+
puts 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
45
|
+
puts "You can index this query by adding this to #{self.to_s.downcase}.rb: index [#{attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
|
46
|
+
end
|
47
|
+
scan_hash = {}
|
48
|
+
attributes.each_with_index {|attr, index| scan_hash[attr.to_sym] = args[index]}
|
49
|
+
Dynamoid::Adapter.scan(self.table_name, scan_hash).collect {|hash| self.new(hash)}
|
50
|
+
end
|
51
|
+
|
52
|
+
if finder =~ /all/
|
53
|
+
return Array(results)
|
54
|
+
else
|
55
|
+
return results.first
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
module Dynamoid #:nodoc:
|
5
|
+
|
6
|
+
# Builds all indexes present on the model.
|
7
|
+
module Indexes
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :indexes
|
12
|
+
|
13
|
+
self.indexes = []
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def index(name, options = {})
|
18
|
+
name = Array(name).collect(&:to_s).sort.collect(&:to_sym)
|
19
|
+
raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless name.all?{|n| self.fields.include?(n)}
|
20
|
+
self.indexes << name
|
21
|
+
create_indexes
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_indexes
|
25
|
+
self.indexes.each do |index|
|
26
|
+
self.create_table(index_table_name(index), index_key_name(index)) unless self.table_exists?(index_table_name(index))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def index_table_name(index)
|
31
|
+
"#{Dynamoid::Config.namespace}_index_#{index_key_name(index)}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def index_key_name(index)
|
35
|
+
"#{self.to_s.downcase}_#{index.collect(&:to_s).collect(&:pluralize).join('_and_')}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def key_for_index(index, values = [])
|
39
|
+
values = values.collect(&:to_s).sort
|
40
|
+
Digest::SHA2.new.tap do |sha|
|
41
|
+
index.each_with_index {|i, index| sha << values[index] if values[index]}
|
42
|
+
end.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def key_for_index(index)
|
47
|
+
self.class.key_for_index(index, index.collect{|i| self.send(i)})
|
48
|
+
end
|
49
|
+
|
50
|
+
def save_indexes
|
51
|
+
self.class.indexes.each do |index|
|
52
|
+
existing = Dynamoid::Adapter.get_item(self.class.index_table_name(index), self.key_for_index(index))
|
53
|
+
ids = existing ? existing[:ids] : Set.new
|
54
|
+
Dynamoid::Adapter.put_item(self.class.index_table_name(index), {self.class.index_key_name(index).to_sym => self.key_for_index(index), :ids => ids.merge([self.id])})
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
module Dynamoid #:nodoc:
|
5
|
+
|
6
|
+
# This module saves things!
|
7
|
+
module Persistence
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
self.create_table(self.table_name) unless self.table_exists?(self.table_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def save
|
15
|
+
self.id = SecureRandom.uuid if self.id.nil? || self.id.blank?
|
16
|
+
Dynamoid::Adapter.put_item(self.class.table_name, self.attributes)
|
17
|
+
save_indexes
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def table_name
|
22
|
+
"#{Dynamoid::Config.namespace}_#{self.to_s.downcase.pluralize}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_table(table_name, id = :id)
|
26
|
+
Dynamoid::Adapter.create_table(table_name, id.to_sym)
|
27
|
+
end
|
28
|
+
|
29
|
+
def table_exists?(table_name)
|
30
|
+
Dynamoid::Adapter.list_tables.include?(table_name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
|
3
|
+
# encoding: utf-8
|
4
|
+
module Dynamoid #:nodoc:
|
5
|
+
|
6
|
+
# Associate a document with another object: belongs_to, has_many, and has_and_belongs_to_many
|
7
|
+
module Relations
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :indexes
|
12
|
+
|
13
|
+
self.indexes = []
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'dynamoid/adapter/aws_sdk'
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '../../../spec_helper')
|
3
|
+
|
4
|
+
describe Dynamoid::Adapter::AwsSdk do
|
5
|
+
|
6
|
+
if ENV['ACCESS_KEY'] && ENV['SECRET_KEY']
|
7
|
+
|
8
|
+
context 'without a preexisting table' do
|
9
|
+
# CreateTable and DeleteTable
|
10
|
+
it 'performs CreateTable and DeleteTable' do
|
11
|
+
table = Dynamoid::Adapter.create_table('CreateTable', :id)
|
12
|
+
|
13
|
+
Dynamoid::Adapter.connection.tables.collect{|t| t.name}.should include 'CreateTable'
|
14
|
+
|
15
|
+
Dynamoid::Adapter.delete_table('CreateTable')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with a preexisting table' do
|
20
|
+
before(:all) do
|
21
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable1', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable1')
|
22
|
+
Dynamoid::Adapter.create_table('dynamoid_tests_TestTable2', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable2')
|
23
|
+
end
|
24
|
+
|
25
|
+
# GetItem, PutItem and DeleteItem
|
26
|
+
it "performs GetItem for an item that does not exist" do
|
27
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it "performs GetItem for an item that does exist" do
|
31
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
32
|
+
|
33
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:name => 'Josh', :id => '1'}
|
34
|
+
|
35
|
+
Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
|
36
|
+
|
37
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'performs DeleteItem for an item that does not exist' do
|
41
|
+
Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
|
42
|
+
|
43
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'performs PutItem for an item that does not exist' do
|
47
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
48
|
+
|
49
|
+
Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:id => '1', :name => 'Josh'}
|
50
|
+
end
|
51
|
+
|
52
|
+
# BatchGetItem
|
53
|
+
it "performs BatchGetItem with singular keys" do
|
54
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
55
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable2', {:id => '1', :name => 'Justin'})
|
56
|
+
|
57
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => '1', 'dynamoid_tests_TestTable2' => '1')
|
58
|
+
results.size.should == 2
|
59
|
+
results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
|
60
|
+
results['dynamoid_tests_TestTable2'].should include({:name => 'Justin', :id => '1'})
|
61
|
+
end
|
62
|
+
|
63
|
+
it "performs BatchGetItem with multiple keys" do
|
64
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
65
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
|
66
|
+
|
67
|
+
results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => ['1', '2'])
|
68
|
+
results.size.should == 1
|
69
|
+
results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
|
70
|
+
results['dynamoid_tests_TestTable1'].should include({:name => 'Justin', :id => '2'})
|
71
|
+
end
|
72
|
+
|
73
|
+
# ListTables
|
74
|
+
it 'performs ListTables' do
|
75
|
+
Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable1'
|
76
|
+
Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable2'
|
77
|
+
end
|
78
|
+
|
79
|
+
# Query
|
80
|
+
it 'performs query on a table and returns items' do
|
81
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
82
|
+
|
83
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable1', '1').should == { :id=> '1', :name=>"Josh" }
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'performs query on a table and returns items if there are multiple items' do
|
87
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
88
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
|
89
|
+
|
90
|
+
Dynamoid::Adapter.query('dynamoid_tests_TestTable1', '1').should == { :id=> '1', :name=>"Josh" }
|
91
|
+
end
|
92
|
+
|
93
|
+
# Scan
|
94
|
+
it 'performs scan on a table and returns items' do
|
95
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
96
|
+
|
97
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'performs scan on a table and returns items if there are multiple items but only one match' do
|
101
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
102
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
|
103
|
+
|
104
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'performs scan on a table and returns multiple items if there are multiple matches' do
|
108
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
|
109
|
+
Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Josh'})
|
110
|
+
|
111
|
+
Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}]
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
# DescribeTable
|
117
|
+
|
118
|
+
# UpdateItem
|
119
|
+
|
120
|
+
# UpdateTable
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|