dynamoid-moda 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +118 -0
- data/Gemfile_activemodel4 +24 -0
- data/Gemfile_activemodel4.lock +88 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +360 -0
- data/Rakefile +93 -0
- data/VERSION +1 -0
- data/doc/.nojekyll +0 -0
- data/doc/Dynamoid.html +328 -0
- data/doc/Dynamoid/Adapter.html +1872 -0
- data/doc/Dynamoid/Adapter/AwsSdk.html +2101 -0
- data/doc/Dynamoid/Adapter/Local.html +1574 -0
- data/doc/Dynamoid/Associations.html +138 -0
- data/doc/Dynamoid/Associations/Association.html +847 -0
- data/doc/Dynamoid/Associations/BelongsTo.html +161 -0
- data/doc/Dynamoid/Associations/ClassMethods.html +766 -0
- data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +167 -0
- data/doc/Dynamoid/Associations/HasMany.html +167 -0
- data/doc/Dynamoid/Associations/HasOne.html +161 -0
- data/doc/Dynamoid/Associations/ManyAssociation.html +1684 -0
- data/doc/Dynamoid/Associations/SingleAssociation.html +627 -0
- data/doc/Dynamoid/Components.html +242 -0
- data/doc/Dynamoid/Config.html +412 -0
- data/doc/Dynamoid/Config/Options.html +638 -0
- data/doc/Dynamoid/Criteria.html +138 -0
- data/doc/Dynamoid/Criteria/Chain.html +1471 -0
- data/doc/Dynamoid/Criteria/ClassMethods.html +105 -0
- data/doc/Dynamoid/Dirty.html +424 -0
- data/doc/Dynamoid/Dirty/ClassMethods.html +174 -0
- data/doc/Dynamoid/Document.html +1033 -0
- data/doc/Dynamoid/Document/ClassMethods.html +1116 -0
- data/doc/Dynamoid/Errors.html +125 -0
- data/doc/Dynamoid/Errors/ConditionalCheckFailedException.html +141 -0
- data/doc/Dynamoid/Errors/DocumentNotValid.html +221 -0
- data/doc/Dynamoid/Errors/Error.html +137 -0
- data/doc/Dynamoid/Errors/InvalidField.html +141 -0
- data/doc/Dynamoid/Errors/InvalidQuery.html +131 -0
- data/doc/Dynamoid/Errors/MissingRangeKey.html +141 -0
- data/doc/Dynamoid/Fields.html +686 -0
- data/doc/Dynamoid/Fields/ClassMethods.html +438 -0
- data/doc/Dynamoid/Finders.html +135 -0
- data/doc/Dynamoid/Finders/ClassMethods.html +943 -0
- data/doc/Dynamoid/IdentityMap.html +492 -0
- data/doc/Dynamoid/IdentityMap/ClassMethods.html +534 -0
- data/doc/Dynamoid/Indexes.html +321 -0
- data/doc/Dynamoid/Indexes/ClassMethods.html +369 -0
- data/doc/Dynamoid/Indexes/Index.html +1142 -0
- data/doc/Dynamoid/Middleware.html +115 -0
- data/doc/Dynamoid/Middleware/IdentityMap.html +264 -0
- data/doc/Dynamoid/Persistence.html +892 -0
- data/doc/Dynamoid/Persistence/ClassMethods.html +836 -0
- data/doc/Dynamoid/Validations.html +415 -0
- data/doc/_index.html +506 -0
- data/doc/class_list.html +53 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +338 -0
- data/doc/file.LICENSE.html +73 -0
- data/doc/file.README.html +416 -0
- data/doc/file_list.html +58 -0
- data/doc/frames.html +28 -0
- data/doc/index.html +416 -0
- data/doc/js/app.js +214 -0
- data/doc/js/full_list.js +178 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +1144 -0
- data/doc/top-level-namespace.html +112 -0
- data/dynamoid-moda.gemspec +210 -0
- data/dynamoid.gemspec +208 -0
- data/lib/dynamoid.rb +46 -0
- data/lib/dynamoid/adapter.rb +267 -0
- data/lib/dynamoid/adapter/aws_sdk.rb +309 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/associations/association.rb +105 -0
- data/lib/dynamoid/associations/belongs_to.rb +44 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
- data/lib/dynamoid/associations/has_many.rb +39 -0
- data/lib/dynamoid/associations/has_one.rb +39 -0
- data/lib/dynamoid/associations/many_association.rb +191 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config.rb +57 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/criteria/chain.rb +326 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +199 -0
- data/lib/dynamoid/errors.rb +28 -0
- data/lib/dynamoid/fields.rb +138 -0
- data/lib/dynamoid/finders.rb +133 -0
- data/lib/dynamoid/identity_map.rb +96 -0
- data/lib/dynamoid/indexes.rb +69 -0
- data/lib/dynamoid/indexes/index.rb +103 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +292 -0
- data/lib/dynamoid/validations.rb +36 -0
- data/spec/app/models/address.rb +13 -0
- data/spec/app/models/camel_case.rb +34 -0
- data/spec/app/models/car.rb +6 -0
- data/spec/app/models/magazine.rb +11 -0
- data/spec/app/models/message.rb +9 -0
- data/spec/app/models/nuclear_submarine.rb +5 -0
- data/spec/app/models/sponsor.rb +8 -0
- data/spec/app/models/subscription.rb +12 -0
- data/spec/app/models/tweet.rb +12 -0
- data/spec/app/models/user.rb +26 -0
- data/spec/app/models/vehicle.rb +7 -0
- data/spec/dynamoid/adapter/aws_sdk_spec.rb +376 -0
- data/spec/dynamoid/adapter_spec.rb +155 -0
- data/spec/dynamoid/associations/association_spec.rb +194 -0
- data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
- data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
- data/spec/dynamoid/associations/has_many_spec.rb +42 -0
- data/spec/dynamoid/associations/has_one_spec.rb +45 -0
- data/spec/dynamoid/associations_spec.rb +16 -0
- data/spec/dynamoid/config_spec.rb +27 -0
- data/spec/dynamoid/criteria/chain_spec.rb +210 -0
- data/spec/dynamoid/criteria_spec.rb +75 -0
- data/spec/dynamoid/dirty_spec.rb +57 -0
- data/spec/dynamoid/document_spec.rb +180 -0
- data/spec/dynamoid/fields_spec.rb +156 -0
- data/spec/dynamoid/finders_spec.rb +147 -0
- data/spec/dynamoid/identity_map_spec.rb +45 -0
- data/spec/dynamoid/indexes/index_spec.rb +104 -0
- data/spec/dynamoid/indexes_spec.rb +25 -0
- data/spec/dynamoid/persistence_spec.rb +301 -0
- data/spec/dynamoid/validations_spec.rb +36 -0
- data/spec/dynamoid_spec.rb +14 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/support/with_partitioning.rb +15 -0
- metadata +363 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Dynamoid
|
|
2
|
+
module IdentityMap
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def self.clear
|
|
6
|
+
models.each { |m| m.identity_map.clear }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.models
|
|
10
|
+
Dynamoid::Config.included_models
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
def identity_map
|
|
15
|
+
@identity_map ||= {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def from_database(attrs = {})
|
|
19
|
+
return super if identity_map_off?
|
|
20
|
+
|
|
21
|
+
key = identity_map_key(attrs)
|
|
22
|
+
document = identity_map[key]
|
|
23
|
+
|
|
24
|
+
if document.nil?
|
|
25
|
+
document = super
|
|
26
|
+
identity_map[key] = document
|
|
27
|
+
else
|
|
28
|
+
document.load(attrs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
document
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def find_by_id(id, options = {})
|
|
35
|
+
return super if identity_map_off?
|
|
36
|
+
|
|
37
|
+
key = id.to_s
|
|
38
|
+
|
|
39
|
+
if range_key = options[:range_key]
|
|
40
|
+
key += "::#{range_key}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if identity_map[key]
|
|
44
|
+
identity_map[key]
|
|
45
|
+
else
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def identity_map_key(attrs)
|
|
51
|
+
key = attrs[hash_key].to_s
|
|
52
|
+
if range_key
|
|
53
|
+
key += "::#{attrs[range_key]}"
|
|
54
|
+
end
|
|
55
|
+
key
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def identity_map_on?
|
|
59
|
+
Dynamoid::Config.identity_map
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def identity_map_off?
|
|
63
|
+
!identity_map_on?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def identity_map
|
|
68
|
+
self.class.identity_map
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def save(*args)
|
|
72
|
+
return super if self.class.identity_map_off?
|
|
73
|
+
|
|
74
|
+
if result = super
|
|
75
|
+
identity_map[identity_map_key] = self
|
|
76
|
+
end
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def delete
|
|
81
|
+
return super if self.class.identity_map_off?
|
|
82
|
+
|
|
83
|
+
identity_map.delete(identity_map_key)
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def identity_map_key
|
|
89
|
+
key = hash_key.to_s
|
|
90
|
+
if self.class.range_key
|
|
91
|
+
key += "::#{range_value}"
|
|
92
|
+
end
|
|
93
|
+
key
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require 'dynamoid/indexes/index'
|
|
3
|
+
|
|
4
|
+
module Dynamoid #:nodoc:
|
|
5
|
+
|
|
6
|
+
# Indexes are quick ways of performing queries by anything other than id in DynamoDB. They are denormalized tables;
|
|
7
|
+
# that is, data is duplicated in the initial table (where the object is saved) and the index table (where
|
|
8
|
+
# we perform indexing).
|
|
9
|
+
module Indexes
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
# Make some helpful attributes to persist indexes.
|
|
13
|
+
included do
|
|
14
|
+
class_attribute :indexes
|
|
15
|
+
|
|
16
|
+
self.indexes = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ClassMethods
|
|
20
|
+
|
|
21
|
+
# The call to create an index. Generates a new index with the specified options -- for more information, see Dynamoid::Indexes::Index.
|
|
22
|
+
# This function also attempts to immediately create the indexing table if it does not exist already.
|
|
23
|
+
#
|
|
24
|
+
# @since 0.2.0
|
|
25
|
+
def index(name, options = {})
|
|
26
|
+
index = Dynamoid::Indexes::Index.new(self, name, options)
|
|
27
|
+
self.indexes[index.name] = index
|
|
28
|
+
create_indexes
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Helper function to find indexes.
|
|
32
|
+
#
|
|
33
|
+
# @since 0.2.0
|
|
34
|
+
def find_index(index)
|
|
35
|
+
self.indexes[Array(index).collect(&:to_s).sort.collect(&:to_sym)]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Helper function to create indexes (if they don't exist already).
|
|
39
|
+
#
|
|
40
|
+
# @since 0.2.0
|
|
41
|
+
def create_indexes
|
|
42
|
+
self.indexes.each do |name, index|
|
|
43
|
+
opts = {:table_name => index.table_name, :id => :id}
|
|
44
|
+
opts[:range_key] = { :range => :number } if index.range_key?
|
|
45
|
+
self.create_table(opts)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Callback for an object to save itself to each of a class' indexes.
|
|
51
|
+
#
|
|
52
|
+
# @since 0.2.0
|
|
53
|
+
def save_indexes
|
|
54
|
+
self.class.indexes.each do |name, index|
|
|
55
|
+
index.save(self)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Callback for an object to delete itself from each of a class' indexes.
|
|
60
|
+
#
|
|
61
|
+
# @since 0.2.0
|
|
62
|
+
def delete_indexes
|
|
63
|
+
self.class.indexes.each do |name, index|
|
|
64
|
+
index.delete(self)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
module Dynamoid #:nodoc:
|
|
3
|
+
module Indexes
|
|
4
|
+
|
|
5
|
+
# The class contains all the information an index contains, including its keys and which attributes it covers.
|
|
6
|
+
class Index
|
|
7
|
+
attr_accessor :source, :name, :hash_keys, :range_keys
|
|
8
|
+
alias_method :range_key?, :range_keys
|
|
9
|
+
|
|
10
|
+
# Create a new index. Pass either :range => true or :range => :column_name to create a ranged index on that column.
|
|
11
|
+
#
|
|
12
|
+
# @param [Class] source the source class for the index
|
|
13
|
+
# @param [Symbol] name the name of the index
|
|
14
|
+
#
|
|
15
|
+
# @since 0.2.0
|
|
16
|
+
def initialize(source, name, options = {})
|
|
17
|
+
@source = source
|
|
18
|
+
|
|
19
|
+
if options.delete(:range)
|
|
20
|
+
@range_keys = sort(name)
|
|
21
|
+
elsif options[:range_key]
|
|
22
|
+
@range_keys = sort(options[:range_key])
|
|
23
|
+
end
|
|
24
|
+
@hash_keys = sort(name)
|
|
25
|
+
@name = sort([hash_keys, range_keys])
|
|
26
|
+
|
|
27
|
+
raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless keys.all?{|n| source.attributes.include?(n)}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sort objects into alphabetical strings, used for composing index names correctly (since we always assume they're alphabetical).
|
|
31
|
+
#
|
|
32
|
+
# @example find all users by first and last name
|
|
33
|
+
# sort([:gamma, :alpha, :beta, :omega]) # => [:alpha, :beta, :gamma, :omega]
|
|
34
|
+
#
|
|
35
|
+
# @since 0.2.0
|
|
36
|
+
def sort(objs)
|
|
37
|
+
Array(objs).flatten.compact.uniq.collect(&:to_s).sort.collect(&:to_sym)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Return the array of keys this index uses for its table.
|
|
41
|
+
#
|
|
42
|
+
# @since 0.2.0
|
|
43
|
+
def keys
|
|
44
|
+
[Array(hash_keys) + Array(range_keys)].flatten.uniq
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Return the table name for this index.
|
|
48
|
+
#
|
|
49
|
+
# @since 0.2.0
|
|
50
|
+
def table_name
|
|
51
|
+
"#{Dynamoid::Config.namespace}_index_" + source.table_name.sub("#{Dynamoid::Config.namespace}_", '').singularize + "_#{name.collect(&:to_s).collect(&:pluralize).join('_and_')}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Given either an object or a list of attributes, generate a hash key and a range key for the index. Optionally pass in
|
|
55
|
+
# true to changed_attributes for a list of all the object's dirty attributes in convenient index form (for deleting stale
|
|
56
|
+
# information from the indexes).
|
|
57
|
+
#
|
|
58
|
+
# @param [Object] attrs either an object that responds to :attributes, or a hash of attributes
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash] a hash with the keys :hash_value and :range_value
|
|
61
|
+
#
|
|
62
|
+
# @since 0.2.0
|
|
63
|
+
def values(attrs, changed_attributes = false)
|
|
64
|
+
if changed_attributes
|
|
65
|
+
hash = {}
|
|
66
|
+
attrs.changes.each {|k, v| hash[k.to_sym] = (v.first || v.last)}
|
|
67
|
+
attrs = hash
|
|
68
|
+
end
|
|
69
|
+
attrs = attrs.send(:attributes) if attrs.respond_to?(:attributes)
|
|
70
|
+
{}.tap do |hash|
|
|
71
|
+
hash[:hash_value] = hash_keys.collect{|key| attrs[key]}.join('.')
|
|
72
|
+
hash[:range_value] = range_keys.inject(0.0) {|sum, key| sum + attrs[key].to_f} if self.range_key?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Save an object to this index, merging it with existing ids if there's already something present at this index location.
|
|
77
|
+
# First, though, delete this object from its old indexes (so the object isn't listed in an erroneous index).
|
|
78
|
+
#
|
|
79
|
+
# @since 0.2.0
|
|
80
|
+
def save(obj)
|
|
81
|
+
self.delete(obj, true)
|
|
82
|
+
values = values(obj)
|
|
83
|
+
return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
|
|
84
|
+
existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], { :range_key => values[:range_value] })
|
|
85
|
+
ids = ((existing and existing[:ids]) or Set.new)
|
|
86
|
+
Dynamoid::Adapter.write(self.table_name, {:id => values[:hash_value], :ids => ids.merge([obj.id]), :range => values[:range_value]})
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Delete an object from this index, preserving existing ids if there are any, and failing gracefully if for some reason the
|
|
90
|
+
# index doesn't already have this object in it.
|
|
91
|
+
#
|
|
92
|
+
# @since 0.2.0
|
|
93
|
+
def delete(obj, changed_attributes = false)
|
|
94
|
+
values = values(obj, changed_attributes)
|
|
95
|
+
return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
|
|
96
|
+
existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], { :range_key => values[:range_value]})
|
|
97
|
+
return true unless existing && existing[:ids] && existing[:ids].include?(obj.id)
|
|
98
|
+
Dynamoid::Adapter.write(self.table_name, {:id => values[:hash_value], :ids => (existing[:ids] - Set[obj.id]), :range => values[:range_value]})
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
|
|
3
|
+
# encoding: utf-8
|
|
4
|
+
module Dynamoid
|
|
5
|
+
|
|
6
|
+
# Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
|
|
7
|
+
# values to be of the same type as when they were passed in, based on the fields in the class.
|
|
8
|
+
module Persistence
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
attr_accessor :new_record
|
|
12
|
+
alias :new_record? :new_record
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
|
|
16
|
+
def table_name
|
|
17
|
+
@table_name ||= "#{Dynamoid::Config.namespace}_#{options[:name] || base_class.name.split('::').last.downcase.pluralize}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Creates a table.
|
|
21
|
+
#
|
|
22
|
+
# @param [Hash] options options to pass for table creation
|
|
23
|
+
# @option options [Symbol] :id the id field for the table
|
|
24
|
+
# @option options [Symbol] :table_name the actual name for the table
|
|
25
|
+
# @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
|
|
26
|
+
# @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
|
|
27
|
+
# @option options [Hash] {range_key => :type} a hash of the name of the range key and a symbol of its type
|
|
28
|
+
#
|
|
29
|
+
# @since 0.4.0
|
|
30
|
+
def create_table(options = {})
|
|
31
|
+
if self.range_key
|
|
32
|
+
range_key_hash = { range_key => dynamo_type(attributes[range_key][:type]) }
|
|
33
|
+
else
|
|
34
|
+
range_key_hash = nil
|
|
35
|
+
end
|
|
36
|
+
options = {
|
|
37
|
+
:id => self.hash_key,
|
|
38
|
+
:table_name => self.table_name,
|
|
39
|
+
:write_capacity => self.write_capacity,
|
|
40
|
+
:read_capacity => self.read_capacity,
|
|
41
|
+
:range_key => range_key_hash
|
|
42
|
+
}.merge(options)
|
|
43
|
+
|
|
44
|
+
return true if table_exists?(options[:table_name])
|
|
45
|
+
|
|
46
|
+
Dynamoid::Adapter.tables << options[:table_name] if Dynamoid::Adapter.create_table(options[:table_name], options[:id], options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Does a table with this name exist?
|
|
50
|
+
#
|
|
51
|
+
# @since 0.2.0
|
|
52
|
+
def table_exists?(table_name)
|
|
53
|
+
Dynamoid::Adapter.tables ? Dynamoid::Adapter.tables.include?(table_name) : false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def from_database(attrs = {})
|
|
57
|
+
clazz = attrs[:type] ? obj = attrs[:type].constantize : self
|
|
58
|
+
clazz.new(attrs).tap { |r| r.new_record = false }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
|
|
62
|
+
#
|
|
63
|
+
# @since 0.2.0
|
|
64
|
+
def undump(incoming = nil)
|
|
65
|
+
incoming = (incoming || {}).symbolize_keys
|
|
66
|
+
Hash.new.tap do |hash|
|
|
67
|
+
self.attributes.each do |attribute, options|
|
|
68
|
+
hash[attribute] = undump_field(incoming[attribute], options)
|
|
69
|
+
end
|
|
70
|
+
incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Undump a value for a given type. Given a string, it'll determine (based on the type provided) whether to turn it into a
|
|
75
|
+
# string, integer, float, set, array, datetime, or serialized return value.
|
|
76
|
+
#
|
|
77
|
+
# @since 0.2.0
|
|
78
|
+
def undump_field(value, options)
|
|
79
|
+
if value.nil? && (default_value = options[:default])
|
|
80
|
+
value = default_value.respond_to?(:call) ? default_value.call : default_value
|
|
81
|
+
else
|
|
82
|
+
return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
case options[:type]
|
|
86
|
+
when :string
|
|
87
|
+
value.to_s
|
|
88
|
+
when :integer
|
|
89
|
+
value.to_i
|
|
90
|
+
when :float
|
|
91
|
+
value.to_f
|
|
92
|
+
when :set, :array
|
|
93
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
|
94
|
+
value
|
|
95
|
+
else
|
|
96
|
+
Set[value]
|
|
97
|
+
end
|
|
98
|
+
when :datetime
|
|
99
|
+
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
|
100
|
+
value
|
|
101
|
+
else
|
|
102
|
+
Time.at(value).to_datetime
|
|
103
|
+
end
|
|
104
|
+
when :serialized
|
|
105
|
+
if value.is_a?(String)
|
|
106
|
+
options[:serializer] ? options[:serializer].load(value) : YAML.load(value)
|
|
107
|
+
else
|
|
108
|
+
value
|
|
109
|
+
end
|
|
110
|
+
when :boolean
|
|
111
|
+
# persisted as 't', but because undump is called during initialize it can come in as true
|
|
112
|
+
if value == 't' || value == true
|
|
113
|
+
true
|
|
114
|
+
elsif value == 'f' || value == false
|
|
115
|
+
false
|
|
116
|
+
else
|
|
117
|
+
raise ArgumentError, "Boolean column neither true nor false"
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def dynamo_type(type)
|
|
125
|
+
case type
|
|
126
|
+
when :integer, :float, :datetime
|
|
127
|
+
:number
|
|
128
|
+
when :string, :serialized
|
|
129
|
+
:string
|
|
130
|
+
else
|
|
131
|
+
raise 'unknown type'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
|
|
138
|
+
#
|
|
139
|
+
def touch(name = nil)
|
|
140
|
+
now = DateTime.now
|
|
141
|
+
self.updated_at = now
|
|
142
|
+
attributes[name] = now if name
|
|
143
|
+
save
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
|
|
147
|
+
#
|
|
148
|
+
# @since 0.2.0
|
|
149
|
+
def persisted?
|
|
150
|
+
!new_record?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Run the callbacks and then persist this object in the datastore.
|
|
154
|
+
#
|
|
155
|
+
# @since 0.2.0
|
|
156
|
+
def save(options = {})
|
|
157
|
+
self.class.create_table
|
|
158
|
+
|
|
159
|
+
if new_record?
|
|
160
|
+
conditions = { :unless_exists => [self.class.hash_key]}
|
|
161
|
+
conditions[:unless_exists] << range_key if(range_key)
|
|
162
|
+
|
|
163
|
+
run_callbacks(:create) { persist(conditions) }
|
|
164
|
+
else
|
|
165
|
+
persist
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
#
|
|
172
|
+
# update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
|
|
173
|
+
# never cause an update! to fail, but an update! may cause a concurrent save to fail.
|
|
174
|
+
#
|
|
175
|
+
#
|
|
176
|
+
def update!(conditions = {}, &block)
|
|
177
|
+
run_callbacks(:update) do
|
|
178
|
+
options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
|
|
179
|
+
new_attrs = Dynamoid::Adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions)) do |t|
|
|
180
|
+
if(self.class.attributes[:lock_version])
|
|
181
|
+
raise "Optimistic locking cannot be used with Partitioning" if(Dynamoid::Config.partitioning)
|
|
182
|
+
t.add(lock_version: 1)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
yield t
|
|
186
|
+
end
|
|
187
|
+
load(new_attrs)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def update(conditions = {}, &block)
|
|
192
|
+
update!(conditions, &block)
|
|
193
|
+
true
|
|
194
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Delete this object, but only after running callbacks for it.
|
|
199
|
+
#
|
|
200
|
+
# @since 0.2.0
|
|
201
|
+
def destroy
|
|
202
|
+
run_callbacks(:destroy) do
|
|
203
|
+
self.delete
|
|
204
|
+
end
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Delete this object from the datastore and all indexes.
|
|
209
|
+
#
|
|
210
|
+
# @since 0.2.0
|
|
211
|
+
def delete
|
|
212
|
+
delete_indexes
|
|
213
|
+
options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
|
|
214
|
+
Dynamoid::Adapter.delete(self.class.table_name, self.hash_key, options)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Dump this object's attributes into hash form, fit to be persisted into the datastore.
|
|
218
|
+
#
|
|
219
|
+
# @since 0.2.0
|
|
220
|
+
def dump
|
|
221
|
+
Hash.new.tap do |hash|
|
|
222
|
+
self.class.attributes.each do |attribute, options|
|
|
223
|
+
hash[attribute] = dump_field(self.read_attribute(attribute), options)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
# Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
|
|
231
|
+
# persisted into the datastore.
|
|
232
|
+
#
|
|
233
|
+
# @since 0.2.0
|
|
234
|
+
def dump_field(value, options)
|
|
235
|
+
return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
236
|
+
|
|
237
|
+
case options[:type]
|
|
238
|
+
when :string
|
|
239
|
+
value.to_s
|
|
240
|
+
when :integer
|
|
241
|
+
value.to_i
|
|
242
|
+
when :float
|
|
243
|
+
value.to_f
|
|
244
|
+
when :set, :array
|
|
245
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
|
246
|
+
value
|
|
247
|
+
else
|
|
248
|
+
Set[value]
|
|
249
|
+
end
|
|
250
|
+
when :datetime
|
|
251
|
+
value.to_time.to_f
|
|
252
|
+
when :serialized
|
|
253
|
+
options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
|
|
254
|
+
when :boolean
|
|
255
|
+
value.to_s[0]
|
|
256
|
+
else
|
|
257
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Persist the object into the datastore. Assign it an id first if it doesn't have one; then afterwards,
|
|
262
|
+
# save its indexes.
|
|
263
|
+
#
|
|
264
|
+
# @since 0.2.0
|
|
265
|
+
def persist(conditions = nil)
|
|
266
|
+
run_callbacks(:save) do
|
|
267
|
+
self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank?
|
|
268
|
+
|
|
269
|
+
# Add an exists check to prevent overwriting existing records with new ones
|
|
270
|
+
if(new_record?)
|
|
271
|
+
conditions ||= {}
|
|
272
|
+
(conditions[:unless_exists] ||= []) << self.class.hash_key
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Add an optimistic locking check if the lock_version column exists
|
|
276
|
+
if(self.class.attributes[:lock_version])
|
|
277
|
+
conditions ||= {}
|
|
278
|
+
raise "Optimistic locking cannot be used with Partitioning" if(Dynamoid::Config.partitioning)
|
|
279
|
+
self.lock_version = (lock_version || 0) + 1
|
|
280
|
+
#Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
|
|
281
|
+
(conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0])
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
Dynamoid::Adapter.write(self.class.table_name, self.dump, conditions)
|
|
285
|
+
save_indexes
|
|
286
|
+
@new_record = false
|
|
287
|
+
true
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
end
|