Skip to content

Commit bb1b2b7

Browse files
committed
Simplify upsert API to use item and optional expression
- Change upsert method signature from (key, UpdateItemEnhancedRequest) to (item, Expression?) - This maintains type safety by using the item type directly - Removes exposure to low-level AWS UpdateItemEnhancedRequest - Keeps the idiomatic Kotlin API with optional Expression parameter - Updates both sync and async View interfaces - Updates implementation in DynamoDbView to build UpdateItemEnhancedRequest internally - Updates tests to use the simplified API
1 parent fa9130b commit bb1b2b7

File tree

4 files changed

+35
-40
lines changed

4 files changed

+35
-40
lines changed

tempest2/src/main/kotlin/app/cash/tempest2/AsyncView.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,17 @@ interface AsyncView<K : Any, I : Any> {
147147

148148
/**
149149
* Performs an upsert operation using DynamoDB's UpdateItem operation.
150-
* Executes the provided UpdateItem request and returns the result.
150+
* Creates or updates an item with the provided data and optional condition.
151151
*
152152
* This operation uses a single UpdateItem call. Any exceptions from
153153
* DynamoDB (including ConditionalCheckFailedException) are bubbled up
154154
* to the caller.
155155
*
156-
* @param key The key of the item to upsert
157-
* @param updateRequest The UpdateItem request to execute
156+
* @param item The item to upsert
157+
* @param upsertExpression Optional condition expression for the upsert
158158
* @return The result of the UpdateItem operation
159159
*/
160-
suspend fun upsert(key: K, updateRequest: UpdateItemEnhancedRequest<*>): I = upsertAsync(key, updateRequest).await()
160+
suspend fun upsert(item: I, upsertExpression: Expression? = null): I = upsertAsync(item, upsertExpression).await()
161161

162-
fun upsertAsync(key: K, updateRequest: UpdateItemEnhancedRequest<*>): CompletableFuture<I>
162+
fun upsertAsync(item: I, upsertExpression: Expression? = null): CompletableFuture<I>
163163
}

tempest2/src/main/kotlin/app/cash/tempest2/View.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,15 @@ interface View<K : Any, I : Any> {
112112

113113
/**
114114
* Performs an upsert operation using DynamoDB's UpdateItem operation.
115-
* Executes the provided UpdateItem request and returns the result.
115+
* Creates or updates an item with the provided data and optional condition.
116116
*
117117
* This operation uses a single UpdateItem call. Any exceptions from
118118
* DynamoDB (including ConditionalCheckFailedException) are bubbled up
119119
* to the caller.
120120
*
121-
* @param key The key of the item to upsert
122-
* @param updateRequest The UpdateItem request to execute
121+
* @param item The item to upsert
122+
* @param upsertExpression Optional condition expression for the upsert
123123
* @return The result of the UpdateItem operation
124124
*/
125-
fun upsert(key: K, updateRequest: UpdateItemEnhancedRequest<*>): I
125+
fun upsert(item: I, upsertExpression: Expression? = null): I
126126
}

tempest2/src/main/kotlin/app/cash/tempest2/internal/DynamoDbView.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,13 @@ internal class DynamoDbView<K : Any, I : Any, R : Any>(
102102
return toItem(itemObject)
103103
}
104104

105-
override fun upsert(key: K, updateRequest: UpdateItemEnhancedRequest<*>): I {
106-
val result = dynamoDbTable.updateItem(updateRequest as UpdateItemEnhancedRequest<R>)
105+
override fun upsert(item: I, upsertExpression: Expression?): I {
106+
val itemObject = itemCodec.toDb(item)
107+
val request = UpdateItemEnhancedRequest.builder(tableSchema.itemType().rawClass())
108+
.item(itemObject)
109+
.conditionExpression(upsertExpression)
110+
.build()
111+
val result = dynamoDbTable.updateItem(request)
107112
return itemCodec.toApp(result)
108113
}
109114
}
@@ -169,8 +174,13 @@ internal class DynamoDbView<K : Any, I : Any, R : Any>(
169174
return dynamoDbTable.deleteItem(request).thenApply(::toItem)
170175
}
171176

