dynomite 1.2.6 → 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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -188
  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 -299
  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/query.rb +48 -0
  103. data/lib/dynomite/reserved_words.rb +13 -3
  104. data/lib/dynomite/seed.rb +12 -0
  105. data/lib/dynomite/types.rb +22 -0
  106. data/lib/dynomite/version.rb +1 -1
  107. data/lib/dynomite/waiter.rb +40 -0
  108. data/lib/dynomite.rb +11 -17
  109. data/lib/generators/application_item/application_item_generator.rb +30 -0
  110. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  111. data/lib/jets/commands/dynamodb_command.rb +29 -0
  112. data/lib/jets/commands/help/generate.md +33 -0
  113. data/lib/jets/commands/help/migrate.md +3 -0
  114. metadata +202 -17
  115. data/docs/migrations/long-example.rb +0 -127
  116. data/docs/migrations/short-example.rb +0 -40
  117. data/lib/dynomite/db_config.rb +0 -107
  118. data/lib/dynomite/errors.rb +0 -15
  119. data/lib/dynomite/log.rb +0 -15
  120. data/lib/dynomite/migration/common.rb +0 -86
  121. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  122. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  123. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  124. data/lib/dynomite/migration/executor.rb +0 -38
@@ -0,0 +1,204 @@
1
+ class Dynomite::Item
2
+ module Write
3
+ extend ActiveSupport::Concern
4
+
5
+ # Not using method_missing to allow usage of dot notation and assign
6
+ # @attrs because it might hide actual missing methods errors.
7
+ # DynamoDB attrs can go many levels deep so it makes less make sense to
8
+ # use to dot notation.
9
+
10
+ def save(options={})
11
+ options.reverse_merge!(validate: true)
12
+ return self if options[:validate] && !valid?
13
+
14
+ action = new_record? ? :create : :update
15
+ run_callbacks(:save) do
16
+ run_callbacks(action) do
17
+ if action == :create
18
+ PutItem.call(self, options)
19
+ else # :update
20
+ call_update_strategy(options)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Similar to save, but raises an error on failed validation.
27
+ def save!(options={})
28
+ raise_error_if_invalid
29
+ save(options)
30
+ end
31
+
32
+ # post.update(title: "test", body: "body")
33
+ # post.update({title: "test", body: "body"}, {validate: false})
34
+ def update(attrs={}, options={})
35
+ self.attrs.merge!(attrs)
36
+ options.reverse_merge!(validate: true)
37
+ return false if options[:validate] && !valid?
38
+
39
+ run_callbacks(:save) do
40
+ run_callbacks(:update) do
41
+ call_update_strategy(options)
42
+ end
43
+ end
44
+ end
45
+
46
+ def call_update_strategy(options)
47
+ if Dynomite.config.update_strategy == :update_item
48
+ # Note: fields assigned directly with brackets are not tracked as changed
49
+ # IE: post[:title] = "test"
50
+ UpdateItem.call(self, options)
51
+ else # default
52
+ PutItem.call(self, options)
53
+ end
54
+ end
55
+
56
+ # Similar to update, but raises an error on failed validation.
57
+ def update!(attrs={}, options={})
58
+ raise_error_if_invalid
59
+ update(attrs, options)
60
+ end
61
+
62
+ # When you add an item, the primary key attributes are the only required attributes.
63
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#put_item-instance_method
64
+ def put(options={})
65
+ found_primary_keys = self.attrs.keys.map(&:to_s) & primary_key_fields
66
+ unless primary_key_fields.sort == found_primary_keys
67
+ raise Dynomite::Error::InvalidPut.new("Invalid put. The primary key fields #{primary_key_fields} must be present in the attrs #{attrs}")
68
+ end
69
+
70
+ options.reverse_merge!(validate: true)
71
+ return self if options[:validate] && !valid? # return self so can grab errors in invalid. save does the same thing
72
+
73
+ # Run callbacks for put so id is also set
74
+ run_callbacks(:save) do
75
+ run_callbacks(:update) do
76
+ PutItem.call(self, options.merge(put: true))
77
+ end
78
+ end
79
+ end
80
+ alias replace put
81
+
82
+ def put!(options={})
83
+ raise_error_if_invalid
84
+ put(options)
85
+ end
86
+ alias replace! put!
87
+
88
+ def destroy(options={})
89
+ run_callbacks(:destroy) do
90
+ DeleteItem.call(self, options)
91
+ end
92
+ end
93
+
94
+ def delete(options={})
95
+ DeleteItem.call(self, options)
96
+ end
97
+
98
+ attr_reader :_touching
99
+ def touch(*names, **options)
100
+ if new_record?
101
+ raise Dynomite::Error, 'cannot touch on a new item'
102
+ end
103
+
104
+ time_to_assign = options.delete(:time) || Time.now
105
+
106
+ self.updated_at = time_to_assign
107
+ names.each do |name|
108
+ attrs.send("#{name}=", time_to_assign)
109
+ end
110
+
111
+ @_touching = true
112
+ run_callbacks :touch do
113
+ UpdateItem.call(self, options)
114
+ end
115
+
116
+ self
117
+ end
118
+
119
+ # Examples:
120
+ # user.increment(:likes)
121
+ # user.increment(:likes, 2)
122
+ def increment(attribute, by = 1)
123
+ self[attribute] ||= 0
124
+ self[attribute] += by
125
+ self
126
+ end
127
+
128
+ # Increment counter. Validations and callbacks are skipped.
129
+ #
130
+ # Examples:
131
+ #
132
+ # user.increment!(:likes)
133
+ # user.increment!(:likes, 2)
134
+ # user.increment!(:likes, touch: true)
135
+ # user.increment!(:likes, touch: :created_at)
136
+ # user.increment!(:likes, touch: [:viewed_at, :created_at])
137
+ #
138
+ def increment!(attribute, by = 1, touch: nil)
139
+ increment(attribute, by)
140
+
141
+ now = Time.now
142
+ attrs = Array(touch).inject({}) do |attrs, field|
143
+ attrs.merge!(field => now)
144
+ end if touch
145
+
146
+ run_callbacks :touch do
147
+ UpdateItem.new(self).save_changes(attrs: attrs, count_changes: { attribute => by })
148
+ end
149
+
150
+ self
151
+ end
152
+
153
+ # Example:
154
+ #
155
+ # user = User.first
156
+ # user.banned? # => false
157
+ # user.toggle(:banned)
158
+ # user.banned? # => true
159
+ #
160
+ def toggle(attribute)
161
+ self[attribute] = !public_send("#{attribute}?")
162
+ self
163
+ end
164
+
165
+ def toggle!(attribute)
166
+ toggle(attribute).update_attribute(attribute, self[attribute])
167
+ end
168
+
169
+ def raise_error_if_invalid
170
+ raise Dynomite::Error::Validation, "Validation failed: #{errors.full_messages.join(', ')}" unless valid?
171
+ end
172
+
173
+ class_methods do
174
+ def put(attrs={}, &block)
175
+ new(attrs, &block).put
176
+ end
177
+
178
+ def put!(attrs={}, &block)
179
+ new(attrs, &block).put!
180
+ end
181
+
182
+ def create(attrs={}, &block)
183
+ new(attrs, &block).save
184
+ end
185
+ alias create_with create
186
+
187
+ def create!(attrs={}, &block)
188
+ new(attrs, &block).save!
189
+ end
190
+
191
+ def find_or_create_by(attrs={})
192
+ find_by(attrs) || create(attrs)
193
+ end
194
+
195
+ def find_or_create_by!(attrs={})
196
+ find_by(attrs) || create!(attrs)
197
+ end
198
+
199
+ def find_or_initialize_by(attrs)
200
+ find_by(attrs) || new(attrs)
201
+ end
202
+ end
203
+ end
204
+ end
data/lib/dynomite/item.rb CHANGED
@@ -1,346 +1,160 @@
1
- require "active_support/core_ext/hash"
2
- require "aws-sdk-dynamodb"
1
+ require "active_model"
3
2
  require "digest"
