statelydb 0.10.0 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '039f735f87b49d58192914fdb0ff62cafce5d059980628671854026864248223'
4
- data.tar.gz: 0ee06ba443aae408cb3a25d98041560a48f1ac6ddfd2ef9f25f13b62c21ee34c
3
+ metadata.gz: 14c7769c562abe9ba0294a0d3ec4a1c7a620f9233f35209d39766331f3dd73e5
4
+ data.tar.gz: db2562de5584684ef9baf9de405b38112a43eb123aa3826ae4e563c6cfa0c1d0
5
5
  SHA512:
6
- metadata.gz: 83ef6dd4a7fc6c98d3668ac42544d568105d78421a7c2bc8d0d56e588b4a39eac823473f2e8e797bb339c6c04459bfc39a07457cadf21590d5d90036ff808e61
7
- data.tar.gz: ff67b6ef6513ebe45df1ad0c12ca8d1906cee183e830a3d303d4cd6eda47e2bbf6a22634a55057ece224b13f473df23ad428f324c52806ab7904d76cb47ef159
6
+ metadata.gz: 4ba09f8776771bb4b6fc078e32255a272d986e6983737e1df9fe71bf95e685484ce6d20a91f16c96973bc89f7c9c79d1c0b9411f4a683430581bbc1e27a17468
7
+ data.tar.gz: cf612cf68f6c714ba7e79d328ab2b18236c711dbe806a6fb9714dd93eb163ac9899a415bb6c5d4861d24f103bc3844dff40cde27c4eca37eecf1ac3304298f93
data/lib/api/db/put_pb.rb CHANGED
@@ -7,7 +7,7 @@ require 'google/protobuf'
7
7
  require 'db/item_pb'
8
8
 
9
9
 
10
- descriptor_data = "\n\x0c\x64\x62/put.proto\x12\nstately.db\x1a\rdb/item.proto\"|\n\nPutRequest\x12\x19\n\x08store_id\x18\x01 \x01(\x04R\x07storeId\x12\'\n\x04puts\x18\x02 \x03(\x0b\x32\x13.stately.db.PutItemR\x04puts\x12*\n\x11schema_version_id\x18\x03 \x01(\rR\x0fschemaVersionId\"/\n\x07PutItem\x12$\n\x04item\x18\x01 \x01(\x0b\x32\x10.stately.db.ItemR\x04item\"5\n\x0bPutResponse\x12&\n\x05items\x18\x01 \x03(\x0b\x32\x10.stately.db.ItemR\x05itemsBc\n\x0e\x63om.stately.dbB\x08PutProtoP\x01\xa2\x02\x03SDX\xaa\x02\nStately.Db\xca\x02\nStately\\Db\xe2\x02\x16Stately\\Db\\GPBMetadata\xea\x02\x0bStately::Dbb\x06proto3"
10
+ descriptor_data = "\n\x0c\x64\x62/put.proto\x12\nstately.db\x1a\rdb/item.proto\"|\n\nPutRequest\x12\x19\n\x08store_id\x18\x01 \x01(\x04R\x07storeId\x12\'\n\x04puts\x18\x02 \x03(\x0b\x32\x13.stately.db.PutItemR\x04puts\x12*\n\x11schema_version_id\x18\x03 \x01(\rR\x0fschemaVersionId\"U\n\x07PutItem\x12$\n\x04item\x18\x01 \x01(\x0b\x32\x10.stately.db.ItemR\x04item\x12$\n\x0emust_not_exist\x18\x03 \x01(\x08R\x0cmustNotExist\"5\n\x0bPutResponse\x12&\n\x05items\x18\x01 \x03(\x0b\x32\x10.stately.db.ItemR\x05itemsBc\n\x0e\x63om.stately.dbB\x08PutProtoP\x01\xa2\x02\x03SDX\xaa\x02\nStately.Db\xca\x02\nStately\\Db\xe2\x02\x16Stately\\Db\\GPBMetadata\xea\x02\x0bStately::Dbb\x06proto3"
11
11
 
12
12
  pool = Google::Protobuf::DescriptorPool.generated_pool
13
13
  pool.add_serialized_file(descriptor_data)
@@ -35,7 +35,7 @@ module Stately
35
35
  rpc :Get, ::Stately::Db::GetRequest, ::Stately::Db::GetResponse
36
36
  # Delete removes one or more Items from the Store by their key paths. This
37
37
  # will fail if the caller does not have permission to delete Items.
