app/data/Recipes.scala (143 lines of code) (raw):

package data import software.amazon.awssdk.services.dynamodb.model._ import models.Recipe.DbModel import models._ import org.joda.time.DateTime import org.scanamo.DynamoReadError import org.scanamo.generic.auto.genericDerivedFormat import org.scanamo.syntax._ import scala.jdk.CollectionConverters._ object Recipes { import Dynamo._ import DynamoFormats._ def list()(implicit dynamo: Dynamo): Iterable[Recipe] = filteredList(_ => true) def filteredList( p: DbModel => Boolean )(implicit dynamo: Dynamo): Iterable[Recipe] = { val dbModels = table.scan().exec().collect { case Right(dbModel) => dbModel } for { dbModel <- dbModels if p(dbModel) baseImage <- BaseImages.findById(dbModel.baseImageId) } yield { Recipe.db2domain(dbModel, baseImage) } } def recipesWithErrors()(implicit dynamo: Dynamo ): (List[DynamoReadError], List[Recipe]) = { val dbResponse = table.scan().exec() val errors = dbResponse.collect { case Left(error) => error } val models = dbResponse.collect { case Right(recipe) => recipe } val recipes = for { dbModel <- models baseImage <- BaseImages.findById(dbModel.baseImageId) } yield { Recipe.db2domain(dbModel, baseImage) } (errors, recipes) } def create( id: RecipeId, description: Option[String], baseImage: BaseImage, diskSize: Option[Int], roles: List[CustomisedRole], createdBy: String, bakeSchedule: Option[BakeSchedule], encryptedCopies: List[AccountNumber] )(implicit dynamo: Dynamo): Recipe = { val now = DateTime.now() val recipe = Recipe( id, description, baseImage, diskSize, roles, createdBy, createdAt = now, modifiedBy = createdBy, modifiedAt = now, bakeSchedule, encryptedCopies ) table.put(Recipe.domain2db(recipe, nextBuildNumber = 0)).exec() recipe } def update( recipe: Recipe, description: Option[String], baseImage: BaseImage, diskSize: Option[Int], roles: List[CustomisedRole], modifiedBy: String, bakeSchedule: Option[BakeSchedule], encryptFor: List[AccountNumber] )(implicit dynamo: Dynamo): Either[DynamoReadError, Recipe] = { val baseUpdateExpr = set("baseImageId", baseImage.id) and set("roles", roles) and set("modifiedBy", modifiedBy) and set("modifiedAt", DateTime.now()) and (if (bakeSchedule.isDefined) set("bakeSchedule", bakeSchedule) else remove("bakeSchedule")) and (if (encryptFor.nonEmpty) set("encryptFor", encryptFor) else remove("encryptFor")) and (if (diskSize.isDefined) set("diskSize", diskSize) else remove("diskSize")) val updateExpr = description match { case Some(desc) => baseUpdateExpr and set("description", desc) case None => baseUpdateExpr } val update = table.update("id" === recipe.id, updateExpr) update.exec().map(Recipe.db2domain(_, baseImage)) } def delete(recipe: Recipe)(implicit dynamo: Dynamo): Unit = { table.delete("id" === recipe.id.value).exec() } def findById(id: RecipeId)(implicit dynamo: Dynamo): Option[Recipe] = { val dbModel: Option[DbModel] = table.get("id" === id).exec().flatMap { attempt => attempt match { case Right(dbModel) => Some(dbModel) case Left(_) => None } } for { dbModel <- dbModel baseImage <- BaseImages.findById(dbModel.baseImageId) } yield { Recipe.db2domain(dbModel, baseImage) } } def findByBaseImage(imageId: BaseImageId)(implicit dynamo: Dynamo ): Iterable[Recipe] = filteredList(_.baseImageId == imageId) def incrementAndGetBuildNumber( id: RecipeId )(implicit dynamo: Dynamo): Option[Int] = { val updateRequest = UpdateItemRequest .builder() .tableName(table.name) .key(Map("id" -> AttributeValue.builder.s(id.value).build()).asJava) .updateExpression("ADD nextBuildNumber :val1") .conditionExpression( "attribute_exists(id)" ) // to ensure the recipe exists in Dynamo .expressionAttributeValues( Map(":val1" -> AttributeValue.builder().n("1").build()).asJava ) .returnValues( ReturnValue.UPDATED_NEW ) // TODO Scanamo doesn't support this .build val updateResult = dynamo.client.updateItem(updateRequest) if (updateResult.attributes.containsKey("nextBuildNumber")) Some(updateResult.attributes.get("nextBuildNumber").n.toInt) else None } private def table(implicit dynamo: Dynamo) = dynamo.Tables.recipes.table }