4
3
  require "yaml"
5
4
 
6
- require "dynomite/reserved_words"
7
-
8
- # The modeling is ActiveRecord-ish but not exactly because DynamoDB is a
9
- # different type of database.
5
+ # The model is ActiveModel compatiable even though DynamoDB is a different type of database.
10
6
  #
11
7
  # Examples:
12
8
  #
13
- # post = MyModel.new
14
- # post = post.replace(title: "test title")
15
- #
16
- # post.attrs[:id] now contain a generaetd unique partition_key id.
17
- # Usually the partition_key is 'id'. You can set your own unique id also:
18
- #
19
- # post = MyModel.new(id: "myid", title: "my title")
20
- # post.replace
21
- #
22
- # Note that the replace method replaces the entire item, so you
23
- # need to merge the attributes if you want to keep the other attributes.
24
- #
25
- # post = MyModel.find("myid")
26
- # post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
27
- # post.replace
9
+ # post = Post.new(id: "myid", title: "my title")
10
+ # post.save
28
11
  #
29
- # The convenience `attrs` method performs a deep_merge:
30
- #
31
- # post = MyModel.find("myid")
32
- # post.attrs("desc": "my desc") # <= does a deep_merge
33
- # post.replace
34
- #
35
- # Note, a race condition edge case can exist when several concurrent replace
36
- # calls are happening. This is why the interface is called replace to
37
- # emphasis that possibility.
38
- # TODO: implement post.update with db.update_item in a Ruby-ish way.
12
+ # post.id now contain a generated unique partition_key id.
39
13
  #