38
- # Tombstones will be saved for deleted items for time, so
38
+ # Tombstones will be saved for deleted items for some time, so
39
39
  # that SyncList can return information about deleted items. Deletes are
40
40
  # always applied atomically; all will fail or all will succeed.
41
41
  rpc :Delete, ::Stately::Db::DeleteRequest, ::Stately::Db::DeleteResponse
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "async"
4
+ require "async/actor"
4
5
  require "async/http/internet"
5
6
  require "async/semaphore"
6
7
  require "json"
@@ -12,6 +13,7 @@ LOGGER = Logger.new($stdout)
12
13
  LOGGER.level = Logger::WARN
13
14
  DEFAULT_GRANT_TYPE = "client_credentials"
14
15
 
16
+ # A module for Stately Cloud auth code
15
17
  module StatelyDB
16
18
  module Common
17
19
  # A module for Stately Cloud auth code
@@ -21,102 +23,183 @@ module StatelyDB
21
23
  # It will default to using the values of `STATELY_CLIENT_ID` and `STATELY_CLIENT_SECRET` if
22
24
  # no credentials are explicitly passed and will throw an error if none are found.
23
25
  class Auth0TokenProvider < TokenProvider
24
- # @param [String] auth_url The URL of the OAuth server
26
+ # @param [Endpoint] domain The domain of the OAuth server
25
27
  # @param [String] audience The OAuth Audience for the token
26
28
  # @param [String] client_secret The StatelyDB client secret credential
27
29
  # @param [String] client_id The StatelyDB client ID credential
28
30
  def initialize(
29
- auth_url: "https://oauth.stately.cloud",
31
+ domain: "https://oauth.stately.cloud",
30
32
  audience: "api.stately.cloud",
31
33
  client_secret: ENV.fetch("STATELY_CLIENT_SECRET"),
32
34
  client_id: ENV.fetch("STATELY_CLIENT_ID")
33
35
  )
34
36
  super()
35
- @client_id = client_id
36
- @client_secret = client_secret
37
- @audience = audience
38
- @auth_url = "#{auth_url}/oauth/token"
39
- @access_token = nil
40
- @pending_refresh = nil
41
- @timer = nil
42
-
43
- Async do |_task|
44
- refresh_token
45
- end
46
-
47
- # need a weak ref to ourself or the GC will never run the finalizer
48
- ObjectSpace.define_finalizer(WeakRef.new(self), finalize)
37
+ @actor = Async::Actor.new(Actor.new(domain: domain, audience: audience, client_secret: client_secret,
38
+ client_id: client_id))
39
+ # this initialization cannot happen in the constructor because it is async and must run on the event loop
40
+ # which is not available in the constructor
41
+ @actor.init
49
42
  end
50
43
 
51
- # finalizer kills the thread running the timer if one exists
52
- # @return [Proc] The finalizer proc
53
- def finalize
54
- proc {
55
- Thread.kill(@timer) unless @timer.nil?
56
- }
44
+ # Close the token provider and kill any background operations
45
+ # This just invokes the close method on the actor which should do the cleanup
46
+ def close
47
+ @actor.close
57
48
  end
58
49
 
59
50
  # Get the current access token
60
51
  # @return [String] The current access token
61
- def access_token
62
- # TODO: - check whether or not the GIL is enough to make this threadsafe
63
- @access_token || refresh_token
52
+ def get_token(force: false)
53
+ @actor.get_token(force: force)
64
54
  end
65
55
 
66
- private
67
-
68
- # Refresh the access token
69
- # @return [void]
70
- def refresh_token
71
- # never run more than one at a time.
72
- @pending_refresh ||= refresh_token_impl
73
- # many threads all wait on the same task here.
74
- # I wrote a test to check this is possible
75
- @pending_refresh.wait
76
- # multiple people will all set this to nil after
77
- # they are done waiting but I don't think i can put this inside
78
- # refresh_token_impl. It seems harmless because of the GIL?
79
- @pending_refresh = nil
80
- end
56
+ # Actor for managing the token refresh
57
+ # This is designed to be used with Async::Actor and run on a dedicated thread.
58
+ class Actor
59
+ # @param [Endpoint] domain The domain of the OAuth server
60
+ # @param [String] audience The OAuth Audience for the token
61
+ # @param [String] client_secret The StatelyDB client secret credential
62
+ # @param [String] client_id The StatelyDB client ID credential
63
+ def initialize(
64
+ domain: "https://oauth.stately.cloud",
65
+ audience: "api.stately.cloud",
66
+ client_secret: ENV.fetch("STATELY_CLIENT_SECRET"),
67
+ client_id: ENV.fetch("STATELY_CLIENT_ID")
68
+ )
69
+ super()
70
+ @client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(domain))
71
+ @client_id = client_id
72
+ @client_secret = client_secret
73
+ @audience = audience
81
74
 
