dynomite 1.2.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +18 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +45 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +178 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +12 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- data/lib/dynomite/migration/executor.rb +0 -38
@@ -0,0 +1,90 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Read
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include Find
|
5
|
+
include FindWithEvent
|
6
|
+
include Query
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
# Override Enumerable#first to limit to 1 item as optimization
|
10
|
+
def first
|
11
|
+
all.limit(1).first
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds very little wrapper logic to scan.
|
15
|
+
#
|
16
|
+
# * Automatically add table_name to options for convenience.
|
17
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
18
|
+
# dynamodb client response.
|
19
|
+
#
|
20
|
+
# Other than that, usage is same was using the dynamodb client scan method
|
21
|
+
# directly. Example:
|
22
|
+
#
|
23
|
+
# MyModel.scan(
|
24
|
+
# expression_attribute_names: {"#updated_at"=>"updated_at"},
|
25
|
+
# expression_attribute_values: {
|
26
|
+
# ":start_time" => "2010-01-01T00:00:00",
|
27
|
+
# ":end_time" => "2020-01-01T00:00:00"
|
28
|
+
# },
|
29
|
+
# filter_expression: "#updated_at between :start_time and :end_time",
|
30
|
+
# )
|
31
|
+
#
|
32
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
33
|
+
def scan(params={})
|
34
|
+
params = { table_name: table_name }.merge(params)
|
35
|
+
resp = client.scan(params)
|
36
|
+
logger.info("REQUEST: #{params}")
|
37
|
+
resp.items.map {|i| self.new(i) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Adds very little wrapper logic to query.
|
41
|
+
#
|
42
|
+
# * Automatically add table_name to options for convenience.
|
43
|
+
# * Decorates return value. Returns Array of [MyModel.new] instead of the
|
44
|
+
# dynamodb client response.
|
45
|
+
#
|
46
|
+
# Other than that, usage is same was using the dynamodb client query method
|
47
|
+
# directly. Example:
|
48
|
+
#
|
49
|
+
# MyModel.query(
|
50
|
+
# index_name: 'category-index',
|
51
|
+
# expression_attribute_names: { "#category_name" => "category" },
|
52
|
+
# expression_attribute_values: { ":category_value" => "Entertainment" },
|
53
|
+
# key_condition_expression: "#category_name = :category_value",
|
54
|
+
# )
|
55
|
+
#
|
56
|
+
# AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
|
57
|
+
def query(params={})
|
58
|
+
params = { table_name: table_name }.merge(params)
|
59
|
+
resp = client.query(params)
|
60
|
+
resp.items.map { |i| self.new(i) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def count
|
64
|
+
if Dynomite.config.default_count_method.to_sym == :item_count
|
65
|
+
item_count
|
66
|
+
else
|
67
|
+
scan_count
|
68
|
+
end
|
69
|
+
end
|
70
|
+
alias_method :size, :count
|
71
|
+
|
72
|
+
def scan_count
|
73
|
+
warn_scan <<~EOL
|
74
|
+
WARN: Using scan to count. Though it is more accurate.
|
75
|
+
It can be slow and expensive. You can use item_count instead.
|
76
|
+
Note: item_count may be stale for about 6 hours.
|
77
|
+
You can set the Dynomite.config.default_count_method = :item_count to make it the default.
|
78
|
+
EOL
|
79
|
+
all.count
|
80
|
+
end
|
81
|
+
|
82
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Table.html#item_count-instance_method
|
83
|
+
# DynamoDB updates this value approximately every six hours.
|
84
|
+
def item_count
|
85
|
+
table = Aws::DynamoDB::Table.new(name: table_name, client: client)
|
86
|
+
table.item_count # fast but can be stale
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module Sti
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :inheritance_field_name
|
7
|
+
end
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def enable_sti(field_name='type')
|
11
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
12
|
+
def self.inherited(subclass)
|
13
|
+
field :#{field_name} # IE: field_name :type
|
14
|
+
subclass.table_name(sti_base_table_name) # IE: subclass: Car base_table: vehicles
|
15
|
+
subclass.inheritance_field_name = :#{field_name}
|
16
|
+
|
17
|
+
before_save :set_type
|
18
|
+
super
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
alias inheritance_field enable_sti
|
23
|
+
|
24
|
+
def sti_base_table_name
|
25
|
+
klass = self
|
26
|
+
table_name = nil
|
27
|
+
until klass.abstract? # IE: ApplicationItem
|
28
|
+
table_name = klass.name.pluralize.gsub('::','_').underscore # vehicles
|
29
|
+
klass = klass.superclass
|
30
|
+
end
|
31
|
+
table_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def sti_enabled?
|
35
|
+
inheritance_field_name.present?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_type
|
40
|
+
self[self.class.inheritance_field_name] = self.class.name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Dynomite::Item
|
2
|
+
module TableNamespace
|
3
|
+
def table_name(*args)
|
4
|
+
case args.size
|
5
|
+
when 0
|
6
|
+
get_table_name
|
7
|
+
when 1
|
8
|
+
set_table_name(args[0])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def set_table_name(value)
|
13
|
+
@table_name = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_table_name
|
17
|
+
@table_name ||= self.name.pluralize.gsub('::','_').underscore
|
18
|
+
[namespace, @table_name].reject {|s| s.nil? || s.empty?}.join(namespace_separator)
|
19
|
+
end
|
20
|
+
|
21
|
+
def namespace(*args)
|
22
|
+
case args.size
|
23
|
+
when 0
|
24
|
+
get_namespace
|
25
|
+
when 1
|
26
|
+
set_namespace(args[0])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_namespace
|
31
|
+
return @namespace if defined?(@namespace)
|
32
|
+
@namespace = Dynomite.config.namespace || Dynomite.config.default_namespace
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_namespace(value)
|
36
|
+
@namespace = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def namespace_separator
|
40
|
+
Dynomite.config.namespace_separator || '_'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
# aws-sdk-dynamodb handles typecast heavy-lifting. Adds typecasting support for DateTime objects.
|
4
|
+
class Dynomite::Item
|
5
|
+
class Typecaster
|
6
|
+
def initialize(model)
|
7
|
+
@model = model
|
8
|
+
end
|
9
|
+
|
10
|
+
def dump(data, depth=0)
|
11
|
+
case data
|
12
|
+
when Array
|
13
|
+
data.map! { |v| dump(v, depth+1) }
|
14
|
+
when Hash
|
15
|
+
data.each_with_object({}) do |(k,v), dumped|
|
16
|
+
if depth == 0
|
17
|
+
v = cast_to_attribute_type(k, v) # cast to attribute type if defined
|
18
|
+
end
|
19
|
+
dumped[k] = dump(v, depth+1)
|
20
|
+
dumped
|
21
|
+
end
|
22
|
+
else
|
23
|
+
data # pass through
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# IE: field :price, type: :integer
|
28
|
+
# For most cases, we rely on aws-sdk-dynamodb to do the typecasting by inference.
|
29
|
+
#
|
30
|
+
# The method also helps keep track of where we cast_to_type
|
31
|
+
# It's only a few spots this provides an easy to search for it.
|
32
|
+
# See: https://rubyonjets.com/docs/database/dynamodb/model/typecasting/
|
33
|
+
FALSEY = [false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF']
|
34
|
+
def cast_to_type(type, value, on: :read)
|
35
|
+
case type
|
36
|
+
when :integer
|
37
|
+
value.to_i
|
38
|
+
when :boolean
|
39
|
+
!FALSEY.include?(value)
|
40
|
+
when :time
|
41
|
+
cast_to_time(value, on: on)
|
42
|
+
when :string
|
43
|
+
value.to_s # force to string
|
44
|
+
else # :infer
|
45
|
+
value # passthrough and let aws-sdk-dynamodb handle it
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# datetime to string
|
50
|
+
def cast_to_time(value, on: :read)
|
51
|
+
if on == :read
|
52
|
+
if value.is_a?(String)
|
53
|
+
Time.parse(value) # 2023-08-26T14:35:37Z
|
54
|
+
elsif value.respond_to?(:to_datetime) # time-like object already Time or DateTime
|
55
|
+
value
|
56
|
+
end
|
57
|
+
else # write or raw (for querying)
|
58
|
+
if value.respond_to?(:to_datetime) && !value.is_a?(String)
|
59
|
+
value.utc.strftime('%Y-%m-%dT%TZ') # Timestamp format iso8601 from AWS docs: http://amzn.to/2z98Bdc
|
60
|
+
else
|
61
|
+
value # passthrough string
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# string to float if attribute_type is N
|
67
|
+
# number to string if attribute_type is S
|
68
|
+
def cast_to_attribute_type(attribute_name, attribute_value)
|
69
|
+
definition = @model.attribute_definitions.find { |d| d[:attribute_name] == attribute_name.to_s }
|
70
|
+
if definition
|
71
|
+
case definition[:attribute_type]
|
72
|
+
when "N" # Number
|
73
|
+
attribute_value.to_f
|
74
|
+
when "S" # String
|
75
|
+
attribute_value.to_s
|
76
|
+
when "BOOL" # Boolean
|
77
|
+
attribute_value == true
|
78
|
+
else
|
79
|
+
attribute_value # passthrough
|
80
|
+
end
|
81
|
+
else
|
82
|
+
attribute_value # passthrough
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def load(data)
|
87
|
+
case data
|
88
|
+
when Array
|
89
|
+
data.map! { |v| load(v) }
|
90
|
+
when Hash
|
91
|
+
data.each_with_object({}) do |(k,v), loaded|
|
92
|
+
loaded[k] = load(v)
|
93
|
+
loaded
|
94
|
+
end
|
95
|
+
else
|
96
|
+
load_item(data)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
REGEXP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
|
101
|
+
def load_item(obj)
|
102
|
+
return obj unless obj.is_a?(String)
|
103
|
+
obj.match(REGEXP) ? Time.parse(obj) : obj
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dynomite::Item::Write
|
2
|
+
class Base
|
3
|
+
include Dynomite::Client
|
4
|
+
|
5
|
+
# The attributes are in model.attrs and are held by reference
|
6
|
+
# The options are the client.delete_item or client.put_item options.
|
7
|
+
def self.call(model, options={})
|
8
|
+
new(model, options).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(model, options={})
|
12
|
+
@model, @options = model, options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Dynomite::Item::Write
|
2
|
+
class DeleteItem < Base
|
3
|
+
def call
|
4
|
+
key = @model.attrs.slice(@model.class.partition_key_field, @model.class.sort_key_field)
|
5
|
+
params = {
|
6
|
+
table_name: @model.class.table_name,
|
7
|
+
key: key
|
8
|
+
}
|
9
|
+
# In case you want to specify condition_expression or expression_attribute_values
|
10
|
+
params = params.merge(@options)
|
11
|
+
client.delete_item(params) # resp
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Dynomite::Item::Write
|
2
|
+
class PutItem < Base
|
3
|
+
def call
|
4
|
+
# typecaster will convert the attrs to the correct types for saving to DynamoDB
|
5
|
+
item = Dynomite::Item::Typecaster.new(@model).dump(permitted_attrs)
|
6
|
+
@params = {
|
7
|
+
table_name: @model.class.table_name,
|
8
|
+
item: item
|
9
|
+
}
|
10
|
+
@params.merge!(check_unique_params)
|
11
|
+
@params.merge!(locking_params)
|
12
|
+
|
13
|
+
# put_item replaces the item fully. The resp does not contain the attrs.
|
14
|
+
log_debug(@params)
|
15
|
+
begin
|
16
|
+
client.put_item(@params)
|
17
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
18
|
+
handle_conditional_check_failed_exception(e)
|
19
|
+
rescue Aws::DynamoDB::Errors::ValidationException => e
|
20
|
+
@model.reset_lock_version_was if @model.class.locking_enabled?
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
|
24
|
+
@model.new_record = false
|
25
|
+
@model
|
26
|
+
end
|
27
|
+
|
28
|
+
def permitted_attrs
|
29
|
+
field_names = @model.class.field_names.map(&:to_sym)
|
30
|
+
assigned_fields = @model.attrs.keys.map(&:to_sym)
|
31
|
+
undeclared_fields = assigned_fields - field_names
|
32
|
+
declared_fields = field_names - assigned_fields
|
33
|
+
|
34
|
+
case Dynomite.config.undeclared_field_behavior.to_sym
|
35
|
+
when :allow
|
36
|
+
@model.attrs # allow
|
37
|
+
when :silent
|
38
|
+
@model.attrs.slice(*field_names)
|
39
|
+
when :error
|
40
|
+
unless undeclared_fields.empty?
|
41
|
+
raise Dynomite::Error::UndeclaredFields.new("ERROR: Saving undeclared fields not allowed: #{undeclared_fields} for #{@model.class}")
|
42
|
+
end
|
43
|
+
else # warn
|
44
|
+
unless undeclared_fields.empty?
|
45
|
+
logger.info "WARNING: Not saving undeclared fields: #{undeclared_fields}. Saving declared fields only: #{declared_fields} for #{@model.class}"
|
46
|
+
end
|
47
|
+
@model.attrs.slice(*field_names)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def handle_conditional_check_failed_exception(exception)
|
52
|
+
if @params[:condition_expression] == check_unique_condition
|
53
|
+
raise Dynomite::Error::RecordNotUnique.new(not_unique_message)
|
54
|
+
else # currently only other case is locking
|
55
|
+
raise Dynomite::Error::StaleObject.new(exception.message)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def not_unique_message
|
60
|
+
primary_key_attrs = permitted_attrs.stringify_keys.slice(@model.partition_key_field, @model.sort_key_field).symbolize_keys
|
61
|
+
primary_key_found = primary_key_attrs.keys.map(&:to_s)
|
62
|
+
"A #{@model.class.name} with the primary key #{primary_key_attrs} already exists"
|
63
|
+
end
|
64
|
+
|
65
|
+
def check_unique_params
|
66
|
+
if @model.new_record? && !@options[:put]
|
67
|
+
@params.merge!(condition_expression: check_unique_condition)
|
68
|
+
else
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html#Expressions.ConditionExpressions.PreventingOverwrites
|
74
|
+
# Examples:
|
75
|
+
# attribute_not_exists(id)
|
76
|
+
# attribute_not_exists(category) AND attribute_not_exists(sku)
|
77
|
+
def check_unique_condition
|
78
|
+
condition_expression = @model.primary_key_fields.map do |field|
|
79
|
+
"attribute_not_exists(#{field})"
|
80
|
+
end.join(" AND ")
|
81
|
+
end
|
82
|
+
|
83
|
+
def locking_params
|
84
|
+
return {} if @params[:condition_expression] # already set from check_unique_params
|
85
|
+
return {} unless @model.class.locking_enabled?
|
86
|
+
return {} if @model._touching
|
87
|
+
field = @model.class.locking_field_name
|
88
|
+
current_version = @model.send(field) # must use send, since it was set by send. fixes .touch method
|
89
|
+
return {} if current_version == 1
|
90
|
+
|
91
|
+
previous_version = current_version - 1 # since before_save increments it
|
92
|
+
{
|
93
|
+
condition_expression: "#lock_version = :lock_version",
|
94
|
+
expression_attribute_names: {"#lock_version" => "lock_version"},
|
95
|
+
expression_attribute_values: {":lock_version" => previous_version}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Dynomite::Item::Write
|
2
|
+
class UpdateItem < Base
|
3
|
+
def initialize(model, options={})
|
4
|
+
super
|
5
|
+
@attrs = {}
|
6
|
+
@count_changes = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
# Note: fields assigned directly with brackets are not tracked as changed
|
10
|
+
# IE: post[:title] = "test"
|
11
|
+
def call
|
12
|
+
changed_fields = @model.changed_attributes.keys
|
13
|
+
return if changed_fields.empty? # no changes to save
|
14
|
+
@attrs = @model.attrs.slice(*changed_fields)
|
15
|
+
log_debug(params)
|
16
|
+
client.update_item(params)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Allows updates to specific attributes and counters
|
20
|
+
def save_changes(changes={})
|
21
|
+
@attrs = changes[:attrs] || {}
|
22
|
+
@count_changes = changes[:count_changes] || {}
|
23
|
+
log_debug(params)
|
24
|
+
client.update_item(params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def params
|
28
|
+
{
|
29
|
+
expression_attribute_names: expression_attribute_names, # { "##{attribute}" => attribute },
|
30
|
+
expression_attribute_values: expression_attribute_values, # { ':attribute' => value } or { ':by' => by }
|
31
|
+
update_expression: update_expression, # "SET ##{attribute} = ##{attribute}" or "SET ##{attribute} = ##{attribute} + :by"
|
32
|
+
key: @model.primary_key,
|
33
|
+
table_name: @model.class.table_name
|
34
|
+
}
|
35
|
+
end
|
36
|
+
alias to_params params
|
37
|
+
|
38
|
+
def expression_attribute_names
|
39
|
+
attr_names = @attrs.inject({}) do |names, (name,_)|
|
40
|
+
names.merge!("##{name}" => name)
|
41
|
+
end
|
42
|
+
count_names = @count_changes.inject({}) do |names, (name,_)|
|
43
|
+
names.merge!("##{name}" => name)
|
44
|
+
end
|
45
|
+
attr_names.merge(count_names)
|
46
|
+
end
|
47
|
+
|
48
|
+
def expression_attribute_values
|
49
|
+
typecaster = Dynomite::Item::Typecaster.new(@model)
|
50
|
+
attr_values = @attrs.inject({}) do |values, (name,value)|
|
51
|
+
meta = @model.class.fields_meta[name.to_sym] # can be nil if field is not defined
|
52
|
+
type = meta ? meta[:type] : :infer
|
53
|
+
value = typecaster.cast_to_type(type, value, on: :write)
|
54
|
+
values.merge!(":#{name}" => value)
|
55
|
+
end
|
56
|
+
count_values = @count_changes.inject({}) do |values, (name,value)|
|
57
|
+
values.merge!(":#{name}" => value)
|
58
|
+
end
|
59
|
+
attr_values.merge(count_values)
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_expression
|
63
|
+
expressions = []
|
64
|
+
@attrs.inject([]) do |exp, (name,_)|
|
65
|
+
expressions << "##{name} = :#{name}"
|
66
|
+
end
|
67
|
+
@count_changes.inject([]) do |exp, (name,_)|
|
68
|
+
expressions << "##{name} = ##{name} + :#{name}"
|
69
|
+
end
|
70
|
+
"SET " + expressions.join(', ')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|