dynomite 1.2.7 → 2.0.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.
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