82
- # Refresh the access token implementation
83
- # @return [String] The new access token
84
- def refresh_token_impl
85
- Async do
86
- client = Async::HTTP::Internet.new
87
- headers = [["content-type", "application/json"]]
88
- data = { "client_id" => @client_id, client_secret: @client_secret, audience: @audience,
89
- grant_type: DEFAULT_GRANT_TYPE }
90
- body = [JSON.dump(data)]
91
-
92
- resp = client.post(@auth_url, headers, body)
93
- resp_data = JSON.parse(resp.read)
94
- raise "Auth request failed: #{resp_data}" if resp.status != 200
95
-
96
- @access_token = resp_data["access_token"]
97
-
98
- # do this on a thread or else the sleep
99
- # will block the event loop.
100
- # there is no non-blocking sleep in ruby.
101
- # skip this if we have a pending timer thread already
102
- @timer = Thread.new do
103
- # Calculate a random multiplier between 0.3 and 0.8 to to apply to the expiry
75
+ @access_token = nil
76
+ @expires_at_secs = nil
77
+ @pending_refresh = nil
78
+ end
79
+
80
+ # Initialize the actor. This runs on the actor thread which means
81
+ # we can dispatch async operations here.
82
+ def init
83
+ refresh_token
84
+ end
85
+
86
+ # Close the token provider and kill any background operations
87
+ def close
88
+ @scheduled&.stop
89
+ @client&.close
90
+ end
91
+
92
+ # Get the current access token
93
+ # @param [Boolean] force Whether to force a refresh of the token
94
+ # @return [String] The current access token
95
+ def get_token(force: false)
96
+ if force
97
+ @access_token = nil
98
+ @expires_at_secs = nil
99
+ else
100
+ token, ok = valid_access_token
101
+ return token if ok
102
+ end
103
+
104
+ refresh_token.wait
105
+ end
106
+
107
+ # Get the current access token and whether it is valid
108
+ # @return [Array] The current access token and whether it is valid
109
+ def valid_access_token
110
+ return "", false if @access_token.nil?
111
+ return "", false if @expires_at_secs.nil?
112
+ return "", false if @expires_at_secs < Time.now.to_i
113
+
114
+ [@access_token, true]
115
+ end
116
+
117
+ # Refresh the access token
118
+ # @return [string] The new access token
119
+ def refresh_token
120
+ Async do
121
+ # we use an Async::Condition to dedupe multiple requests here
122
+ # if the condition exists, we wait on it to complete
123
+ # otherwise we create a condition, make the request, then signal the condition with the result
124
+ # If there is an error then we signal that instead so we can raise it for the waiters.
125
+ if @pending_refresh.nil?
126
+ begin
127
+ @pending_refresh = Async::Condition.new
128
+ new_access_token = refresh_token_impl.wait
129
+ # now broadcast the new token to any waiters
130
+ @pending_refresh.signal(new_access_token)
131
+ new_access_token
132
+ rescue StandardError => e
133
+ @pending_refresh.signal(e)
134
+ raise e
135
+ ensure
136
+ # delete the condition to restart the process
137
+ @pending_refresh = nil
138
+ end
139
+ else
140
+ res = @pending_refresh.wait
141
+ # if the refresh result is an error, re-raise it.
142
+ # otherwise return the token
143
+ raise res if res.is_a?(StandardError)
144
+
145
+ res
146
+ end
147
+ end
148
+ end
149
+
150
+ # Refresh the access token implementation
151
+ # @return [String] The new access token
152
+ def refresh_token_impl
153
+ Async do
154
+ resp_data = make_auth0_request
155
+
156
+ new_access_token = resp_data["access_token"]
157
+ new_expires_in_secs = resp_data["expires_in"]
158
+ new_expires_at_secs = Time.now.to_i + new_expires_in_secs
159
+ if @expires_at_secs.nil? || new_expires_at_secs > @expires_at_secs
160
+
161
+ @access_token = new_access_token
162
+ @expires_at_secs = new_expires_at_secs
163
+ else
164
+
165
+ new_access_token = @access_token
166
+ new_expires_in_secs = @expires_at_secs - Time.now.to_i
167
+ end
168
+
169
+ # Schedule a refresh of the token ahead of the expiry time
170
+ # Calculate a random multiplier between 0.9 and 0.95 to to apply to the expiry
104
171
  # so that we refresh in the background ahead of expiration, but avoid