40
14
  module Dynomite
41
15
  class Item
42
- include Log
43
- include DbConfig
44
- include Errors
45
-
46
- def initialize(attrs={})
47
- @attrs = attrs
48
- end
49
-
50
- # Defining our own reader so we can do a deep merge if user passes in attrs
51
- def attrs(*args)
52
- case args.size
53
- when 0
54
- ActiveSupport::HashWithIndifferentAccess.new(@attrs)
55
- when 1
56
- attributes = args[0] # Hash
57
- if attributes.empty?
58
- ActiveSupport::HashWithIndifferentAccess.new
59
- else
60
- @attrs = attrs.deep_merge!(attributes)
16
+ class_attribute :fields_map
17
+ self.fields_map = {}
18
+ class_attribute :id_prefix_value
19
+
20
+ include Components
21
+ include Abstract
22
+ abstract!
23
+
24
+ # Must come after include Dynomite::Associations
25
+ def self.inherited(subclass)
26
+ subclass.id_prefix_value = subclass.name.underscore
27
+ # Not direct descendants of Dynomite::Item are abstract
28
+ # IE: SchemaMigration < Dynomite::Item
29
+ subclass.abstract! if subclass.name == "ApplicationItem"
30
+ subclass.class_attribute :fields_map
31
+ subclass.fields_map = {}
32
+ super # Dynomite::Associations.inherited
33
+ end
34
+
35
+ delegate :partition_key_field, :sort_key_field, to: :class
36
+ attr_reader :attrs
37
+ attr_accessor :new_record
38
+ alias_method :new_record?, :new_record
39
+ def initialize(attrs={}, &block)
40
+ run_callbacks(:initialize) do
41
+ @new_record = true
42
+ attrs = attrs.to_hash if attrs.respond_to?(:to_hash) # IE: ActionController::Parameters
43
+ raise ArgumentError, "attrs must be a Hash. attrs is a #{attrs.class}" unless attrs.is_a?(Hash)
44
+ @attrs = ActiveSupport::HashWithIndifferentAccess.new(attrs)
45
+ attrs.each do |k,v|
46
+ send("#{k}=", v) if respond_to?("#{k}=") # so typecasting happens
61
47
  end
62
- end
63
- end
64
-
65
- # Not using method_missing to allow usage of dot notation and assign
66
- # @attrs because it might hide actual missing methods errors.
67
- # DynamoDB attrs can go many levels deep so it makes less make sense to
68
- # use to dot notation.
69
-
70
- # The method is named replace to clearly indicate that the item is
71
- # fully replaced.
72
- def replace(hash={})
73
- @attrs = @attrs.deep_merge(hash)
48
+ @associations = {}
74
49
 
