app/services/DynamoBannerDesigns.scala (212 lines of code) (raw):
package services
import com.typesafe.scalalogging.StrictLogging
import io.circe.generic.auto._
import io.circe.syntax._
import models.DynamoErrors._
import models._
import models.BannerDesign
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model._
import utils.Circe.{dynamoMapToJson, jsonToDynamo}
import zio.blocking.effectBlocking
import zio.{ZEnv, ZIO}
import java.time.OffsetDateTime
import scala.jdk.CollectionConverters._
class DynamoBannerDesigns(stage: String, client: DynamoDbClient)
extends DynamoService(stage, client)
with StrictLogging {
protected val tableName = s"support-admin-console-banner-designs-$stage"
private def buildKey(
designName: String): java.util.Map[String, AttributeValue] =
Map(
"name" -> AttributeValue.builder.s(designName).build
).asJava
/**
* Attempts to retrieve a banner design from dynamodb. Fails if the banner design does not exist.
*/
private def get(designName: String)
: ZIO[ZEnv, DynamoGetError, java.util.Map[String, AttributeValue]] =
effectBlocking {
val query = QueryRequest.builder
.tableName(tableName)
.keyConditionExpression("#name = :name")
.expressionAttributeValues(
Map(
":name" -> AttributeValue.builder.s(designName).build
).asJava
)
.expressionAttributeNames(Map("#name" -> "name").asJava) // name is a reserved word in dynamodb
.build()
client
.query(query)
.items
.asScala
.headOption
}.flatMap {
case Some(item) => ZIO.succeed(item)
case None =>
ZIO.fail(
DynamoGetError(
new Exception(s"Banner design does not exist: $designName")))
}
.mapError(error => DynamoGetError(error))
private def getAll()
: ZIO[ZEnv,
DynamoGetError,
java.util.List[java.util.Map[String, AttributeValue]]] =
effectBlocking {
client
.scan(
ScanRequest
.builder()
.tableName(tableName)
.build()
)
.items()
}.mapError(DynamoGetError)
private def update(
updateRequest: UpdateItemRequest): ZIO[ZEnv, DynamoError, Unit] =
effectBlocking {
val result = client.updateItem(updateRequest)
logger.info(s"UpdateItemResponse: $result")
()
}.mapError {
case err: ConditionalCheckFailedException => DynamoNoLockError(err)
case other => DynamoPutError(other)
}
private def delete(deleteRequest: DeleteItemRequest): ZIO[ZEnv, DynamoError, Unit] =
effectBlocking {
val result = client.deleteItem(deleteRequest)
logger.info(s"DeleteItemResponse: $result")
()
}.mapError {
case err: ConditionalCheckFailedException => DynamoNoLockError(err)
case other => DynamoPutError(other)
}
def getBannerDesign(
designName: String): ZIO[ZEnv, DynamoGetError, BannerDesign] =
get(designName)
.map(item => dynamoMapToJson(item).as[BannerDesign])
.flatMap {
case Right(bannerDesign) => ZIO.succeed(bannerDesign)
case Left(error) => ZIO.fail(DynamoGetError(error))
}
def getAllBannerDesigns(): ZIO[ZEnv, DynamoGetError, List[BannerDesign]] =
getAll().map(
results =>
results.asScala
.map(item => dynamoMapToJson(item).as[BannerDesign])
.flatMap {
case Right(bannerDesign) => Some(bannerDesign)
case Left(error) =>
logger.error(
s"Failed to decode item from Dynamo: ${error.getMessage}")
None
}
.toList
)
/**
* With an UpdateItem request we have to provide an expression to specify each attribute to be updated.
* We do this by iterating over the attributes in `item` and building an expression
*/
private def buildUpdateBannerDesignExpression(
item: Map[String, AttributeValue]): String = {
val subExprs = item.foldLeft[List[String]](Nil) {
case (acc, (key, value)) =>
s"$key = :$key" +: acc
}
s"set ${subExprs.mkString(", ")} remove lockStatus" // Unlock the banner design at the same time
}
def createBannerDesign(
bannerDesign: BannerDesign): ZIO[ZEnv, DynamoError, Unit] = {
val item = jsonToDynamo(bannerDesign.asJson).m()
val request = PutItemRequest.builder
.tableName(tableName)
.item(item)
// Do not overwrite if already in dynamo
.conditionExpression("attribute_not_exists(#name)")
.expressionAttributeNames(Map("#name" -> "name").asJava)
.build()
put(request)
}
def updateBannerDesign(bannerDesign: BannerDesign,
email: String): ZIO[ZEnv, DynamoError, Unit] = {
val item = jsonToDynamo(bannerDesign.asJson).m().asScala.toMap -
"lockStatus" - // Unlock by removing lockStatus
"name" - // key field"
"status" // status updates happen separately
val updateExpression = buildUpdateBannerDesignExpression(item)
val attributeValues = item.map { case (key, value) => s":$key" -> value }
// Add email, for the conditional update
val attributeValuesWithEmail = attributeValues + (":email" -> AttributeValue.builder
.s(email)
.build)
val updateRequest = UpdateItemRequest.builder
.tableName(tableName)
.key(buildKey(bannerDesign.name))
.updateExpression(updateExpression)
.expressionAttributeValues(attributeValuesWithEmail.asJava)
.conditionExpression("lockStatus.email = :email")
.build()
update(updateRequest)
}
def lockBannerDesign(designName: String,
email: String,
force: Boolean): ZIO[ZEnv, DynamoError, Unit] = {
val lockStatus = LockStatus(
locked = true,
email = Some(email),
timestamp = Some(OffsetDateTime.now())
)
val request = {
val builder = UpdateItemRequest.builder
.tableName(tableName)
.key(buildKey(designName))
.updateExpression("set lockStatus = :lockStatus")
.expressionAttributeValues(
Map(
":lockStatus" -> jsonToDynamo(lockStatus.asJson)
).asJava)
if (!force) {
// Check it isn't already locked
builder.conditionExpression("attribute_not_exists(lockStatus.email)")
}
builder.build()
}
update(request)
}
// Removes the lockStatus attribute if the user currently has it locked
def unlockBannerDesign(designName: String,
email: String): ZIO[ZEnv, DynamoError, Unit] = {
val request = UpdateItemRequest.builder
.tableName(tableName)
.key(buildKey(designName))
.updateExpression("remove lockStatus")
.conditionExpression("lockStatus.email = :email")
.expressionAttributeValues(Map(
":email" -> AttributeValue.builder.s(email).build
).asJava)
.build()
update(request)
}
def deleteBannerDesign(designName: String): ZIO[ZEnv, DynamoError, Unit] = {
val request = DeleteItemRequest.builder
.tableName(tableName)
.key(buildKey(designName))
.build()
delete(request)
}
def updateStatus(
designName: String,
status: BannerDesignStatus
): ZIO[ZEnv, DynamoError, Unit] = {
val updateRequest = UpdateItemRequest.builder
.tableName(tableName)
.key(buildKey(designName))
.updateExpression("SET #status = :status")
.expressionAttributeValues(
Map(
":status" -> jsonToDynamo(status.asJson),
":name" -> AttributeValue.builder.s(designName).build
).asJava)
.expressionAttributeNames(Map(
"#status" -> "status",
"#name" -> "name"
).asJava)
.conditionExpression("#name = :name") // only update if it already exists in the table
.build()
update(updateRequest)
}
// Does not decode the Dynamodb data
def getRawDesign(designName: String): ZIO[ZEnv, DynamoGetError,java.util.Map[String, AttributeValue]] = {
get(designName)
}
}