105
172
  # multiple processes hammering the service at the same time.
106
- jitter = (Random.rand * 0.5) + 0.3
107
- delay = resp_data["expires_in"] * jitter
108
- sleep(delay)
109
- refresh_token
173
+ jitter = (Random.rand * 0.05) + 0.9
174
+ delay_secs = new_expires_in_secs * jitter
175
+
176
+ # do this on the fiber scheduler (the root scheduler) to avoid infinite recursion
177
+ @scheduled ||= Fiber.scheduler.async do
178
+ # Kernel.sleep is non-blocking if Ruby 3.1+ and Async 2+
179
+ # https://github.com/socketry/async/issues/305#issuecomment-1945188193
180
+ sleep(delay_secs)
181
+ refresh_token
182
+ @scheduled = nil
183
+ end
184
+
185
+ new_access_token
186
+ end
187
+ end
188
+
189
+ def make_auth0_request
190
+ headers = [["content-type", "application/json"]]
191
+ body = JSON.dump({ "client_id" => @client_id, client_secret: @client_secret, audience: @audience,
192
+ grant_type: DEFAULT_GRANT_TYPE })
193
+ Sync do
194
+ # TODO: Wrap this in a retry loop and parse errors like we
195
+ # do in the Go SDK.
196
+ response = @client.post("/oauth/token", headers, body)
197
+ raise "Auth request failed" if response.status != 200
198
+
199
+ JSON.parse(response.read)
200
+ ensure
201
+ response&.close
110
202
  end
111
- @pending_refresh = nil
112
- resp_data["access_token"]
113
- rescue StandardError => e
114
- # set the token to nil so that it will
115
- # be refreshed on the next get
116
- @access_token = nil
117
- LOGGER.warn(e)
118
- ensure
119
- client.close
120
203
  end
121
204
  end
122
205
  end
@@ -74,7 +74,7 @@ module StatelyDB
74
74
  # @return [void]
75
75
  # @api private
76
76
  def add_jwt_to_grpc_request(metadata:)
77
- metadata["authorization"] = "Bearer #{@token_provider.access_token}"
77
+ metadata["authorization"] = "Bearer #{@token_provider.get_token}"
78
78
  end
79
79
  end
80
80
  end
@@ -8,8 +8,14 @@ module StatelyDB
8
8
  # for individual token provider implementations
9
9
  class TokenProvider
10
10
  # Get the current access token
11
+ # @param [Boolean] force Whether to force a refresh of the token
11
12
  # @return [String] The current access token
12
- def access_token
13
+ def get_token(force: false) # rubocop:disable Lint/UnusedMethodArgument
14
+ raise "Not Implemented"
15
+ end
16
+
17
+ # Close the token provider and kill any background operations
18
+ def close
13
19
  raise "Not Implemented"
14
20
  end
15
21
  end
data/lib/error.rb CHANGED
@@ -11,10 +11,13 @@ module StatelyDB
11
11
  # The Error class contains common StatelyDB error types.
12
12
  class Error < StandardError
13
13
  # The gRPC/Connect Code for this error.
14
+ # @return [Integer]
14
15
  attr_reader :code
15
16
  # The more fine-grained Stately error code, which is a human-readable string.
17
+ # @return [String]
16
18
  attr_reader :stately_code
17
19
  # The upstream cause of the error, if available.
20
+ # @return [Exception]
18
21
  attr_reader :cause
19
22
 
20
23
  # @param [String] message
@@ -53,6 +56,8 @@ module StatelyDB
53
56
  new(error.message, code: GRPC::Core::StatusCodes::UNKNOWN, stately_code: "Unknown", cause: error)
54
57
  end
55
58
 
59
+ # Turn this error's gRPC status code into a human-readable string. e.g. 3 -> "InvalidArgument"
60
+ # @return [String]
56
61
  def code_string
57
62
  self.class.grpc_code_to_string(@code)
58
63
  end
data/lib/statelydb.rb CHANGED
@@ -38,20 +38,36 @@ module StatelyDB
38
38
  token_provider: Common::Auth::Auth0TokenProvider.new,
39
39
  endpoint: nil,
40
40
  region: nil)
