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,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,334 +1,161 @@
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
- require "dynomite/query"
8
-
9
- # The modeling is ActiveRecord-ish but not exactly because DynamoDB is a
10
- # different type of database.
5
+ # The model is ActiveModel compatiable even though DynamoDB is a different type of database.
11
6
  #
12
7
  # Examples:
13
8
  #
14
- # post = MyModel.new
15
- # post = post.replace(title: "test title")
16
- #
17
- # post.attrs[:id] now contain a generaetd unique partition_key id.
18
- # Usually the partition_key is 'id'. You can set your own unique id also:
19
- #
20
- # post = MyModel.new(id: "myid", title: "my title")
21
- # post.replace
22
- #
23
- # Note that the replace method replaces the entire item, so you
24
- # need to merge the attributes if you want to keep the other attributes.
25
- #
26
- # post = MyModel.find("myid")
27
- # post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
28
- # post.replace
9
+ # post = Post.new(id: "myid", title: "my title")
10
+ # post.save
29
11
  #
30
- # The convenience `attrs` method performs a deep_merge:
31
- #
32
- # post = MyModel.find("myid")
33
- # post.attrs("desc": "my desc") # <= does a deep_merge
34
- # post.replace
35
- #
36
- # Note, a race condition edge case can exist when several concurrent replace
37
- # calls are happening. This is why the interface is called replace to
38
- # emphasis that possibility.
39
- # TODO: implement post.update with db.update_item in a Ruby-ish way.
12
+ # post.id now contain a generated unique partition_key id.
40
13
  #
41
14
  module Dynomite
42
15
  class Item
43
- include Log
44
- include DbConfig
45
- include Errors
46
-
47
- def initialize(attrs={})
48
- @attrs = attrs
49
- end
50
-
51
- # Defining our own reader so we can do a deep merge if user passes in attrs
52
- def attrs(*args)
53
- case args.size
54
- when 0
55
- ActiveSupport::HashWithIndifferentAccess.new(@attrs)
56
- when 1
57
- attributes = args[0] # Hash
58
- if attributes.empty?
59
- ActiveSupport::HashWithIndifferentAccess.new
60
- else
61
- @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
62
47
  end
63
- end
64
- end
65
-
66
- # Not using method_missing to allow usage of dot notation and assign
67
- # @attrs because it might hide actual missing methods errors.
68
- # DynamoDB attrs can go many levels deep so it makes less make sense to
69
- # use to dot notation.
48
+ @associations = {}
70
49
 
71
- # The method is named replace to clearly indicate that the item is
72
- # fully replaced.
73
- def replace(hash={})
74
- @attrs = @attrs.deep_merge(hash)
75
-
76
- # valid? method comes from ActiveModel::Validations
77
- if respond_to? :valid?
78
- return false unless valid?
50
+ if block
51
+ yield(self)
52
+ end
79
53
  end
80
-
81
- attrs = self.class.replace(@attrs)
82
-
83
- @attrs = attrs # refresh attrs because it now has the id
84
- self
85
54
  end
86
55
 
87
- # Similar to replace, but raises an error on failed validation.
88
- # Works that way only if ActiveModel::Validations are included
89
- def replace!(hash={})
90
- 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)
91
59
  end
92
60
 
93
- def find(id)
94
- self.class.find(id)
95
- end
61
+ # Longer hand methods for completeness. Internally encourage shorter attrs.
62
+ alias_method :attributes=, :attrs=
63
+ alias_method :attributes, :attrs
96
64
 
97
- def delete
98
- self.class.delete(@attrs[:id]) if @attrs[:id]
99
- end
100
-
101
- def table_name
102
- self.class.table_name
103
- end
104
-
105
- def partition_key
106
- self.class.partition_key
107
- end
108
-
109
- # For render json: item
110
- def as_json(options={})
111
- @attrs
112
- end
113
-
114
- # Longer hand methods for completeness.
115
- # Internallly encourage the shorter attrs method.
116
- def attributes=(attributes)
117
- @attributes = attributes
118
- end
119
-
120
- def attributes
121
- @attributes
122
- end
123
-
124
- # Adds very little wrapper logic to scan.
125
- #
126
- # * Automatically add table_name to options for convenience.
127
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
128
- # dynamodb client response.
129
- #
130
- # Other than that, usage is same was using the dynamodb client scan method
131
- # directly. Example:
132
- #
133
- # MyModel.scan(
134
- # expression_attribute_names: {"#updated_at"=>"updated_at"},
135
- # filter_expression: "#updated_at between :start_time and :end_time",
136
- # expression_attribute_values: {
137
- # ":start_time" => "2010-01-01T00:00:00",
138
- # ":end_time" => "2020-01-01T00:00:00"
139
- # }
140
- # )
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.
141
71
  #
142
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
143
- def self.scan(params={})
144
- 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.")
145
- log("Scanning table: #{table_name}")
146
- params = { table_name: table_name }.merge(params)
147
- resp = db.scan(params)
148
- 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}"
149
78
  end
150
79
 