75
- # valid? method comes from ActiveModel::Validations
76
- if respond_to? :valid?
77
- return false unless valid?
50
+ if block
51
+ yield(self)
52
+ end
78
53
  end
79
-
80
- attrs = self.class.replace(@attrs)
81
-
82
- @attrs = attrs # refresh attrs because it now has the id
83
- self
84
54
  end
85
55
 
86
- # Similar to replace, but raises an error on failed validation.
87
- # Works that way only if ActiveModel::Validations are included
88
- def replace!(hash={})
89
- raise ValidationError, "Validation failed: #{errors.full_messages.join(', ')}" unless replace(hash)
56
+ # Keeps the current attrs
57
+ def attrs=(attrs)
58
+ @attrs.deep_merge!(attrs)
90
59
  end
91
60
 
92
- def find(id)
93
- self.class.find(id)
94
- end
95
-
96
- def delete
97
- self.class.delete(@attrs[:id]) if @attrs[:id]
98
- end
99
-
100
- def table_name
101
- self.class.table_name
102
- end
103
-
104
- def partition_key
105
- self.class.partition_key
106
- end
107
-
108
- # For render json: item
109
- def as_json(options={})
110
- @attrs
111
- end
112
-
113
- # Longer hand methods for completeness.
114
- # Internallly encourage the shorter attrs method.
115
- def attributes=(attributes)
116
- @attributes = attributes
117
- end
118
-
119
- def attributes
120
- @attributes
121
- end
122
-
123
- # Adds very little wrapper logic to scan.
124
- #
125
- # * Automatically add table_name to options for convenience.
126
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
127
- # dynamodb client response.
128
- #
129
- # Other than that, usage is same was using the dynamodb client scan method
130
- # directly. Example:
131
- #
132
- # MyModel.scan(
133
- # expression_attribute_names: {"#updated_at"=>"updated_at"},
134
- # filter_expression: "#updated_at between :start_time and :end_time",
135
- # expression_attribute_values: {
136
- # ":start_time" => "2010-01-01T00:00:00",
137
- # ":end_time" => "2020-01-01T00:00:00"
138
- # }
139
- # )
140
- #
141
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
142
- def self.scan(params={})
143
- log("It's recommended to not use scan for production. It can be slow and expensive. You can a LSI or GSI and query the index instead.")
144
- log("Scanning table: #{table_name}")
145
- params = { table_name: table_name }.merge(params)
146
- resp = db.scan(params)
147
- resp.items.map {|i| self.new(i) }
148
- end
61
+ # Longer hand methods for completeness. Internally encourage shorter attrs.
62
+ alias_method :attributes=, :attrs=
63
+ alias_method :attributes, :attrs
149
64
 
150
- # Adds very little wrapper logic to query.
151
- #
152
- # * Automatically add table_name to options for convenience.
153
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
154
- # dynamodb client response.
65
+ # Because using `define_attribute_methods *names` as part of `add_field` dsl.
66
+ # Found that define_attribute_methods is required for dirty support.
67
+ # This adds missing_attribute method that will look for a method called attribute.
68
+ # send(match.target, match.attr_name, *args, &block)
69
+ # send(:attribute, :my_column)
70
+ # The error message when an attribute is not found is more helpful when this is defined.
155
71
  #
156
- # Other than that, usage is same was using the dynamodb client query method
157
- # directly. Example:
158
- #
159
- # MyModel.query(
160
- # index_name: 'category-index',
161
- # expression_attribute_names: { "#category_name" => "category" },
162
- # expression_attribute_values: { ":category_value" => "Entertainment" },
163
- # key_condition_expression: "#category_name = :category_value",
164
- # )
165
- #
166
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
167
- def self.query(params={})
168
- params = { table_name: table_name }.merge(params)
169
- resp = db.query(params)
170
- resp.items.map {|i| self.new(i) }
72
+ # It looks confusing that we always raise an error for attribute because fields must
73
+ # be defined to access them through dot notation. This is because users to
74
+ # explicitly define fields and access undeclared fields with hash notation [],
75
+ # read_attribute, or attributes.
76
+ def attribute(name)
77
+ raise NoMethodError, "undefined method '#{name}' for #{self.class}"
171
78
  end