41
+ if store_id.nil?
42
+ raise StatelyDB::Error.new("store_id is required",
43
+ code: GRPC::Core::StatusCodes::INVALID_ARGUMENT,
44
+ stately_code: "InvalidArgument")
45
+ end
46
+ if schema.nil?
47
+ raise StatelyDB::Error.new("schema is required",
48
+ code: GRPC::Core::StatusCodes::INVALID_ARGUMENT,
49
+ stately_code: "InvalidArgument")
50
+ end
41
51
 
42
52
  endpoint = self.class.make_endpoint(endpoint:, region:)
43
- channel = Common::Net.new_channel(endpoint:)
53
+ @channel = Common::Net.new_channel(endpoint:)
54
+ @token_provider = token_provider
44
55
 
45
56
  auth_interceptor = Common::Auth::Interceptor.new(token_provider:)
46
57
  error_interceptor = Common::ErrorInterceptor.new
47
58
 
48
- @stub = Stately::Db::DatabaseService::Stub.new(nil, nil, channel_override: channel,
59
+ @stub = Stately::Db::DatabaseService::Stub.new(nil, nil, channel_override: @channel,
49
60
  interceptors: [error_interceptor, auth_interceptor])
50
61
  @store_id = store_id.to_i
51
62
  @schema = schema
52
63
  @allow_stale = false
53
64
  end
54
65
 
66
+ def close
67
+ @channel&.close
68
+ @token_provider&.close
69
+ end
70
+
55
71
  # Set whether to allow stale results for all operations with this client. This produces a new client
56
72
  # with the allow_stale flag set.
57
73
  # @param allow_stale [Boolean] whether to allow stale results
@@ -169,12 +185,19 @@ module StatelyDB
169
185
  # Put an Item into a StatelyDB Store at the given key_path.
170
186
  #
171
187
  # @param item [StatelyDB::Item] a StatelyDB Item
188
+ # @param must_not_exist [Boolean] A condition that indicates this item must
189
+ # not already exist at any of its key paths. If there is already an item
190
+ # at one of those paths, the Put operation will fail with a
191
+ # "ConditionalCheckFailed" error. Note that if the item has an
192
+ # `initialValue` field in its key, that initial value will automatically
193
+ # be chosen not to conflict with existing items, so this condition only
194
+ # applies to key paths that do not contain the `initialValue` field.
172
195
  # @return [StatelyDB::Item] the item that was stored
173
196
  #
174
- # @example
175
- # client.data.put(my_item)
176
- def put(item)
177
- resp = put_batch(item)
197
+ # @example client.data.put(my_item)
198
+ # @example client.data.put(my_item, must_not_exist: true)
199
+ def put(item, must_not_exist: false)
200
+ resp = put_batch({ item:, must_not_exist: })
178
201
 
179
202
  # Always return a single Item.
180
203
  resp.first
@@ -182,21 +205,32 @@ module StatelyDB
182
205
 
183
206
  # Put a batch of up to 50 Items into a StatelyDB Store.
184
207
  #
185
- # @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store. Max 50 items.
208
+ # @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store.
209
+ # Max 50 items.
186
210
  # @return [Array<StatelyDB::Item>] the items that were stored
187
211
  #
188
212
  # @example
189
213
  # client.data.put_batch(item1, item2)
214
+ # @example
215
+ # client.data.put_batch({ item: item1, must_not_exist: true }, item2)
190
216
  def put_batch(*items)
191
- items = Array(items).flatten
192
- req = Stately::Db::PutRequest.new(
193
- store_id: @store_id,
194
- schema_version_id: @schema::SCHEMA_VERSION_ID,
195
- puts: items.map do |item|
217
+ puts = Array(items).flatten.map do |input|
218
+ if input.is_a?(Hash)
219
+ item = input[:item]
220
+ Stately::Db::PutItem.new(
221
+ item: item.send("marshal_stately"),
222
+ must_not_exist: input[:must_not_exist]
223
+ )
224
+ else
196
225
  Stately::Db::PutItem.new(
197
- item: item.send("marshal_stately")
226
+ item: input.send("marshal_stately")
198
227
  )
199
228
  end
229
+ end
230
+ req = Stately::Db::PutRequest.new(
231
+ store_id: @store_id,
232
+ schema_version_id: @schema::SCHEMA_VERSION_ID,
233
+ puts:
200
234
  )
201
235
  resp = @stub.put(req)
202
236
 
@@ -208,8 +242,8 @@ module StatelyDB
208
242
  # Delete up to 50 Items from a StatelyDB Store at the given key_paths.
209
243
  #
210
244
  # @param key_paths [String, Array<String>] the paths to the items. Max 50 key paths.
211
- # @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
212
- # @raise [StatelyDB::Error::NotFound] if the item is not found
245
+ # @raise [StatelyDB::Error] if the parameters are invalid
246
+ # @raise [StatelyDB::Error] if the item is not found
213
247
  # @return [void] nil
214
248
  #
215
249
  # @example
@@ -230,8 +264,8 @@ module StatelyDB
230
264
  # If the block completes successfully, the transaction is committed.
231
265
  #
232
266
  # @return [StatelyDB::Transaction::Transaction::Result] the result of the transaction
233
- # @raise [StatelyDB::Error::InvalidParameters] if the parameters are invalid
234
- # @raise [StatelyDB::Error::NotFound] if the item is not found
267
+ # @raise [StatelyDB::Error] if the parameters are invalid
268
+ # @raise [StatelyDB::Error] if the item is not found
235
269
  # @raise [Exception] if any other exception is raised
236
270
  #
237
271
  # @example
@@ -226,6 +226,13 @@ module StatelyDB
226
226
  # the item will be returned while inside the transaction block.
227
227
  #
228
228
  # @param item [StatelyDB::Item] the item to store
229
+ # @param must_not_exist [Boolean] A condition that indicates this item must
230
+ # not already exist at any of its key paths. If there is already an item
231
+ # at one of those paths, the Put operation will fail with a
232
+ # "ConditionalCheckFailed" error. Note that if the item has an
233
+ # `initialValue` field in its key, that initial value will automatically
234
+ # be chosen not to conflict with existing items, so this condition only
235
+ # applies to key paths that do not contain the `initialValue` field.
229
236
  # @return [String, Integer] the id of the item
230
237
  #
231
238
  # @example
@@ -235,16 +242,18 @@ module StatelyDB
235
242
  # results.puts.each do |result|
236
243
  # puts result.key_path
237
244
  # end
238
- def put(item)
239
- resp = put_batch(item)
245
+ def put(item, must_not_exist: false)
246
+ resp = put_batch({ item:, must_not_exist: })
240
247
  resp.first
241
248
  end
242
249
 
243
- # Put a batch of up to 50 Items into a StatelyDB Store. Results are not returned until the transaction is
244
- # committed and will be available in the Result object returned by commit. A list of identifiers
245
- # for the items will be returned while inside the transaction block.
250
+ # Put a batch of up to 50 Items into a StatelyDB Store. Results are not
251
+ # returned until the transaction is committed and will be available in the
252
+ # Result object returned by commit. A list of identifiers for the items
253
+ # will be returned while inside the transaction block.
246
254
  #
247
- # @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store. Max 50 items.
255
+ # @param items [StatelyDB::Item, Array<StatelyDB::Item>] the items to store. Max
256
+ # 50 items.
248
257
  # @return [Array<StatelyDB::UUID, String, Integer, nil>] the ids of the items
249
258
  #
250
259
  # @example
@@ -255,14 +264,22 @@ module StatelyDB
255
264
  # puts result.key_path
256
265
  # end
257
266
  def put_batch(*items)
258
- items = Array(items).flatten
267
+ puts = Array(items).flatten.map do |input|
268
+ if input.is_a?(Hash)
269
+ item = input[:item]
270
+ Stately::Db::PutItem.new(
271
+ item: item.send("marshal_stately"),
272
+ must_not_exist: input[:must_not_exist]
273
+ )
274
+ else
275
+ Stately::Db::PutItem.new(
276
+ item: input.send("marshal_stately")
277
+ )
278
+ end
279
+ end
259
280
  req = Stately::Db::TransactionRequest.new(
260
281
  put_items: Stately::Db::TransactionPut.new(
261
- puts: items.map do |item|
262
- Stately::Db::PutItem.new(
263
- item: item.send("marshal_stately")
264
- )
265
- end
282
+ puts:
266
283
  )
267
284
  )
268
285
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statelydb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stately Cloud, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-20 00:00:00.000000000 Z
11
+ date: 2024-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 2.10.1
19
+ version: 2.21.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 2.10.1
26
+ version: 2.21.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: async-actor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.1
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: async-http
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - '='
32
46
  - !ruby/object:Gem::Version
33
- version: 0.64.0
47
+ version: 0.85.0
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - '='
39
53
  - !ruby/object:Gem::Version
40
- version: 0.64.0
54
+ version: 0.85.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: grpc
43
57
  requirement: !ruby/object:Gem::Requirement