172-
override fun upsertAsync(key: K, updateRequest: UpdateItemEnhancedRequest<*>): CompletableFuture<I> {
173-
return dynamoDbTable.updateItem(updateRequest as UpdateItemEnhancedRequest<R>)
177+
override fun upsertAsync(item: I, upsertExpression: Expression?): CompletableFuture<I> {
178+
val itemObject = itemCodec.toDb(item)
179+
val request = UpdateItemEnhancedRequest.builder(tableSchema.itemType().rawClass())
180+
.item(itemObject)
181+
.conditionExpression(upsertExpression)
182+
.build()
183+
return dynamoDbTable.updateItem(request)
174184
.thenApply { result ->
175185
itemCodec.toApp(result)
176186
}

tempest2/src/test/kotlin/app/cash/tempest2/DynamoDbViewTest.kt

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -187,18 +187,13 @@ class DynamoDbViewTest {
187187
"Pop"
188188
)
189189

190-
// Create an UpdateItem request with condition to only create if item doesn't exist
191-
val updateRequest = UpdateItemEnhancedRequest.builder(MusicItem::class.java)
192-
.item(musicTable.codec(AlbumInfo::class).toDb(albumInfo))
193-
.conditionExpression(
194-
Expression.builder()
195-
.expression("attribute_not_exists(partition_key)")
196-
.build()
197-
)
190+
// Create condition to only create if item doesn't exist
191+
val condition = Expression.builder()
192+
.expression("attribute_not_exists(partition_key)")
198193
.build()
199194

200195
// Upsert should create the item since it doesn't exist
201-
val result = musicTable.albumInfo.upsert(albumInfo.key, updateRequest)
196+
val result = musicTable.albumInfo.upsert(albumInfo, condition)
202197
assertThat(result).isEqualTo(albumInfo)
203198

204199
// Verify the item was created
@@ -228,20 +223,15 @@ class DynamoDbViewTest {
228223
"Jazz"
229224
)
230225

231-
// Create an UpdateItem request with condition to only create if item doesn't exist
232-
val updateRequest = UpdateItemEnhancedRequest.builder(MusicItem::class.java)
233-
.item(musicTable.codec(AlbumInfo::class).toDb(modifiedAlbumInfo))
234-
.conditionExpression(
235-
Expression.builder()
236-
.expression("attribute_not_exists(partition_key)")
237-
.build()
238-
)
226+
// Create condition to only create if item doesn't exist
227+
val condition = Expression.builder()
228+
.expression("attribute_not_exists(partition_key)")
239229
.build()
240230

241231
// Upsert should throw ConditionalCheckFailedException since item already exists
242232
assertThatExceptionOfType(ConditionalCheckFailedException::class.java)
243233
.isThrownBy {
244-
musicTable.albumInfo.upsert(originalAlbumInfo.key, updateRequest)
234+
musicTable.albumInfo.upsert(modifiedAlbumInfo, condition)
245235
}
246236

247237
// Verify the original item is still there unchanged
@@ -262,20 +252,15 @@ class DynamoDbViewTest {
262252
// Simulate concurrent creation by saving directly first
263253
musicTable.albumInfo.save(albumInfo)
264254

265-
// Create an UpdateItem request with condition to only create if item doesn't exist
266-
val updateRequest = UpdateItemEnhancedRequest.builder(MusicItem::class.java)
267-
.item(musicTable.codec(AlbumInfo::class).toDb(albumInfo))
268-
.conditionExpression(
269-
Expression.builder()
270-
.expression("attribute_not_exists(partition_key)")
271-
.build()
272-
)
255+
// Create condition to only create if item doesn't exist
256+
val condition = Expression.builder()
257+
.expression("attribute_not_exists(partition_key)")
273258
.build()
274259

275260
// Now upsert should throw ConditionalCheckFailedException since item already exists
276261
assertThatExceptionOfType(ConditionalCheckFailedException::class.java)
277262
.isThrownBy {
278-
musicTable.albumInfo.upsert(albumInfo.key, updateRequest)
263+
musicTable.albumInfo.upsert(albumInfo, condition)
279264
}
280265
}
281266
}

0 commit comments

Comments
 (0)