Skip to content

Migration Guide from Tempest v1 to Tempest v2

This guide will explain some items that need to be changed when upgrading from Tempest 1 to Tempest 2

Dependencies

The first change is to swap the dependency from v1 to v2.

Depenencies.kt

- val tempest = "app.cash.tempest:tempest:{dependencies.tempestVersion}"
+ val tempest2 = "app.cash.tempest:tempest2:{dependencies.tempestVersion}"

build.gradle.kts

- implementation(Dependencies.tempest)
+ implementation(Dependencies.tempest2)

Import Changes

Many of the classes and objects imported from a tempest or aws package will likely be found by just adding a 2 to the import path.

- import app.cash.tempest.BeginsWith
+ import app.cash.tempest2.BeginsWith

Though some of your imports may have moved into Amazons new package structure

- import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException
+ import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException

Logical DB Upgrades

One of the largest changes will be to your LogicalDB and the class used as your LogicalTable<T> type.

TableName Annotation

In Tempest v1 your table would likely have been annotated with an @DynamoDBTable annotation. This is no longer on the table definition class, but has been moved to an annotation on the member variable inside the LogicalDb interface.

Additionally, that annotation needs to be replaced with an DynamoDbBean annotation.

Old

interface DyDatabase : LogicalDb {
  val table: DyTable
}

interface DyTable : LogicalTable<DyItem> {
  // View and Index member variables
}

@DynamoDBTable(tableName = TABLE_NAME)
class DyItem {
  // Attribute Definitions
}

New

interface DyDatabase : LogicalDb {
  @TableName(TABLE_NAME)
  val table: DyTable
}

interface DyTable : LogicalTable<DyItem> {
  // View and Index member variables
}

@DynamoDbBean
class DyItem {
  // Attribute Definitions
}

Hash Key Annotation

@DynamoDBHashKey has been replaced by @get:DynamoDbPartitionKey

- @DynamoDBHashKey
+ @get:DynamoDbPartitionKey
  var partition_key: String? = null

Range Key Annotation

@DynamoDBRangeKey has been replaced by @get:DynamoDbSortKey

- @DynamoDBRangeKey
+ @get:DynamoDbSortKey
  var sort_key: String? = null

DynamoDBAttribute Annotation

The @DynamoDBAttribute is no longer needed on class member variables

- @DynamoDBAttribute
  var description: String? = null

Index on Hash Keys

@DynamoDBIndexHashKey has been replaced by @get:DynamoDbSecondaryPartitionKey

- @DynamoDBIndexHashKey(globalSecondaryIndexName = ENTITY_TYPE_INDEX)
+ @get:DynamoDbSecondaryPartitionKey(indexNames = [ENTITY_TYPE_INDEX])
  var gsi_pk: String? = null

Index on Range Keys

@DynamoDBIndexRangeKey has been replaced by @get:DynamoDbSecondarySortKey

- @DynamoDBIndexRangeKey(globalSecondaryIndexName = ENTITY_TYPE_INDEX)
+ @get:DynamoDbSecondarySortKey(indexNames = [ENTITY_TYPE_INDEX])
  var gsi_sk: String? = null

Version Attribute

@DynamoDBVersionAttribute has been replaced by @get:DynamoDbVersionAttribute

- @DynamoDBVersionAttribute
+ @get:DynamoDbVersionAttribute
  var version: Long? = null

Type Conversion Annotation

@DynamoDBTypeConverted has been replaced by @get:DynamoDbConvertedBy

- @DynamoDBTypeConverted(converter = InstantTypeConverter::class)
+ @get:DynamoDbConvertedBy(InstantAttributeConverter::class)
  var expires_at: Instant? = null

Type Conversion Interface

The DynamoDBTypeConverter<DBType, Mine> interface has been replaced by an AttributeConverter<Mine> interface.

Instead of having two methods * fun convert(mine: Mine): DBType * fun unconvert(dbType: DbType): Mine

There are now four methods * fun transformFrom(mine: Mine): AttributeValue * essentially the same as convert * fun transformTo(input: AttributeValue): Mine * essentiall the same as unconvert * fun type(): EnhancedType<Mine> * Allows the Enhanced Dynamo SDK to avoid Type Erasure * fun attributeValueType(): AttributeValueType * Tells the SDK which value to expect from transformFrom


Included below is an example of the transformation from a DynamoDBTypeConverter<Long, Instant> to AttributeConverter<Instant>

Old

