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,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