statelydb 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatelyDB
4
+ module Transaction
5
+ # Transaction coordinates sending requests and waiting for responses. Consumers should not need
6
+ # to interact with this class directly, but instead use the methods provided by the StatelyDB::Client.
7
+ #
8
+ # The example below demonstrates using a transaction, which accepts a block. The lines in the block
9
+ # are executed within the context of the transaction. The transaction is committed when the block
10
+ # completes successfully, OR is aborted if an exception is raised.
11
+ #
12
+ # @example
13
+ # result = client.transaction do |tx|
14
+ # key_path = StatelyDB::KeyPath.with('movie', 'The Shining')
15
+ # movie = tx.get(key_path:)
16
+ # tx.put(item: movie)
17
+ # end
18
+ #
19
+ class Transaction
20
+ # Result represents the results of a transaction
21
+ #
22
+ # @attr_reader puts [Array<StatelyDB::Item>] the items that were put
23
+ # @attr_reader deletes [Array<String>] the key paths that were deleted
24
+ class Result
25
+ # puts is an array of StatelyDB::Items that were put
26
+ # @return [Array<StatelyDB::Item>]
27
+ attr_reader :puts
28
+
29
+ # deletes is an array of key paths that were deleted
30
+ # @return [Array<String>]
31
+ attr_reader :deletes
32
+
33
+ # Initialize a new Result
34
+ #
35
+ # @param puts [Array<StatelyDB::Item>] the items that were put
36
+ # @param deletes [Array<String>] the key paths that were deleted
37
+ def initialize(puts:, deletes:)
38
+ @puts = puts
39
+ @deletes = deletes
40
+ end
41
+ end
42
+
43
+ # Initialize a new Transaction
44
+ #
45
+ # @param stub [Stately::Db::DatabaseService::Stub] a StatelyDB gRPC stub
46
+ # @param store_id [Integer] the StatelyDB Store to transact against
47
+ # @param schema [StatelyDB::Schema] the schema to use for marshalling and unmarshalling Items
48
+ def initialize(stub:, store_id:, schema:)
49
+ @stub = stub
50
+ @store_id = store_id
51
+ @schema = schema
52
+ @is_transaction_open = false
53
+
54
+ # A queue of outbound requests
55
+ @outgoing_requests = StatelyDB::Transaction::Queue.new
56
+ end
57
+
58
+ # Send a request and wait for a response
59
+ #
60
+ # @param req [Stately::Db::TransactionRequest] the request to send
61
+ # @return [Stately::Db::TransactionResponse] the response
62
+ # @api private
63
+ # @!visibility private
64
+ def request_response(req)
65
+ request_only(req)
66
+ begin
67
+ resp = @incoming_responses.next
68
+ if req.message_id != resp.message_id
69
+ raise "Message ID mismatch: request #{req.message_id} != response #{resp.message_id}"
70
+ end
71
+ raise "Response type mismatch" if infer_response_type_from_request(req) != infer_response_type_from_response(resp)
72
+
73
+ resp
74
+ rescue StopIteration
75
+ nil
76
+ end
77
+ end
78
+
79
+ # Send a request and don't wait for a response
80
+ #
81
+ # @param req [Stately::Db::TransactionRequest] the request to send
82
+ # @return [void] nil
83
+ # @api private
84
+ # @!visibility private
85
+ def request_only(req)
86
+ req.message_id = @outgoing_requests.next_message_id
87
+ @outgoing_requests.push(req)
88
+ nil
89
+ end
90
+
91
+ # Send a request and process all responses, until we receive a finished message. This is used for list operations.
92
+ # Each response is processed by the block passed to this method, and the response for this method is a token.
93
+ #
94
+ # @param req [Stately::Db::TransactionRequest] the request to send
95
+ # @yieldparam resp [Stately::Db::TransactionListResponse] the response
96
+ # @return [Stately::Db::ListToken] the token
97
+ # @example
98
+ # request_list_responses(req) do |resp|
99
+ # resp.result.items.each do |result|
100
+ # puts result.item.key_path
101
+ # end
102
+ # @api private
103
+ # @!visibility private
104
+ def request_list_responses(req)
105
+ request_only(req)
106
+ token = nil
107
+ loop do
108
+ resp = @incoming_responses.next.list_results
109
+ if resp.finished
110
+ raw_token = resp.finished.token
111
+ token = StatelyDB::Token.new(token_data: raw_token.token_data,
112
+ can_continue: raw_token.can_continue,
113
+ can_sync: raw_token.can_sync)
114
+ break
115
+ end
116
+ yield resp
117
+ end
118
+ token
119
+ end
120
+
121
+ # Begin a transaction. Begin is called implicitly when the block passed to transaction is called.
122
+ # @return [void] nil
123
+ # @api private
124
+ # @!visibility private
125
+ def begin
126
+ @is_transaction_open = true
127
+ req = Stately::Db::TransactionRequest.new(begin: Stately::Db::TransactionBegin.new(store_id: @store_id.to_i))
128
+ request_only(req)
129
+ @incoming_responses = @stub.transaction(@outgoing_requests)
130
+ nil
131
+ end
132
+
133
+ # Commit a transaction. Commit is called implicitly when the block passed to transaction completes.
134
+ # @return [StatelyDB::Transaction::Transaction::Result]
135
+ # @api private
136
+ # @!visibility private
137
+ def commit
138
+ req = Stately::Db::TransactionRequest.new(
139
+ commit: Google::Protobuf::Empty.new
140
+ )
141
+ resp = request_response(req).finished
142
+ @is_transaction_open = false
143
+ Result.new(
144
+ puts: resp.put_results.map do |result|
145
+ @schema.unmarshal_item(stately_item: result)
146
+ end,
147
+ deletes: resp.delete_results.map(&:key_path)
148
+ )
149
+ end
150
+
151
+ # Abort a transaction. Abort is called implicitly if an exception is raised within the block passed to transaction.
152
+ # @return [Stately::Db::TransactionResponse]
153
+ # @api private
154
+ # @!visibility private
155
+ def abort
156
+ req = Stately::Db::TransactionRequest.new(
157
+ abort: Google::Protobuf::Empty.new
158
+ )
159
+ resp = request_only(req)
160
+ @is_transaction_open = false
161
+ resp
162
+ end
163
+
164
+ # Check if a transaction is open. A transaction is open if begin has been called and commit or abort has not been called.
165
+ #
166
+ # @return [Boolean] true if a transaction is open
167
+ # @api private
168
+ # @!visibility private
169
+ def open?
170
+ @is_transaction_open
171
+ end
172
+
173
+ # Fetch Items from a StatelyDB Store at the given key_path. Note that Items need to exist before being retrieved inside a
174
+ # transaction.
175
+ #
176
+ # @param key_path [String] the path to the item
177
+ # @return [StatelyDB::Item, NilClass] the item or nil if not found
178
+ # @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
179
+ # @raise [StatelyDB::Error::NotFound] if the item is not found
180
+ #
181
+ # @example
182
+ # client.data.transaction do |txn|
183
+ # item = txn.get("/ItemType-identifier")
184
+ # end
185
+ def get(key_path)
186
+ resp = get_batch(key_path)
187
+
188
+ # Always return a single Item.
189
+ resp.first
190
+ end
191
+
192
+ # Fetch a batch of Items from a StatelyDB Store at the given key_paths. Note that Items need to exist before being retrieved
193
+ # inside a transaction.
194
+ #
195
+ # @param key_paths [String, Array<String>] the paths to the items
196
+ # @return [Array<StatelyDB::Item>] the items
197
+ # @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
198
+ # @raise [StatelyDB::Error::NotFound] if the item is not found
199
+ #
200
+ # Example:
201
+ # client.data.transaction do |txn|
202
+ # items = txn.get_batch("/foo", "/bar")
203
+ # end
204
+ def get_batch(*key_paths)
205
+ key_paths = Array(key_paths).flatten
206
+ req = Stately::Db::TransactionRequest.new(
207
+ get_items: Stately::Db::TransactionGet.new(
208
+ gets: key_paths.map { |key_path| Stately::Db::GetItem.new(key_path: String(key_path)) }
209
+ )
210
+ )
211
+ resp = request_response(req).get_results
212
+
213
+ resp.items.map do |result|
214
+ @schema.unmarshal_item(stately_item: result)
215
+ end
216
+ end
217
+
218
+ # Put a single Item into a StatelyDB store. Results are not returned until the transaction is
219
+ # committed and will be available in the Result object returned by commit. An identifier for
220
+ # the item will be returned while inside the transaction block.
221
+ #
222
+ # @param item [StatelyDB::Item] the item to store
223
+ # @return [String, Integer] the id of the item
224
+ #
225
+ # @example
226
+ # results = client.data.transaction do |txn|
227
+ # txn.put(my_item)
228
+ # end
229
+ # results.puts.each do |result|
230
+ # puts result.key_path
231
+ # end
232
+ def put(item)
233
+ resp = put_batch(item)
234
+ resp.first
235
+ end
236
+
237
+ # Put a batch of Items into a StatelyDB Store. Results are not returned until the transaction is
238
+ # committed and will be available in the Result object returned by commit. A list of identifiers
239
+ # for the items will be returned while inside the transaction block.
240
+ #
241
+ # @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store
242
+ # @return [Array<String>] the key paths of the items
243
+ #
244
+ # @example
245
+ # results = client.data.transaction do |txn|
246
+ # txn.put_batch(item1, item2)
247
+ # end
248
+ # results.puts.each do |result|
249
+ # puts result.key_path
250
+ # end
251
+ def put_batch(*items)
252
+ items = Array(items).flatten
253
+ req = Stately::Db::TransactionRequest.new(
254
+ put_items: Stately::Db::TransactionPut.new(
255
+ puts: items.map do |item|
256
+ Stately::Db::PutItem.new(
257
+ item: item.send("marshal_stately")
258
+ )
259
+ end
260
+ )
261
+ )
262
+
263
+ resp = request_response(req).put_ack
264
+ resp.generated_ids.map(&:value)
265
+ end
266
+
267
+ # Delete one or more Items from a StatelyDB Store at the given key_paths. Results are not returned until the transaction is
268
+ # committed and will be available in the Result object returned by commit.
269
+ #
270
+ # @param key_paths [String, Array<String>] the paths to the items
271
+ # @return [void] nil
272
+ #
273
+ # Example:
274
+ # client.data.transaction do |txn|
275
+ # txn.delete("/ItemType-identifier", "/ItemType-identifier2")
276
+ # end
277
+ def delete(*key_paths)
278
+ key_paths = Array(key_paths).flatten
279
+ req = Stately::Db::TransactionRequest.new(
280
+ delete_items: Stately::Db::TransactionDelete.new(
281
+ deletes: key_paths.map { |key_path| Stately::Db::DeleteItem.new(key_path: String(key_path)) }
282
+ )
283
+ )
284
+ request_only(req)
285
+ nil
286
+ end
287
+
288
+ # Begin listing Items from a StatelyDB Store at the given prefix.
289
+ #
290
+ # @param prefix [String] the prefix to list
291
+ # @param limit [Integer] the maximum number of items to return
292
+ # @param sort_property [String] the property to sort by
293
+ # @param sort_direction [Symbol] the direction to sort by (:ascending or :descending)
294
+ # @return [(Array<StatelyDB::Item>, Stately::Db::ListToken)] the list of Items and the token
295
+ #
296
+ # Example:
297
+ # client.data.transaction do |txn|
298
+ # (items, token) = txn.begin_list("/ItemType-identifier")
299
+ # (items, token) = txn.continue_list(token)
300
+ # end
301
+ def begin_list(prefix,
302
+ limit: 100,
303
+ sort_property: nil,
304
+ sort_direction: :ascending)
305
+ sort_direction = case sort_direction
306
+ when :ascending
307
+ 0
308
+ else
309
+ 1
310
+ end
311
+ req = Stately::Db::TransactionRequest.new(
312
+ begin_list: Stately::Db::TransactionBeginList.new(
313
+ key_path_prefix: String(prefix),
314
+ limit:,
315
+ sort_property:,
316
+ sort_direction:
317
+ )
318
+ )
319
+ do_list_request_response(req)
320
+ end
321
+
322
+ # Continue listing Items from a StatelyDB Store using a token.
323
+ #
324
+ # @param token [Stately::Db::ListToken] the token to continue from
325
+ # @param continue_direction [Symbol] the direction to continue by (:forward or :backward)
326
+ # @return [(Array<StatelyDB::Item>, Stately::Db::ListToken)] the list of Items and the token
327
+ #
328
+ # Example:
329
+ # client.data.transaction do |txn|
330
+ # (items, token) = txn.begin_list("/foo")
331
+ # (items, token) = txn.continue_list(token)
332
+ # end
333
+ def continue_list(token, continue_direction: :forward)
334
+ continue_direction = continue_direction == :forward ? 0 : 1
335
+
336
+ req = Stately::Db::TransactionRequest.new(
337
+ continue_list: Stately::Db::TransactionContinueList.new(
338
+ token_data: token.token_data,
339
+ direction: continue_direction
340
+ )
341
+ )
342
+ do_list_request_response(req)
343
+ end
344
+
345
+ private
346
+
347
+ # Processes a list response from begin_list or continue_list
348
+ #
349
+ # @param req [Stately::Db::TransactionRequest] the request to send
350
+ # @return [(Array<StatelyDB::Item>, Stately::Db::ListToken)] the list of Items and the token
351
+ # @api private
352
+ # @!visibility private
353
+ def do_list_request_response(req)
354
+ items = []
355
+ token = request_list_responses(req) do |resp|
356
+ resp.result.items.each do |list_items_result|
357
+ items << @schema.unmarshal_item(stately_item: list_items_result)
358
+ end
359
+ end
360
+ [items, token]
361
+ end
362
+
363
+ # We are using a oneof inside the TransactionRequest to determine the type of request. The ruby
364
+ # generated code does not have a helper for the internal request type so we need to infer it.
365
+ #
366
+ # @param req [Stately::Db::TransactionRequest] the request
367
+ # @return [Class] the response type
368
+ # @api private
369
+ # @!visibility private
370
+ def infer_response_type_from_request(req)
371
+ if req.respond_to?(:get_items)
372
+ Stately::Db::TransactionGetResponse
373
+ elsif req.respond_to?(:list_items)
374
+ Stately::Db::TransactionListResponse
375
+ else
376
+ raise "Unknown request type or request type does not have a corresponding response type"
377
+ end
378
+ end
379
+
380
+ # We are using a oneof inside the TransactionResponse to determine the type of response. The ruby
381
+ # generated code does not have a helper for the internal response type so we need to infer it.
382
+ #
383
+ # @param resp [Stately::Db::TransactionResponse] the response
384
+ # @return [Class] the response type
385
+ # @api private
386
+ # @!visibility private
387
+ def infer_response_type_from_response(resp)
388
+ if resp.respond_to?(:get_results)
389
+ Stately::Db::TransactionGetResponse
390
+ elsif resp.respond_to?(:list_results)
391
+ Stately::Db::TransactionListResponse
392
+ else
393
+ raise "Unknown response type"
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end
data/lib/uuid.rb ADDED
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatelyDB
4
+ # UUID is a helper class for working with UUIDs in StatelyDB. The ruby version of a StatelyDB is a binary string,
5
+ # and this class provides convenience methods for converting to the base16 representation specified in RFC 9562.
6
+ # Internally this class uses the byte_string attribute to store the UUID as a string with the Encoding::ASCII_8BIT
7
+ # encoding.
8
+ class UUID
9
+ attr_accessor :byte_string
10
+
11
+ # @param [String] byte_string A binary-encoded string (eg: Encoding::ASCII_8BIT encoding)
12
+ def initialize(byte_string)
13
+ @byte_string = byte_string
14
+ end
15
+
16
+ # to_s returns the UUID as a base16 string (eg: "f4a8a24a-129d-411f-91d2-6d19d0eaa096")
17
+ # @return [String]
18
+ def to_s
19
+ to_str
20
+ end
21
+
22
+ # to_str returns the UUID as a base16 string (eg: "f4a8a24a-129d-411f-91d2-6d19d0eaa096")
23
+ #
24
+ # Note: to_str is a type coercion method that is called by Ruby when an object is coerced to a string.
25
+ # @return [String]
26
+ def to_str
27
+ @byte_string.unpack("H8H4H4H4H12").join("-")
28
+ end
29
+
30
+ # Encodes the byte string as a url-safe base64 string with padding removed.
31
+ # @return [String]
32
+ def to_base64
33
+ [@byte_string].pack("m0").tr("=", "").tr("+/", "-_")
34
+ end
35
+
36
+ # UUIDs are equal if their byte_strings are equal.
37
+ # @param [StatelyDB::UUID] other
38
+ # @return [Boolean]
39
+ def ==(other)
40
+ self.class == other.class &&
41
+ @byte_string == other.byte_string
42
+ end
43
+
44
+ # UUIDs are sorted lexigraphically by their base16 representation.
45
+ # @param [StatelyDB::UUID] other
46
+ # @return [Integer]
47
+ def <=>(other)
48
+ to_s <=> other.to_s
49
+ end
50
+
51
+ # Parses a base16 string (eg: "f4a8a24a-129d-411f-91d2-6d19d0eaa096") into a UUID object.
52
+ # The string can be the following:
53
+ # 1. Encoded as Encoding::ASCII_8BIT (also aliased as Encoding::BINARY) and be 16 bytes long.
54
+ # 2. A string of the form "f4a8a24a-129d-411f-91d2-6d19d0eaa096"
55
+ # @param [String] byte_string A binary-encoded string (eg: Encoding::ASCII_8BIT encoding) that is 16 bytes in length, or a
56
+ # base16-formatted UUID string.
57
+ # @return [StatelyDB::UUID]
58
+ def self.parse(byte_string)
59
+ if byte_string.encoding == Encoding::BINARY && byte_string.bytesize == 16
60
+ return new(byte_string)
61
+ elsif byte_string.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
62
+ return new([byte_string.delete("-")].pack("H*"))
63
+ end
64
+
65
+ raise "Invalid UUID"
66
+ end
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statelydb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Stately Cloud, Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: async
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.10.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.10.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: async-http
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.64.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.64.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: grpc
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.63.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.63.0
55
+ description: ''
56
+ email: ruby@stately.cloud
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/api/db/continue_list_pb.rb
62
+ - lib/api/db/delete_pb.rb
63
+ - lib/api/db/get_pb.rb
64
+ - lib/api/db/item_pb.rb
65
+ - lib/api/db/item_property_pb.rb
66
+ - lib/api/db/list_pb.rb
67
+ - lib/api/db/list_token_pb.rb
68
+ - lib/api/db/put_pb.rb
69
+ - lib/api/db/scan_root_paths_pb.rb
70
+ - lib/api/db/service_pb.rb
71
+ - lib/api/db/service_services_pb.rb
72
+ - lib/api/db/sync_list_pb.rb
73
+ - lib/api/db/transaction_pb.rb
74
+ - lib/api/errors/error_details_pb.rb
75
+ - lib/common/auth/auth0_token_provider.rb
76
+ - lib/common/auth/interceptor.rb
77
+ - lib/common/auth/token_provider.rb
78
+ - lib/common/error_interceptor.rb
79
+ - lib/common/net/conn.rb
80
+ - lib/error.rb
81
+ - lib/key_path.rb
82
+ - lib/statelydb.rb
83
+ - lib/token.rb
84
+ - lib/transaction/queue.rb
85
+ - lib/transaction/transaction.rb
86
+ - lib/uuid.rb
87
+ homepage: https://stately.cloud/sdk
88
+ licenses:
89
+ - Apache-2.0
90
+ metadata:
91
+ rubygems_mfa_required: 'true'
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.3.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.5.11
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: A library for interacting with StatelyDB
111
+ test_files: []