internal class InstantTypeEpochConverter : DynamoDBTypeConverter<Long, Instant> {
  override fun unconvert(epochSeconds: Long): Instant {
    return Instant.ofEpochSecond(epochSeconds)
  }

  override fun convert(instant: Instant): Long {
    return instant.epochSecond
  }
}

New

internal class InstantTypeEpochConverter : AttributeConverter<Instant> {
  override fun transformFrom(input: Instant): AttributeValue {
    val timeLongAsString = input.epochSecond.toString()
    return AttributeValue.builder()
      .n(timeLongAsString)
      .build()
  }

  override fun transformTo(input: AttributeValue): Instant {
    val timeLong = input.n().toLong()
    return Instant.ofEpochSecond(timeLong)
  }

  override fun type(): EnhancedType<Instant> {
    return EnhancedType.of(Instant::class.java)
  }

  override fun attributeValueType(): AttributeValueType {
    return AttributeValueType.N
  }
}

Creating your LogicalDb

Previously you could create your LogicalDb object using an AmazonDynamoDB object. However, the new LogicalDb objects require DynamoDbEnhancedClient objects, which themselves can be created using the new base DynamoDbClient object.

New Creation Semantics

fun createClient(
  awsRegionName: String, 
  awsCredentialsProvider: AwsCredentialsProvider,
) : DynamoDbClient = 
  DynamoDbClient.builder()
    .region(Region.of(awsRegionName))
    .credentialsProvider(awsCredentialsProvider)
    .build()

fun createEnhancedClient(dynamoDbClient: DynamoDbClient): DynamoDbEnhancedClient =
  DynamoDbEnhancedClient.builder()
    .dynamoDbClient(dynamoDbClient)
    .build()

fun createLogicalDb(dynamoDbEnhancedClient: DynamoDbEnhancedClient): DyDatabase =
  LogicalDb<DyDatabase>(dynamoDbEnhancedClient)

API Type changes

Consistent Read Enum

When doing a load from an InlineView object in Tempest 1 the consistent read was specified via an enum. In Tempest 2 the option is now a boolean specifying to use consistent reads or not.

val key = ...
val entity = dyDatabase.dyTable.dyEntitiy.load(
  key = key,
- consistentReads = DynamoDBMapperConfig.ConsistentReads.CONSISTENT
+ consistentReads = true
)

Changes to Misk

If you are using Misk you will need to update your dependencies and DI configuration

Dependencies

-val miskAwsDynamodb = "com.squareup.misk:misk-aws-dynamodb:VERSION"
-val miskAwsDynamodbTesting = "com.squareup.misk:misk-aws-dynamodb-testing:VERSION"
+val miskAws2Dynamodb = "com.squareup.misk:misk-aws2-dynamodb:VERSION"
+val miskAws2DynamodbTesting = "com.squareup.misk:misk-aws2-dynamodb-testing:VERSION"

DynamoDbModule

Changes to the configuration of the RealDynamoDbModule are due to changes in the AWS SDK v2 for the Dynamo Client. An example of the changes is below

-import misk.dynamodb.RealDynamoDbModule
-import com.amazonaws.ClientConfiguration
-install(
-  RealDynamoDbModule(
-    ClientConfiguration()
-      .withMaxErrorRetry(DYNAMO_CLIENT_MAX_ERROR_RETRIES)
-      // Set a timeout per retry.
-      .withRequestTimeout(DYNAMO_REQUEST_TIMEOUT_MILLIS)
-      .withRetryPolicy(
-        PredefinedRetryPolicies
-          .getDynamoDBDefaultRetryPolicyWithCustomMaxRetries(DYNAMO_CLIENT_MAX_ERROR_RETRIES)
-      ),
-    DyItem::class
-  )
-)
+import misk.aws2.dynamodb.RealDynamoDbModule
+import misk.aws2.dynamodb.RequiredDynamoDbTable
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
+install(
+  RealDynamoDbModule(
+    ClientOverrideConfiguration.builder()
+      .retryPolicy(
+        RetryPolicy.defaultRetryPolicy().copy {
+          it.numRetries(
+            DYNAMO_CLIENT_MAX_ERROR_RETRIES
+          )
+        }
+      )
+      .apiCallAttemptTimeout(Duration.ofMillis(DYNAMO_REQUEST_TIMEOUT_MILLIS.toLong()))
+      .apiCallTimeout(Duration.ofMillis(DYNAMO_CLIENT_EXECUTION_TIMEOUT_MILLIS.toLong()))
+      .build(),
+    listOf(
+      Constants.DyTable,
+    ).map { RequiredDynamoDbTable(it) }
+  )
+)