172
79
 
173
- # Translates simple query searches:
174
- #
175
- # Post.where({category: "Drama"}, index_name: "category-index")
176
- #
177
- # translates to
178
- #
179
- # resp = db.query(
180
- # table_name: "demo-dev-post",
181
- # index_name: 'category-index',
182
- # expression_attribute_names: { "#category_name" => "category" },
183
- # expression_attribute_values: { ":category_value" => category },
184
- # key_condition_expression: "#category_name = :category_value",
185
- # )
186
- #
187
- # TODO: Implement nicer where syntax with index_name as a chained method.
188
- #
189
- # Post.where({category: "Drama"}, {index_name: "category-index"})
190
- # VS
191
- # Post.where(category: "Drama").index_name("category-index")
192
- def self.where(attributes, options={})
193
- raise "attributes.size == 1 only supported for now" if attributes.size != 1
194
-
195
- attr_name = attributes.keys.first
196
- attr_value = attributes[attr_name]
197
-
198
- # params = {
199
- # expression_attribute_names: { "#category_name" => "category" },
200
- # expression_attribute_values: { ":category_value" => "Entertainment" },
201
- # key_condition_expression: "#category_name = :category_value",
202
- # }
203
- name_key, value_key = "##{attr_name}_name", ":#{attr_name}_value"
204
- params = {
205
- expression_attribute_names: { name_key => attr_name },
206
- expression_attribute_values: { value_key => attr_value },
207
- key_condition_expression: "#{name_key} = #{value_key}",
208
- }
209
- # Allow direct access to override params passed to dynamodb query options.
210
- # This is is how index_name is passed:
211
- params = params.merge(options)
212
-
213
- query(params)
80
+ def read_attribute(field)
81
+ @attrs[field.to_sym]
214
82
  end
215
83
 
216
- def self.replace(attrs)
217
- # Automatically adds some attributes:
218
- # partition key unique id
219
- # created_at and updated_at timestamps. Timestamp format from AWS docs: http://amzn.to/2z98Bdc
220
- defaults = {
221
- partition_key => Digest::SHA1.hexdigest([Time.now, rand].join)
222
- }
223
- item = defaults.merge(attrs)
224
- item["created_at"] ||= Time.now.utc.strftime('%Y-%m-%dT%TZ')
225
- item["updated_at"] = Time.now.utc.strftime('%Y-%m-%dT%TZ')
226
-
227
- # put_item full replaces the item
228
- resp = db.put_item(
229
- table_name: table_name,
230
- item: item
231
- )
232
-
233
- # The resp does not contain the attrs. So might as well return
234
- # the original item with the generated partition_key value
235
- item
84
+ # Only updates in memory, does not save to database.
85
+ # Same as ActiveRecord behavior.
86
+ def write_attribute(field, value)
87
+ @attrs[field.to_sym] = value
236
88
  end
237
89
 
238
- def self.find(id)
239
- params =
240
- case id
241
- when String
242
- { partition_key => id }
243
- when Hash
244
- id
245
- end
246
-
247
- resp = db.get_item(
248
- table_name: table_name,
249
- key: params
250
- )
251
- attributes = resp.item # unwraps the item's attributes
252
- self.new(attributes) if attributes
90
+ def update_attribute(field, value)
91
+ write_attribute(field, value)
92
+ update(@attrs, {validate: false})
93
+ valid? # ActiveRecord return value behavior
253
94
  end
254
95
 
255
- # Two ways to use the delete method:
256
- #
257
- # 1. Specify the key as a String. In this case the key will is the partition_key
258
- # set on the model.
259
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
260
- #
261
- # 2. Specify the key as a Hash, you can arbitrarily specific the key structure this way
262
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
263
- #
264
- # options is provided in case you want to specific condition_expression or
265
- # expression_attribute_values.
266
- def self.delete(key_object, options={})
267
- if key_object.is_a?(String)
268
- key = {
269
- partition_key => key_object
270
- }
271
- else # it should be a Hash
272
- key = key_object
273
- end
274
-
275
- params = {
276
- table_name: table_name,
277
- key: key
278
- }
279
- # In case you want to specify condition_expression or expression_attribute_values
280
- params = params.merge(options)
281
-
282
- resp = db.delete_item(params)
96
+ def delete_attribute(field)
97
+ @attrs.delete(field.to_sym)
98
+ update(@attrs, {validate: false})
99
+ valid? # ActiveRecord does not have a delete_attribute. Follow update_attribute behavior.
283
100
  end
