Skip to content
This repository has been archived by the owner on Nov 15, 2024. It is now read-only.

Commit

Permalink
Rework DynamoDb API to be typesafe (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton authored Mar 23, 2021
1 parent ffa9b92 commit 46baa63
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 166 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
This list is not intended to be all-encompassing - it will document major and breaking API
changes with their rationale when appropriate. Given version `A.B.C.D`, breaking changes are to be expected in version number increments where changes in the `A` or `B` sections:

### v2.17.0.0
- **http4k-connect-amazon-dynamodb** : Reworked DynamoDb API to be typesafe, tightened up types in responses, added Scan.

### v2.16.0.0
- **http4k-connect-amazon-dynamodb** : New client module. No fake as yet.
- **http4k-connect-amazon-*** : [Break] Rename `Base64Blob.encoded()` -> `Base64Blob.encode()` for clarity.
Expand Down
1 change: 1 addition & 0 deletions amazon/dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The DynamoDb connector provides the following Actions:
* GetItem
* PutItem
* Query
* Scan
* UpdateItem

* TransactGetItems
Expand Down
50 changes: 26 additions & 24 deletions amazon/dynamodb/client/src/examples/kotlin/using_connect_client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ import org.http4k.connect.amazon.dynamodb.getItem
import org.http4k.connect.amazon.dynamodb.putItem
import org.http4k.connect.amazon.model.Attribute
import org.http4k.connect.amazon.model.Base64Blob
import org.http4k.connect.amazon.model.Item
import org.http4k.connect.amazon.model.Region
import org.http4k.connect.amazon.model.TableName
import org.http4k.connect.amazon.model.with
import org.http4k.connect.successValue
import org.http4k.core.HttpHandler
import org.http4k.filter.debug

private val attrBool = Attribute.boolean("theBool")
private val attrB = Attribute.base64Blob("theBase64Blob")
private val attrBS = Attribute.base64Blobs("theBase64Blobs")
private val attrN = Attribute.number("theNum")
private val attrNS = Attribute.numbers("theNums")
private val attrL = Attribute.list("theList")
private val attrM = Attribute.map("theMap")
private val attrS = Attribute.string("theString")
private val attrSS = Attribute.strings("theStrings")
private val attrNL = Attribute.string("theNull")
private val attrBool = Attribute.boolean().required("theBool")
private val attrB = Attribute.base64Blob().required("theBase64Blob")
private val attrBS = Attribute.base64Blobs().required("theBase64Blobs")
private val attrN = Attribute.int().required("theNum")
private val attrNS = Attribute.ints().required("theNums")
private val attrL = Attribute.list().required("theList")
private val attrM = Attribute.map().required("theMap")
private val attrS = Attribute.string().required("theString")
private val attrSS = Attribute.strings().required("theStrings")
private val attrNL = Attribute.string().optional("theNull")

fun main() {
// we can connect to the real service
Expand All @@ -40,30 +42,30 @@ fun main() {

val table = TableName.of("myTable")

// we can bind values to the attributes
// we can bind typed values to the attributes of an item
client.putItem(
table,
item = mapOf(
attrS to "foobar",
attrBool to true,
attrB to Base64Blob.encode("foo"),
attrBS to setOf(Base64Blob.encode("bar")),
attrN to 123,
attrNS to setOf(123, 12.34),
attrL to listOf(
item = Item(
attrS of "foobar",
attrBool of true,
attrB of Base64Blob.encode("foo"),
attrBS of setOf(Base64Blob.encode("bar")),
attrN of 123,
attrNS of setOf(123, 321),
attrL of listOf(
List(listOf(AttributeValue.Str("foo"))),
Num(123),
Null()
),
attrM to mapOf(attrS to "foo", attrBool to false),
attrSS to setOf("345", "567"),
attrNL to null
attrM of Item().with(attrS of "foo", attrBool of false),
attrSS of setOf("345", "567"),
attrNL of null
)
)

// lookup an item from the database
val item = client.getItem(table, key = mapOf(attrS to "hello")).successValue().item
val str: String? = attrS[item]
val item = client.getItem(table, key = Item(attrS of "hello")).successValue().item!!
val str: String = attrS(item)

// all operations return a Result monad of the API type
val deleteResult: Result<TableDescriptionResponse, RemoteFailure> = client.deleteTable(table)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import org.http4k.connect.amazon.dynamodb.action.KotshiReqWriteItemJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiRestoreSummaryJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiSSEDescriptionJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiSSESpecificationJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiScanJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiScanResponseJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiStatementResponseJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiStreamSpecificationJsonAdapter
import org.http4k.connect.amazon.dynamodb.action.KotshiTableDescriptionJsonAdapter
Expand Down Expand Up @@ -109,9 +111,10 @@ object DynamoDbJsonAdapterFactory : AwsJsonAdapterFactory(
adapter(::KotshiGetItemJsonAdapter),
adapter(::KotshiPutItemJsonAdapter),
adapter(::KotshiQueryJsonAdapter),
adapter(::KotshiUpdateItemJsonAdapter),
adapter(::KotshiScanJsonAdapter),
adapter(::KotshiTransactGetItemsJsonAdapter),
adapter(::KotshiTransactWriteItemsJsonAdapter),
adapter(::KotshiUpdateItemJsonAdapter),

// Batch
adapter(::KotshiBatchGetItemJsonAdapter),
Expand Down Expand Up @@ -169,6 +172,7 @@ object DynamoDbJsonAdapterFactory : AwsJsonAdapterFactory(
adapter(::KotshiReqWriteItemJsonAdapter),
adapter(::KotshiReqStatementJsonAdapter),
adapter(::KotshiRestoreSummaryJsonAdapter),
adapter(::KotshiScanResponseJsonAdapter),
adapter(::KotshiSSEDescriptionJsonAdapter),
adapter(::KotshiSSESpecificationJsonAdapter),
adapter(::KotshiStatementResponseJsonAdapter),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ data class BatchGetItem(

@JsonSerializable
data class ReqGetItem internal constructor(
val Keys: List<NamesToValues>,
val Keys: List<ItemAttributes>,
val ProjectionExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
val ConsistentRead: Boolean? = null
) {
companion object {
fun Get(
Keys: List<NamesToValues>,
Keys: List<ItemAttributes>,
ProjectionExpression: String? = null,
ExpressionAttributeNames: TokensToNames? = null,
ConsistentRead: Boolean? = null
Expand All @@ -32,6 +32,6 @@ data class ReqGetItem internal constructor(
@JsonSerializable
data class BatchGetItems(
val ConsumedCapacity: List<ConsumedCapacity>?,
val Responses: Map<String, List<NamesToValues>>?,
val Responses: Map<String, List<ItemAttributes>>?,
val UnprocessedItems: Map<String, ReqGetItem>?
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ data class BatchWriteItem(

@JsonSerializable
data class ReqWriteItem internal constructor(
val DeleteRequest: Map<String, NamesToValues>? = null,
val PutRequest: Map<String, NamesToValues>? = null
val DeleteRequest: Map<String, ItemAttributes>? = null,
val PutRequest: Map<String, ItemAttributes>? = null
) {
companion object {
fun Delete(Key: NamesToValues) = ReqWriteItem(DeleteRequest = mapOf("Key" to Key))
fun Put(Item: NamesToValues) = ReqWriteItem(PutRequest = mapOf("Item" to Item))
fun Delete(Key: ItemAttributes) = ReqWriteItem(DeleteRequest = mapOf("Key" to Key))
fun Put(Item: ItemAttributes) = ReqWriteItem(PutRequest = mapOf("Item" to Item))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class DeleteItem(
val TableName: TableName,
val Key: NamesToValues,
val Key: ItemAttributes,
val ConditionExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
val ExpressionAttributeValues: TokensToValues? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class GetItem(
val TableName: TableName,
val Key: NamesToValues,
val Key: ItemAttributes,
val ProjectionExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
val ConsistentRead: Boolean? = null,
Expand All @@ -21,5 +21,5 @@ data class GetResponse(
val ConsumedCapacity: ConsumedCapacity?,
internal val Item: ItemResult?
) {
val item = Item?.toItem() ?: emptyMap()
val item = Item?.toItem()
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class PutItem(
val TableName: TableName,
val Item: ItemAttributes,
val ConditionExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
val ExpressionAttributeValues: TokensToValues? = null,
val Item: NamesToValues? = null,
val ReturnConsumedCapacity: ReturnConsumedCapacity? = null,
val ReturnItemCollectionMetrics: ReturnItemCollectionMetrics? = null,
val ReturnValues: ReturnValues? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ data class Query(
val ExpressionAttributeValues: TokensToValues? = null,
val Select: Select? = null,
val ConsistentRead: Boolean? = null,
val ExclusiveStartKey: NamesToValues? = null,
val ExclusiveStartKey: ItemAttributes? = null,
val Limit: Int? = null,
val ReturnConsumedCapacity: ReturnConsumedCapacity? = null,
val ScanIndexForward: Boolean? = null,
Expand All @@ -29,7 +29,7 @@ data class QueryResponse(
internal val Items: List<ItemResult>?,
val ConsumedCapacity: ConsumedCapacity?,
val Count: Int?,
val LastEvaluatedKey: NamesToValues?,
val LastEvaluatedKey: ItemAttributes?,
val ScannedCount: Int?
) {
val items = Items?.map(ItemResult::toItem) ?: emptyList()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.http4k.connect.amazon.dynamodb.action

import org.http4k.connect.Http4kConnectAction
import org.http4k.connect.amazon.dynamodb.DynamoDbMoshi
import org.http4k.connect.amazon.model.TableName
import se.ansman.kotshi.JsonSerializable

@Http4kConnectAction
@JsonSerializable
data class Scan(
val TableName: TableName,
val FilterExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
val ExpressionAttributeValues: TokensToValues? = null,
val ExclusiveStartKey: ItemAttributes? = null,
val IndexName: String? = null,
val Limit: Int? = null,
val ConsistentRead: Boolean? = null,
val ProjectionExpression: String? = null,
val Segment: Int? = null,
val Select: String? = null,
val TotalSegments: Int? = null,
val ReturnConsumedCapacity: String? = null,
): DynamoDbAction<ScanResponse>(ScanResponse::class, DynamoDbMoshi)

@JsonSerializable
data class ScanResponse(
val ConsumedCapacity: ConsumedCapacity?,
val Count: Int?,
val LastEvaluatedKey: ItemAttributes?,
val ScannedCount: Int?,
internal val Items: List<ItemResult>?
) {
val items = Items?.map(ItemResult::toItem) ?: emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ data class TransactGetItems(
@JsonSerializable
data class Get(
val TableName: TableName,
val Key: NamesToValues,
val Key: ItemAttributes,
val ProjectionExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null
)
Expand All @@ -25,7 +25,7 @@ data class TransactGetItem internal constructor(val Get: Map<String, Any?>) {
companion object {
fun Get(
TableName: TableName,
Key: NamesToValues,
Key: ItemAttributes,
ProjectionExpression: String? = null,
ExpressionAttributeNames: TokensToNames? = null
) = TransactGetItem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data class TransactWriteItem internal constructor(
companion object {
fun ConditionCheck(
TableName: TableName,
Key: NamesToValues,
Key: ItemAttributes,
ConditionExpression: String,
ExpressionAttributeNames: TokensToNames? = null,
ExpressionAttributeValues: TokensToValues? = null,
Expand All @@ -48,7 +48,7 @@ data class TransactWriteItem internal constructor(

fun Delete(
TableName: TableName,
Key: NamesToValues,
Key: ItemAttributes,
ConditionExpression: String? = null,
ExpressionAttributeNames: TokensToNames? = null,
ExpressionAttributeValues: TokensToValues? = null,
Expand All @@ -66,7 +66,7 @@ data class TransactWriteItem internal constructor(

fun Put(
TableName: TableName,
Item: NamesToValues,
Item: ItemAttributes,
ConditionExpression: String? = null,
ExpressionAttributeNames: TokensToNames? = null,
ExpressionAttributeValues: TokensToValues? = null,
Expand All @@ -84,7 +84,7 @@ data class TransactWriteItem internal constructor(

fun Update(
TableName: TableName,
Key: NamesToValues,
Key: ItemAttributes,
UpdateExpression: String,
ConditionExpression: String? = null,
ExpressionAttributeNames: TokensToNames? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class UpdateItem(
val TableName: TableName,
val Key: NamesToValues,
val Key: ItemAttributes,
val ConditionExpression: String? = null,
val UpdateExpression: String? = null,
val ExpressionAttributeNames: TokensToNames? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import se.ansman.kotshi.JsonSerializable
import java.math.BigDecimal

typealias TokensToNames = Map<String, AttributeName>
typealias NamesToValues = Map<AttributeName, AttributeValue>
typealias ItemAttributes = Map<AttributeName, AttributeValue>
typealias TokensToValues = Map<String, AttributeValue>
typealias ItemResult = Map<String, Map<String, Any>>

Expand All @@ -43,7 +43,7 @@ data class AttributeValue internal constructor(
val BOOL: Boolean? = null,
val BS: Set<Base64Blob>? = null,
val L: List<AttributeValue>? = null,
val M: NamesToValues? = null,
val M: ItemAttributes? = null,
val N: String? = null,
val NS: Set<String>? = null,
val NULL: Boolean? = null,
Expand All @@ -55,7 +55,7 @@ data class AttributeValue internal constructor(
fun Bool(value: Boolean?) = value?.let { AttributeValue(BOOL = it) } ?: Null()
fun Base64Set(value: Set<Base64Blob>?) = value?.let { AttributeValue(BS = it) } ?: Null()
fun List(value: List<AttributeValue>?) = value?.let { AttributeValue(L = it) } ?: Null()
fun Map(value: NamesToValues?) = value?.let { AttributeValue(M = it) } ?: Null()
fun Map(value: ItemAttributes?) = value?.let { AttributeValue(M = it) } ?: Null()
fun Num(value: Number?) = value?.let { AttributeValue(N = it.toString()) } ?: Null()
fun NumSet(value: Set<Number>?) = value?.let { AttributeValue(NS = it.map { it.toString() }.toSet()) } ?: Null()
fun Null() = AttributeValue(NULL = true)
Expand Down Expand Up @@ -86,7 +86,7 @@ data class AttributeValue internal constructor(

@JsonSerializable
data class ItemCollectionMetrics(
val ItemCollectionKey: NamesToValues?,
val ItemCollectionKey: ItemAttributes?,
val SizeEstimateRangeGB: List<Long>?
)

Expand Down
Loading

0 comments on commit 46baa63

Please sign in to comment.