statelydb 0.1.1

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,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: []