mara 0.1.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.
@@ -0,0 +1,208 @@
1
+ require_relative '../error'
2
+
3
+ module Mara
4
+ module Model
5
+ ##
6
+ # An error raised if the index requested in
7
+ # {Dsl::ClassMethods.global_secondary_index} or
8
+ # {Dsl::ClassMethods.global_secondary_index} are not found.
9
+ #
10
+ # @author Maddie Schipper
11
+ # @since 1.0.0
12
+ class IndexError < Mara::Error; end
13
+
14
+ ##
15
+ # Represents a DynamoDB Local Secondary Index.
16
+ #
17
+ # @see Mara::Model::Dsl::ClassMethods#add_lsi
18
+ #
19
+ # @!attribute [rw] name
20
+ # The name of the index.
21
+ #
22
+ # @return [String]
23
+ #
24
+ # @!attribute [rw] key_name
25
+ # The name of the LSI sort_key.
26
+ #
27
+ # @return [String]
28
+ LocalSecondaryIndex = Struct.new(:name, :key_name)
29
+
30
+ ##
31
+ # Represents a DynamoDB Global Secondary Index.
32
+ #
33
+ # @see Mara::Model::Dsl::ClassMethods#add_gsi
34
+ #
35
+ # @!attribute [rw] name
36
+ # The name of the index.
37
+ #
38
+ # @return [String]
39
+ #
40
+ # @!attribute [rw] partition_key
41
+ # The name of the GSI partion key.
42
+ #
43
+ # @return [String]
44
+ #
45
+ # @!attribute [rw] sort_key
46
+ # The name of the GSI sort_key.
47
+ #
48
+ # @return [String, nil]
49
+ GlobalSecondaryIndex = Struct.new(:name, :partition_key, :sort_key)
50
+
51
+ ##
52
+ # Helper DSL methods for Base class.
53
+ #
54
+ # @author Maddie Schipper
55
+ # @since 1.0.0
56
+ module Dsl
57
+ ##
58
+ # @private
59
+ def self.included(klass)
60
+ klass.extend(ClassMethods)
61
+ end
62
+
63
+ ##
64
+ # Helper method added at the class level.
65
+ #
66
+ # @author Maddie Schipper
67
+ # @since 1.0.0
68
+ module ClassMethods
69
+ ##
70
+ # Set a partion_key and sort_key for a model.
71
+ #
72
+ # @see #partition_key
73
+ #
74
+ # @see #sort_key
75
+ #
76
+ # @example Setting the partition_key & sort_key
77
+ # class Person < Mara::Model::Base
78
+ # primary_key('PartionKeyName', 'SortKeyName')
79
+ # # ...
80
+ #
81
+ # @param partition_key [#to_s] The name of the DynamoDB table's partion
82
+ # key.
83
+ #
84
+ # @param sort_key [#to_s, nil] The name of the DynamoDB table's sort
85
+ # key.
86
+ #
87
+ # @return [void]
88
+ def primary_key(partition_key, sort_key = nil)
89
+ partition_key(partition_key)
90
+ sort_key(sort_key)
91
+ end
92
+
93
+ ##
94
+ # Set the partion key name for the model. This value is required.
95
+ #
96
+ # @param partition_key [#to_s] The name of the partion key.
97
+ #
98
+ # @return [String]
99
+ def partition_key(partition_key = nil)
100
+ unless partition_key.nil?
101
+ @partition_key = partition_key.to_s
102
+ validates_presence_of :partition_key
103
+ end
104
+ @partition_key
105
+ end
106
+
107
+ ##
108
+ # Set the sort key name for the model.
109
+ #
110
+ # @param sort_key [#to_s] The name of the sort key.
111
+ #
112
+ # @return [String]
113
+ def sort_key(sort_key = nil)
114
+ unless sort_key.nil?
115
+ @sort_key = sort_key.to_s
116
+ validates_presence_of :sort_key
117
+ end
118
+
119
+ @sort_key
120
+ end
121
+
122
+ ##
123
+ # Add a local secondary index definition.
124
+ #
125
+ # @note This is only required for querying.
126
+ #
127
+ # @param name [String] The name of the index.
128
+ #
129
+ # @param key_name [String] The name of the LSI sort key.
130
+ #
131
+ # @return [void]
132
+ def add_lsi(name, key_name)
133
+ local_secondary_indices[name.to_s] = LocalSecondaryIndex.new(name.to_s, key_name.to_s)
134
+ end
135
+
136
+ ##
137
+ # @private
138
+ #
139
+ # All registered local secondary indices
140
+ #
141
+ # @return [Hash<String, LocalSecondaryIndex>]
142
+ def local_secondary_indices
143
+ @local_secondary_indices ||= {}
144
+ end
145
+
146
+ ##
147
+ # Get a defined local secondary index by name.
148
+ #
149
+ # @param name [#to_s] The name of the LSI to get.
150
+ #
151
+ # @raise [IndexError] The index is not registered on the model.
152
+ #
153
+ # @return [LocalSecondaryIndex]
154
+ def local_secondary_index(name)
155
+ index = local_secondary_indices[name.to_s]
156
+ if index.nil?
157
+ raise Mara::Model::IndexError, "Can't find a LSI with the name `#{name}`"
158
+ end
159
+
160
+ index
161
+ end
162
+
163
+ ##
164
+ # Add a global secondary index definition.
165
+ #
166
+ # @note This is only required for querying.
167
+ #
168
+ # @param name [#to_s] The name of the index.
169
+ #
170
+ # @param partition_key [#to_s] The name of the GSI partition key.
171
+ #
172
+ # @param sort_key [String, nil] The name of the GSI sort key.
173
+ #
174
+ # @return [void]
175
+ def add_gsi(name, partition_key, sort_key = nil)
176
+ global_secondary_indices[name.to_s] = GlobalSecondaryIndex.new(name.to_s, partition_key.to_s, sort_key)
177
+ end
178
+
179
+ ##
180
+ # @private
181
+ #
182
+ # All registered global secondary indices
183
+ #
184
+ # @return [Hash<String, GlobalSecondaryIndex>]
185
+ def global_secondary_indices
186
+ @global_secondary_indices ||= {}
187
+ end
188
+
189
+ ##
190
+ # Get a defined global secondary index by name.
191
+ #
192
+ # @param name [#to_s] The name of the GSI to get.
193
+ #
194
+ # @raise [IndexError] The index is not registered on the model.
195
+ #
196
+ # @return [GlobalSecondaryIndex]
197
+ def global_secondary_index(name)
198
+ index = global_secondary_indices[name.to_s]
199
+ if index.nil?
200
+ raise Mara::Model::IndexError, "Can't find a GSI with the name `#{name}`"
201
+ end
202
+
203
+ index
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,120 @@
1
+ require_relative '../attribute_formatter'
2
+ require_relative '../batch'
3
+ require_relative '../instrument'
4
+
5
+ module Mara
6
+ module Model
7
+ ##
8
+ # Methods that save/update/delete a model.
9
+ #
10
+ # @author Maddie Schipper
11
+ # @since 1.0.0
12
+ module Persistence
13
+ ##
14
+ # @private
15
+ def self.included(klass)
16
+ klass.extend(ClassMethods)
17
+ end
18
+
19
+ ##
20
+ # Helper method added at the class level.
21
+ #
22
+ # @author Maddie Schipper
23
+ # @since 1.0.0
24
+ module ClassMethods
25
+ end
26
+
27
+ ##
28
+ # @private
29
+ #
30
+ # Converts the attributes into a DynamoDB compatable hash.
31
+ #
32
+ # @return [Hash]
33
+ def to_dynamo
34
+ {}.tap do |formatted|
35
+ attributes.each do |key, value|
36
+ formatted[key] = Mara::AttributeFormatter.format(value)
37
+ end
38
+ end
39
+ end
40
+
41
+ ##
42
+ # @private
43
+ #
44
+ # Get a primary key attribute for the item.
45
+ #
46
+ # @return [Hash]
47
+ def primary_key
48
+ {}.tap do |base|
49
+ base[self.class.partition_key] = Mara::AttributeFormatter.format(partition_key)
50
+
51
+ unless self.class.sort_key.blank?
52
+ base[self.class.sort_key] = Mara::AttributeFormatter.format(sort_key)
53
+ end
54
+ end
55
+ end
56
+
57
+ ##
58
+ # @private
59
+ #
60
+ # Create a DynamoDB representation of the model.
61
+ #
62
+ # @return [Hash]
63
+ def to_item
64
+ to_dynamo.merge(primary_key)
65
+ end
66
+
67
+ ##
68
+ # Perform validation and save the model.
69
+ #
70
+ # @return [true, false]
71
+ def save
72
+ Mara.instrument('model.save', model: self) do
73
+ next false unless valid?
74
+
75
+ Mara::Batch.save_model(to_item)
76
+ end
77
+ end
78
+
79
+ ##
80
+ # Perform validation and save the model.
81
+ #
82
+ # @see #save
83
+ #
84
+ # @note Same as {#save} but will raise an error on validation faiure and
85
+ # save failure
86
+ #
87
+ # @return [void]
88
+ def save!
89
+ Mara.instrument('model.save', model: self) do
90
+ validate!
91
+ Mara::Batch.save_model!(to_item)
92
+ end
93
+ end
94
+
95
+ ##
96
+ # Perform a destroy on the model.
97
+ #
98
+ # @return [true, false]
99
+ def destroy
100
+ Mara.instrument('model.destroy', model: self) do
101
+ Mara::Batch.delete_model(primary_key)
102
+ end
103
+ end
104
+
105
+ ##
106
+ # Perform a destroy on the model.
107
+ #
108
+ # @see #destroy
109
+ #
110
+ # @note Same as {#destroy} but will raise an error on delete failure.
111
+ #
112
+ # @return [void]
113
+ def destroy!
114
+ Mara.instrument('model.destroy', model: self) do
115
+ Mara::Batch.delete_model!(primary_key)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,97 @@
1
+ require_relative '../error'
2
+ require_relative '../query'
3
+ require_relative '../attribute_formatter'
4
+ require_relative '../instrument'
5
+
6
+ module Mara
7
+ module Model
8
+ ##
9
+ # Error raised when a find method fails to find the requested object.
10
+ #
11
+ # @author Maddie Schipper
12
+ # @since 1.0.0
13
+ class NotFoundError < Error; end
14
+
15
+ ##
16
+ # Methods to query for a model.
17
+ #
18
+ # @author Maddie Schipper
19
+ # @since 1.0.0
20
+ module Query
21
+ ##
22
+ # @private
23
+ def self.included(klass)
24
+ klass.extend(ClassMethods)
25
+ end
26
+
27
+ ##
28
+ # Helper methods defined on the class.
29
+ #
30
+ # @author Maddie Schipper
31
+ # @since 1.0.0
32
+ module ClassMethods
33
+ ##
34
+ # Find a single object with the matching partition_key and sort key.
35
+ #
36
+ # @param partition_key [#to_s] The value for the partition key.
37
+ #
38
+ # @param sort_key [#to_s, nil] The value for the sort key.
39
+ #
40
+ # @raise [NotFoundError] If the object doesn't exist in the table for
41
+ # the requested primary key.
42
+ #
43
+ # @raise [ArgumentError] If the +partition_key+ is blank, or the
44
+ # +sort_key+ is blank and the class defines a sort_key name.
45
+ #
46
+ # @return [ Mara::Model::Base]
47
+ def find(partition_key, sort_key = nil)
48
+ Mara.instrument('model.find', class_name: name, partition_key: partition_key, sort_key: sort_key) do
49
+ _find(partition_key, sort_key)
50
+ end
51
+ end
52
+
53
+ ##
54
+ # @private
55
+ #
56
+ # @see #find
57
+ def _find(partition_key, sort_key = nil)
58
+ if partition_key.blank?
59
+ raise ArgumentError, 'Must specify a valid partition key value'
60
+ end
61
+
62
+ if sort_key.nil? && !self.sort_key.blank?
63
+ raise ArgumentError, "#{self.class.name} specifies a sort key, but no sort key value was given."
64
+ end
65
+
66
+ key_params = {}
67
+ key_params[self.partition_key] = Mara::AttributeFormatter.format(partition_key)
68
+ if self.sort_key.present?
69
+ key_params[self.sort_key] = Mara::AttributeFormatter.format(sort_key)
70
+ end
71
+
72
+ response = Mara::Query.get_item(key: key_params)
73
+
74
+ if response.nil? || response.items.empty?
75
+ raise NotFoundError, "Can't find item with pk=#{partition_key} sk=#{sort_key}"
76
+ end
77
+
78
+ item = response.items[0]
79
+
80
+ construct(item)
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Checks if a the model exists in the table?
86
+ #
87
+ # @return [true, false]
88
+ def exist?
89
+ pk = partition_key
90
+ sk = conditional_sort_key
91
+ self.class.find(pk, sk).present?
92
+ rescue Mara::Model::NotFoundError
93
+ false
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ module Mara
2
+ ##
3
+ # @private
4
+ #
5
+ # The null value placeholder
6
+ class NullValue; end
7
+
8
+ ##
9
+ # @private
10
+ #
11
+ # Default NULL value
12
+ NULL = NullValue.new.freeze
13
+ end
@@ -0,0 +1,204 @@
1
+ require_relative 'error'
2
+ require_relative 'client'
3
+ require_relative 'configure'
4
+ require_relative 'instrument'
5
+ require_relative 'dynamo_helpers'
6
+
7
+ module Mara
8
+ ##
9
+ # @private
10
+ #
11
+ # Perform calls to DynamoDB for saving/updating/deleting
12
+ #
13
+ # @author Maddie Schipper
14
+ # @since 1.0.0
15
+ class Persistence
16
+ include DynamoHelpers
17
+
18
+ ##
19
+ # @private
20
+ #
21
+ # A wrapper for a create request.
22
+ #
23
+ # @!attribute [rw] record
24
+ # The record hash to be created.
25
+ #
26
+ # @return [Hash]
27
+ #
28
+ # @author Maddie Schipper
29
+ # @since 1.0.0
30
+ CreateRequest = Struct.new(:record) do
31
+ ##
32
+ # Converts the CreateRequest to JSON
33
+ #
34
+ # @return [Hash]
35
+ def as_json
36
+ {
37
+ put_request: {
38
+ item: record
39
+ }
40
+ }
41
+ end
42
+ end
43
+
44
+ ##
45
+ # @private
46
+ #
47
+ # A wrapper for a destroy request.
48
+ #
49
+ # @!attribute [rw] record
50
+ # The record hash to be destroyed.
51
+ #
52
+ # @return [Hash]
53
+ #
54
+ # @author Maddie Schipper
55
+ # @since 1.0.0
56
+ DestroyRequest = Struct.new(:record) do
57
+ ##
58
+ # Converts the DestroyRequest to JSON
59
+ #
60
+ # @return [Hash]
61
+ def as_json
62
+ {
63
+ delete_request: {
64
+ key: record
65
+ }
66
+ }
67
+ end
68
+ end
69
+
70
+ ##
71
+ # @private The response for a save operation.
72
+ #
73
+ # @!attribute [r] consumed_capacity
74
+ # The total consumed capacity for the request.
75
+ #
76
+ # @return [Float]
77
+ #
78
+ # @!attribute [r] operation_count
79
+ # The total number of API calls required to perform the operation.
80
+ #
81
+ # @return [Integer]
82
+ #
83
+ # @author Maddie Schipper
84
+ # @since 1.0.0
85
+ Response = Struct.new(:consumed_capacity, :operation_count)
86
+
87
+ ##
88
+ # @private
89
+ #
90
+ # Error thrown by Persistence calls.
91
+ #
92
+ # @author Maddie Schipper
93
+ # @since 1.0.0
94
+ class Error < Mara::Error; end
95
+
96
+ class << self
97
+ ##
98
+ # Perform a save on a item.
99
+ #
100
+ # @param item [Hash] The item to be saved.
101
+ #
102
+ # @return [true, false]
103
+ def save_model(item)
104
+ create = CreateRequest.new(item)
105
+ response = perform_requests(
106
+ Mara::Client.shared,
107
+ Mara.config.dynamodb.table_name,
108
+ [create]
109
+ )
110
+ !response.nil?
111
+ end
112
+
113
+ ##
114
+ # Perform a save on the a item.
115
+ #
116
+ # @see .save_model
117
+ #
118
+ # @param item [Hash] The item to be saved.
119
+ #
120
+ # @raise Error If the save operation fails.
121
+ #
122
+ # @return [void]
123
+ def save_model!(item)
124
+ return if save_model(item)
125
+
126
+ raise Error, 'Failed to save!'
127
+ end
128
+
129
+ ##
130
+ # Delete an item.
131
+ #
132
+ # @param item [Hash] The item to be deleted.
133
+ #
134
+ # @return [true, false]
135
+ def delete_model(item)
136
+ delete = DestroyRequest.new(item)
137
+ response = perform_requests(
138
+ Mara::Client.shared,
139
+ Mara.config.dynamodb.table_name,
140
+ [delete]
141
+ )
142
+ !response.nil?
143
+ end
144
+
145
+ ##
146
+ # Delete an item.
147
+ #
148
+ # @see .delete_model
149
+ #
150
+ # @param item [Hash] The item to be deleted.
151
+ #
152
+ # @raise Error If the delete operation fails.
153
+ #
154
+ # @return [void]
155
+ def delete_model!(item)
156
+ return if delete_model(item)
157
+
158
+ raise Error, 'Failed to delete!'
159
+ end
160
+
161
+ ##
162
+ # Perform a batch of save requests.
163
+ def perform_requests(client, table_name, requests, group_size = 10)
164
+ results = Mara.instrument('save_batch_records', requests: requests, table_name: table_name) do
165
+ requests.each_slice(group_size).map do |sub_requests|
166
+ _perform_requests(client, table_name, sub_requests)
167
+ end
168
+ end
169
+ Response.new(
170
+ results.map(&:consumed_capacity).sum,
171
+ results.map(&:operation_count).sum
172
+ )
173
+ end
174
+
175
+ private
176
+
177
+ def _base_batch_write_item_params(table_name)
178
+ params = {
179
+ return_consumed_capacity: 'TOTAL',
180
+ return_item_collection_metrics: 'SIZE',
181
+ request_items: {}
182
+ }
183
+ if block_given?
184
+ params[:request_items][table_name] = yield
185
+ end
186
+ params
187
+ end
188
+
189
+ def _perform_requests(client, table_name, requests)
190
+ params = _base_batch_write_item_params(table_name) { requests.map(&:as_json) }
191
+ response = Mara.instrument('save_batch_record_operation', requests: requests, table_name: table_name) do
192
+ client.batch_write_item(params)
193
+ end
194
+
195
+ cc = calculate_consumed_capacity(response.consumed_capacity, table_name)
196
+
197
+ Response.new(
198
+ cc,
199
+ 1
200
+ )
201
+ end
202
+ end
203
+ end
204
+ end