dynamoid 3.2.0 → 3.3.0
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 +4 -4
- data/.travis.yml +9 -6
- data/Appraisals +8 -14
- data/CHANGELOG.md +24 -0
- data/README.md +493 -228
- data/gemfiles/rails_4_2.gemfile +5 -7
- data/gemfiles/rails_5_0.gemfile +4 -6
- data/gemfiles/rails_5_1.gemfile +4 -6
- data/gemfiles/rails_5_2.gemfile +4 -6
- data/gemfiles/rails_6_0.gemfile +8 -0
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +3 -10
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +25 -69
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +105 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +9 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +11 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +11 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +3 -2
- data/lib/dynamoid/components.rb +6 -3
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria.rb +1 -1
- data/lib/dynamoid/criteria/chain.rb +33 -6
- data/lib/dynamoid/criteria/key_fields_detector.rb +101 -32
- data/lib/dynamoid/dirty.rb +186 -34
- data/lib/dynamoid/document.rb +8 -216
- data/lib/dynamoid/fields.rb +8 -0
- data/lib/dynamoid/loadable.rb +31 -0
- data/lib/dynamoid/persistence.rb +177 -85
- data/lib/dynamoid/persistence/import.rb +72 -0
- data/lib/dynamoid/persistence/save.rb +63 -0
- data/lib/dynamoid/persistence/update_fields.rb +62 -0
- data/lib/dynamoid/persistence/upsert.rb +60 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +9 -2
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
module Persistence
|
7
|
+
class Import
|
8
|
+
def self.call(model_class, array_of_attributes)
|
9
|
+
new(model_class, array_of_attributes).call
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(model_class, array_of_attributes)
|
13
|
+
@model_class = model_class
|
14
|
+
@array_of_attributes = array_of_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
models = @array_of_attributes.map(&method(:build_model))
|
19
|
+
|
20
|
+
unless Dynamoid.config.backoff
|
21
|
+
import(models)
|
22
|
+
else
|
23
|
+
import_with_backoff(models)
|
24
|
+
end
|
25
|
+
|
26
|
+
models.each { |d| d.new_record = false }
|
27
|
+
models
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def build_model(attributes)
|
33
|
+
attrs = attributes.symbolize_keys
|
34
|
+
|
35
|
+
if Dynamoid::Config.timestamps
|
36
|
+
time_now = DateTime.now.in_time_zone(Time.zone)
|
37
|
+
attrs[:created_at] ||= time_now
|
38
|
+
attrs[:updated_at] ||= time_now
|
39
|
+
end
|
40
|
+
|
41
|
+
@model_class.build(attrs).tap do |model|
|
42
|
+
model.hash_key = SecureRandom.uuid if model.hash_key.blank?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def import_with_backoff(models)
|
47
|
+
backoff = nil
|
48
|
+
table_name = @model_class.table_name
|
49
|
+
items = array_of_dumped_attributes(models)
|
50
|
+
|
51
|
+
Dynamoid.adapter.batch_write_item(table_name, items) do |has_unprocessed_items|
|
52
|
+
if has_unprocessed_items
|
53
|
+
backoff ||= Dynamoid.config.build_backoff
|
54
|
+
backoff.call
|
55
|
+
else
|
56
|
+
backoff = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def import(models)
|
62
|
+
Dynamoid.adapter.batch_write_item(@model_class.table_name, array_of_dumped_attributes(models))
|
63
|
+
end
|
64
|
+
|
65
|
+
def array_of_dumped_attributes(models)
|
66
|
+
models.map do |m|
|
67
|
+
Dumping.dump_attributes(m.attributes, @model_class.attributes)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Persistence
|
5
|
+
class Save
|
6
|
+
def self.call(model)
|
7
|
+
new(model).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(model)
|
11
|
+
@model = model
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
@model.hash_key = SecureRandom.uuid if @model.hash_key.blank?
|
16
|
+
|
17
|
+
# Add an optimistic locking check if the lock_version column exists
|
18
|
+
if @model.class.attributes[:lock_version]
|
19
|
+
@model.lock_version = (@model.lock_version || 0) + 1
|
20
|
+
end
|
21
|
+
|
22
|
+
attributes_dumped = Dumping.dump_attributes(@model.attributes, @model.class.attributes)
|
23
|
+
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
|
24
|
+
|
25
|
+
@model.new_record = false
|
26
|
+
true
|
27
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException => e
|
28
|
+
if @model.new_record?
|
29
|
+
raise Dynamoid::Errors::RecordNotUnique.new(e, @model)
|
30
|
+
else
|
31
|
+
raise Dynamoid::Errors::StaleObjectError.new(@model, 'persist')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Should be called after incrementing `lock_version` attribute
|
38
|
+
def conditions_for_write
|
39
|
+
conditions = {}
|
40
|
+
|
41
|
+
# Add an 'exists' check to prevent overwriting existing records with new ones
|
42
|
+
if @model.new_record?
|
43
|
+
conditions[:unless_exists] = [@model.class.hash_key]
|
44
|
+
if @model.range_key
|
45
|
+
conditions[:unless_exists] << @model.range_key
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add an optimistic locking check if the lock_version column exists
|
50
|
+
if @model.class.attributes[:lock_version]
|
51
|
+
# Uses the original lock_version value from Dirty API
|
52
|
+
# in case user changed 'lock_version' manually
|
53
|
+
if @model.changes[:lock_version][0]
|
54
|
+
conditions[:if] ||= {}
|
55
|
+
conditions[:if][:lock_version] = @model.changes[:lock_version][0]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
conditions
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Persistence
|
5
|
+
class UpdateFields
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(model_class, partition_key:, sort_key:, attributes:, conditions:)
|
11
|
+
@model_class = model_class
|
12
|
+
@partition_key = partition_key
|
13
|
+
@sort_key = sort_key
|
14
|
+
@attributes = attributes.symbolize_keys
|
15
|
+
@conditions = conditions
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
if Dynamoid::Config.timestamps
|
20
|
+
@attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
|
21
|
+
end
|
22
|
+
|
23
|
+
raw_attributes = update_item
|
24
|
+
@model_class.new(undump_attributes(raw_attributes))
|
25
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def update_item
|
31
|
+
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
|
32
|
+
@attributes.each do |k, v|
|
33
|
+
value_casted = TypeCasting.cast_field(v, @model_class.attributes[k])
|
34
|
+
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[k])
|
35
|
+
t.set(k => value_dumped)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def undump_attributes(attributes)
|
41
|
+
Undumping.undump_attributes(attributes, @model_class.attributes)
|
42
|
+
end
|
43
|
+
|
44
|
+
def options_to_update_item
|
45
|
+
options = {}
|
46
|
+
|
47
|
+
if @model_class.range_key
|
48
|
+
value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
|
49
|
+
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
|
50
|
+
options[:range_key] = value_dumped
|
51
|
+
end
|
52
|
+
|
53
|
+
conditions = @conditions.deep_dup
|
54
|
+
conditions[:if_exists] ||= {}
|
55
|
+
conditions[:if_exists][@model_class.hash_key] = @partition_key
|
56
|
+
options[:conditions] = conditions
|
57
|
+
|
58
|
+
options
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Persistence
|
5
|
+
class Upsert
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(model_class, partition_key:, sort_key:, attributes:, conditions:)
|
11
|
+
@model_class = model_class
|
12
|
+
@partition_key = partition_key
|
13
|
+
@sort_key = sort_key
|
14
|
+
@attributes = attributes.symbolize_keys
|
15
|
+
@conditions = conditions
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
if Dynamoid::Config.timestamps
|
20
|
+
@attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
|
21
|
+
end
|
22
|
+
|
23
|
+
raw_attributes = update_item
|
24
|
+
@model_class.new(undump_attributes(raw_attributes))
|
25
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def update_item
|
31
|
+
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
|
32
|
+
@attributes.each do |k, v|
|
33
|
+
value_casted = TypeCasting.cast_field(v, @model_class.attributes[k])
|
34
|
+
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[k])
|
35
|
+
|
36
|
+
t.set(k => value_dumped)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def options_to_update_item
|
42
|
+
options = {}
|
43
|
+
|
44
|
+
if @model_class.range_key
|
45
|
+
value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
|
46
|
+
value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
|
47
|
+
options[:range_key] = value_dumped
|
48
|
+
end
|
49
|
+
|
50
|
+
options[:conditions] = @conditions
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
def undump_attributes(raw_attributes)
|
55
|
+
Undumping.undump_attributes(raw_attributes, @model_class.attributes)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
data/lib/dynamoid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Symonds
|
@@ -21,7 +21,7 @@ authors:
|
|
21
21
|
autorequire:
|
22
22
|
bindir: exe
|
23
23
|
cert_chain: []
|
24
|
-
date: 2019-
|
24
|
+
date: 2019-08-20 00:00:00.000000000 Z
|
25
25
|
dependencies:
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: activemodel
|
@@ -237,9 +237,11 @@ files:
|
|
237
237
|
- gemfiles/rails_5_0.gemfile
|
238
238
|
- gemfiles/rails_5_1.gemfile
|
239
239
|
- gemfiles/rails_5_2.gemfile
|
240
|
+
- gemfiles/rails_6_0.gemfile
|
240
241
|
- lib/dynamoid.rb
|
241
242
|
- lib/dynamoid/adapter.rb
|
242
243
|
- lib/dynamoid/adapter_plugin/aws_sdk_v3.rb
|
244
|
+
- lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb
|
243
245
|
- lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb
|
244
246
|
- lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb
|
245
247
|
- lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb
|
@@ -278,8 +280,13 @@ files:
|
|
278
280
|
- lib/dynamoid/finders.rb
|
279
281
|
- lib/dynamoid/identity_map.rb
|
280
282
|
- lib/dynamoid/indexes.rb
|
283
|
+
- lib/dynamoid/loadable.rb
|
281
284
|
- lib/dynamoid/middleware/identity_map.rb
|
282
285
|
- lib/dynamoid/persistence.rb
|
286
|
+
- lib/dynamoid/persistence/import.rb
|
287
|
+
- lib/dynamoid/persistence/save.rb
|
288
|
+
- lib/dynamoid/persistence/update_fields.rb
|
289
|
+
- lib/dynamoid/persistence/upsert.rb
|
283
290
|
- lib/dynamoid/primary_key_type_mapping.rb
|
284
291
|
- lib/dynamoid/railtie.rb
|
285
292
|
- lib/dynamoid/tasks.rb
|