statelydb 0.10.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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