151
- # Adds very little wrapper logic to query.
152
- #
153
- # * Automatically add table_name to options for convenience.
154
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
155
- # dynamodb client response.
156
- #
157
- # Other than that, usage is same was using the dynamodb client query method
158
- # directly. Example:
159
- #
160
- # MyModel.query(
161
- # index_name: 'category-index',
162
- # expression_attribute_names: { "#category_name" => "category" },
163
- # expression_attribute_values: { ":category_value" => "Entertainment" },
164
- # key_condition_expression: "#category_name = :category_value",
165
- # )
166
- #
167
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
168
- def self.query(params={})
169
- params = { table_name: table_name }.merge(params)
170
- resp = db.query(params)
171
- resp.items.map {|i| self.new(i) }
172
- end
173
-
174
- # Creates a new chainable ActiveRecord Query-style instance with a certain index_name.
175
- #
176
- # Post.index_name("category-index").where(category: "Drama")
177
- #
178
- def self.index_name(name)
179
- _new_query.index_name(name)
180
- end
181
-
182
- # Translates simple query searches:
183
- #
184
- # Post.index_name("category-index").where(category: "Drama")
185
- #
186
- # translates to
187
- #
188
- # resp = db.query(
189
- # table_name: "demo-dev-post",
190
- # index_name: 'category-index',
191
- # expression_attribute_names: { "#category_name" => "category" },
192
- # expression_attribute_values: { ":category_value" => category },
193
- # key_condition_expression: "#category_name = :category_value",
194
- # )
195
- def self.where(attributes)
196
- _new_query.where(attributes)
80
+ def read_attribute(field)
81
+ @attrs[field.to_sym]
197
82
  end
198
83
 
199
- def self.replace(attrs)
200
- # Automatically adds some attributes:
201
- # partition key unique id
202
- # created_at and updated_at timestamps. Timestamp format from AWS docs: http://amzn.to/2z98Bdc
203
- defaults = {
204
- partition_key => Digest::SHA1.hexdigest([Time.now, rand].join)
205
- }
206
- item = defaults.merge(attrs)
207
- item["created_at"] ||= Time.now.utc.strftime('%Y-%m-%dT%TZ')
208
- item["updated_at"] = Time.now.utc.strftime('%Y-%m-%dT%TZ')
209
-
210
- # put_item full replaces the item
211
- resp = db.put_item(
212
- table_name: table_name,
213
- item: item
214
- )
215
-
216
- # The resp does not contain the attrs. So might as well return
217
- # the original item with the generated partition_key value
218
- 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
219
88
  end
220
89
 
221
- def self.find(id)
222
- params =
223
- case id
224
- when String
225
- { partition_key => id }
226
- when Hash
227
- id
228
- end
229
-
230
- resp = db.get_item(
231
- table_name: table_name,
232
- key: params
233
- )
234
- attributes = resp.item # unwraps the item's attributes
235
- 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
236
94
  end
237
95
 
238
- # Two ways to use the delete method:
239
- #
240
- # 1. Specify the key as a String. In this case the key will is the partition_key
241
- # set on the model.
242
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
243
- #
244
- # 2. Specify the key as a Hash, you can arbitrarily specific the key structure this way
245
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
246
- #
247
- # options is provided in case you want to specific condition_expression or
248
- # expression_attribute_values.
249
- def self.delete(key_object, options={})
250
- if key_object.is_a?(String)
251
- key = {
252
- partition_key => key_object
253
- }
254
- else # it should be a Hash
255
- key = key_object
256
- end
257
-
258
- params = {
259
- table_name: table_name,
260
- key: key
261
- }
262
- # In case you want to specify condition_expression or expression_attribute_values
263
- params = params.merge(options)
264
-
265
- 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.
266
100
  end
101
+ alias :remove_attribute :delete_attribute
267
102
 
268
- # When called with an argument we'll set the internal @partition_key value
269
- # When called without an argument just retun it.
270
- # class Comment < Dynomite::Item
271
- # partition_key "post_id"
272
- # end
273
- def self.partition_key(*args)
274
- case args.size
275
- when 0
276
- @partition_key || "id" # defaults to id
277
- when 1
278
- @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)
279
108
  end
280
109
  end
281
110
 
282
- def self.table_name(*args)
283
- case args.size
284
- when 0
285
- get_table_name
286
- when 1
287
- set_table_name(args[0])
288
- end
111
+ def [](field)
112
+ read_attribute(field)
289
113
  end
290
114
 
291
- def self.get_table_name
292
- @table_name ||= self.name.pluralize.gsub('::','-').underscore.dasherize
293
- [table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
115
+ def []=(field, value)
116
+ write_attribute(field, value)
294
117
  end
295
118
 
296
- def self.set_table_name(value)
297
- @table_name = value
119
+ # For render json: item
120
+ def as_json(options={})
121
+ @attrs
298
122
  end
299
123
 
300
- def self.table
301
- Aws::DynamoDB::Table.new(name: table_name, client: db)
124
+ # Required for ActiveModel
125
+ def persisted?
126
+ !new_record?
302
127
  end
303
128
 
304
- def self.count
305
- 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
306
140
  end
307
141
 
308
- # Defines column. Defined column can be accessed by getter and setter methods of the same
309
- # name (e.g. [model.my_column]). Attributes with undefined columns can be accessed by
310
- # [model.attrs] method.
311
- def self.column(*names)
312
- 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
313
151
  end
314
152
 
315
- # @see Item.column
316
- def self.add_column(name)
317
- if Dynomite::RESERVED_WORDS.include?(name)
318
- raise ReservedWordError, "'#{name}' is a reserved word"
319
- end
320
-
321
- define_method(name) do
322
- @attrs[name.to_s]
153
+ def to_param
154
+ if id
155
+ id
156
+ else
157
+ raise "Need to define a id field for to_param"
323
158
  end
324
-
325
- define_method("#{name}=") do |value|
326
- @attrs[name.to_s] = value
327
- end
328
- end
329
-
330
- def self._new_query
331
- Dynomite::Query.new(self, {})
332
159
  end
333
160
  end
334
161
  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