dynomite 1.2.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -190
  7. data/Rakefile +13 -1
  8. data/dynomite.gemspec +9 -2
  9. data/exe/dynomite +14 -0
  10. data/lib/dynomite/associations/association.rb +126 -0
  11. data/lib/dynomite/associations/belongs_to.rb +35 -0
  12. data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
  13. data/lib/dynomite/associations/has_many.rb +19 -0
  14. data/lib/dynomite/associations/has_one.rb +19 -0
  15. data/lib/dynomite/associations/many_association.rb +257 -0
  16. data/lib/dynomite/associations/single_association.rb +157 -0
  17. data/lib/dynomite/associations.rb +248 -0
  18. data/lib/dynomite/autoloader.rb +25 -0
  19. data/lib/dynomite/cli.rb +48 -0
  20. data/lib/dynomite/client.rb +118 -0
  21. data/lib/dynomite/command.rb +89 -0
  22. data/lib/dynomite/completer/script.rb +6 -0
  23. data/lib/dynomite/completer/script.sh +10 -0
  24. data/lib/dynomite/completer.rb +159 -0
  25. data/lib/dynomite/config.rb +39 -0
  26. data/lib/dynomite/core.rb +18 -19
  27. data/lib/dynomite/engine.rb +45 -0
  28. data/lib/dynomite/erb.rb +5 -3
  29. data/lib/dynomite/error.rb +12 -0
  30. data/lib/dynomite/help/completion.md +20 -0
  31. data/lib/dynomite/help/completion_script.md +3 -0
  32. data/lib/dynomite/help/migrate.md +3 -0
  33. data/lib/dynomite/help.rb +9 -0
  34. data/lib/dynomite/install.rb +4 -0
  35. data/lib/dynomite/item/abstract.rb +15 -0
  36. data/lib/dynomite/item/components.rb +33 -0
  37. data/lib/dynomite/item/dsl.rb +101 -0
  38. data/lib/dynomite/item/id.rb +41 -0
  39. data/lib/dynomite/item/indexes/finder.rb +58 -0
  40. data/lib/dynomite/item/indexes/index.rb +21 -0
  41. data/lib/dynomite/item/indexes/primary_index.rb +18 -0
  42. data/lib/dynomite/item/indexes.rb +25 -0
  43. data/lib/dynomite/item/locking.rb +53 -0
  44. data/lib/dynomite/item/magic_fields.rb +66 -0
  45. data/lib/dynomite/item/primary_key.rb +85 -0
  46. data/lib/dynomite/item/query/delegates.rb +28 -0
  47. data/lib/dynomite/item/query/params/base.rb +42 -0
  48. data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
  49. data/lib/dynomite/item/query/params/filter.rb +41 -0
  50. data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
  51. data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
  52. data/lib/dynomite/item/query/params/function/base.rb +33 -0
  53. data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
  54. data/lib/dynomite/item/query/params/function/contains.rb +7 -0
  55. data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
  56. data/lib/dynomite/item/query/params/helpers.rb +94 -0
  57. data/lib/dynomite/item/query/params/key_condition.rb +34 -0
  58. data/lib/dynomite/item/query/params.rb +115 -0
  59. data/lib/dynomite/item/query/partiql/executer.rb +72 -0
  60. data/lib/dynomite/item/query/partiql.rb +67 -0
  61. data/lib/dynomite/item/query/relation/chain.rb +125 -0
  62. data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
  63. data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
  64. data/lib/dynomite/item/query/relation/delete.rb +38 -0
  65. data/lib/dynomite/item/query/relation/ids.rb +21 -0
  66. data/lib/dynomite/item/query/relation/math.rb +19 -0
  67. data/lib/dynomite/item/query/relation/where_field.rb +32 -0
  68. data/lib/dynomite/item/query/relation/where_group.rb +78 -0
  69. data/lib/dynomite/item/query/relation.rb +127 -0
  70. data/lib/dynomite/item/query.rb +7 -0
  71. data/lib/dynomite/item/read/find.rb +196 -0
  72. data/lib/dynomite/item/read/find_with_event.rb +42 -0
  73. data/lib/dynomite/item/read.rb +90 -0
  74. data/lib/dynomite/item/sti.rb +43 -0
  75. data/lib/dynomite/item/table_namespace.rb +43 -0
  76. data/lib/dynomite/item/typecaster.rb +106 -0
  77. data/lib/dynomite/item/waiter_methods.rb +18 -0
  78. data/lib/dynomite/item/write/base.rb +15 -0
  79. data/lib/dynomite/item/write/delete_item.rb +14 -0
  80. data/lib/dynomite/item/write/put_item.rb +99 -0
  81. data/lib/dynomite/item/write/update_item.rb +73 -0
  82. data/lib/dynomite/item/write.rb +204 -0
  83. data/lib/dynomite/item.rb +113 -286
  84. data/lib/dynomite/migration/dsl/accessor.rb +19 -0
  85. data/lib/dynomite/migration/dsl/index/base.rb +42 -0
  86. data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
  87. data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
  88. data/lib/dynomite/migration/dsl/index.rb +72 -0
  89. data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
  90. data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
  91. data/lib/dynomite/migration/dsl.rb +89 -142
  92. data/lib/dynomite/migration/file_info.rb +28 -0
  93. data/lib/dynomite/migration/generator.rb +30 -16
  94. data/lib/dynomite/migration/helpers.rb +7 -0
  95. data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
  96. data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
  97. data/lib/dynomite/migration/runner.rb +178 -0
  98. data/lib/dynomite/migration/templates/create_table.rb +7 -23
  99. data/lib/dynomite/migration/templates/delete_table.rb +7 -0
  100. data/lib/dynomite/migration/templates/update_table.rb +3 -18
  101. data/lib/dynomite/migration.rb +53 -10
  102. data/lib/dynomite/reserved_words.rb +13 -3
  103. data/lib/dynomite/seed.rb +12 -0
  104. data/lib/dynomite/types.rb +22 -0
  105. data/lib/dynomite/version.rb +1 -1
  106. data/lib/dynomite/waiter.rb +40 -0
  107. data/lib/dynomite.rb +11 -17
  108. data/lib/generators/application_item/application_item_generator.rb +30 -0
  109. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  110. data/lib/jets/commands/dynamodb_command.rb +29 -0
  111. data/lib/jets/commands/help/generate.md +33 -0
  112. data/lib/jets/commands/help/migrate.md +3 -0
  113. metadata +201 -17
  114. data/docs/migrations/long-example.rb +0 -127
  115. data/docs/migrations/short-example.rb +0 -40
  116. data/lib/dynomite/db_config.rb +0 -121
  117. data/lib/dynomite/errors.rb +0 -15
  118. data/lib/dynomite/log.rb +0 -15
  119. data/lib/dynomite/migration/common.rb +0 -86
  120. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  121. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  122. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  123. 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,18 @@
1
+ class Dynomite::Item
2
+ module WaiterMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ def waiter
6
+ self.class.waiter
7
+ end
8
+
9
+ class_methods do
10
+ extend Memoist
11
+
12
+ def waiter
13
+ Dynomite::Waiter.new
14
+ end
15
+ memoize :waiter
16
+ end
17
+ end
18
+ 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