101
+ alias :remove_attribute :delete_attribute
284
102
 
285
- # When called with an argument we'll set the internal @partition_key value
286
- # When called without an argument just retun it.
287
- # class Comment < Dynomite::Item
288
- # partition_key "post_id"
289
- # end
290
- def self.partition_key(*args)
291
- case args.size
292
- when 0
293
- @partition_key || "id" # defaults to id
294
- when 1
295
- @partition_key = args[0].to_s
103
+ def update_attribute_presence(field, value)
104
+ if value.present?
105
+ update_attribute(field, value)
106
+ else # nil or empty string or empty array
107
+ delete_attribute(field)
296
108
  end
297
109
  end
298
110
 
299
- def self.table_name(*args)
300
- case args.size
301
- when 0
302
- get_table_name
303
- when 1
304
- set_table_name(args[0])
305
- end
111
+ def [](field)
112
+ read_attribute(field)
306
113
  end
307
114
 
308
- def self.get_table_name
309
- @table_name ||= self.name.pluralize.gsub('::','-').underscore.dasherize
310
- [table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
115
+ def []=(field, value)
116
+ write_attribute(field, value)
311
117
  end
312
118
 
313
- def self.set_table_name(value)
314
- @table_name = value
119
+ # For render json: item
120
+ def as_json(options={})
121
+ @attrs
315
122
  end
316
123
 
317
- def self.table
318
- Aws::DynamoDB::Table.new(name: table_name, client: db)
124
+ # Required for ActiveModel
125
+ def persisted?
126
+ !new_record?
319
127
  end
320
128
 
321
- def self.count
322
- table.item_count
129
+ def reload
130
+ if persisted?
131
+ id = @attrs[partition_key_field]
132
+ item = if sort_key_field
133
+ find(partition_key_field => id, sort_key_field => @attrs[sort_key_field])
134
+ else
135
+ find(id) # item has different object_id
136
+ end
137
+ @attrs = item.attrs # replace current loaded attributes
138
+ end
139
+ self
323
140
  end
324
141
 
325
- # Defines column. Defined column can be accessed by getter and setter methods of the same
326
- # name (e.g. [model.my_column]). Attributes with undefined columns can be accessed by
327
- # [model.attrs] method.
328
- def self.column(*names)
329
- names.each(&method(:add_column))
142
+ # p1 = Product.first
143
+ # p2 = Product.first
144
+ # p1 == p2 # => true
145
+ #
146
+ # p1 = Product.first
147
+ # products = Product.all
148
+ # products.include?(p1) # => true
149
+ def ==(other)
150
+ self.class == other.class && self.attrs == other.attrs
330
151
  end
331
152
 
332
- # @see Item.column
333
- def self.add_column(name)
334
- if Dynomite::RESERVED_WORDS.include?(name)
335
- raise ReservedWordError, "'#{name}' is a reserved word"
336
- end
337
-
338
- define_method(name) do
339
- @attrs[name.to_s]
340
- end
341
-
342
- define_method("#{name}=") do |value|
343
- @attrs[name.to_s] = value
153
+ def to_param
154
+ if id
155
+ id
156
+ else
157
+ raise "Need to define a id field for to_param"
344
158
  end
345
159
  end
346
160
  end
@@ -0,0 +1,19 @@
1
+ class Dynomite::Migration::Dsl
2
+ module Accessor
3
+ def dsl_accessor(*names)
4
+ names.each do |name|
5
+ define_dsl_accessor(name)
6
+ end
7
+ end
8
+
9
+ def define_dsl_accessor(name)
10
+ define_method(name) do |*args|
11
+ if args.empty?
12
+ instance_variable_get("@#{name}")
13
+ else
14
+ instance_variable_set("@#{name}", args.first)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end