app/story_packages/services/Database.scala (163 lines of code) (raw):

package story_packages.services import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration import com.amazonaws.services.dynamodbv2.{AmazonDynamoDBClient, AmazonDynamoDBClientBuilder} import com.amazonaws.services.dynamodbv2.document._ import com.amazonaws.services.dynamodbv2.document.spec.{ScanSpec, UpdateItemSpec} import com.amazonaws.services.dynamodbv2.document.utils.ValueMap import com.amazonaws.services.dynamodbv2.model.ReturnValue import com.gu.pandomainauth.model.User import story_packages.metrics.StoryPackagesMetrics import story_packages.model.StoryPackage import org.joda.time.{DateTime, DateTimeZone} import conf.ApplicationConfiguration import story_packages.updates.ReindexPage import story_packages.util.Identity._ import scala.jdk.CollectionConverters._ import scala.concurrent.Future import scala.util.{Failure, Success, Try} class InvalidQueryResult(msg: String) extends Throwable(msg) class Database(config: ApplicationConfiguration) extends Logging { private lazy val client = AmazonDynamoDBClientBuilder.standard .withCredentials(config.aws.mandatoryCredentials) .withEndpointConfiguration(new EndpointConfiguration(config.aws.endpoints.dynamoDB, config.aws.region)) .build private lazy val table = new DynamoDB(client).getTable(config.storage.configTable) def createStoryPackage(story: StoryPackage, user: User): Future[StoryPackage] = { val errorMessage = "Exception in dynamoDB putItem while creating a story package" WithExceptionHandling(errorMessage, { val item = DynamoToScala.convertToItem(story.copy( lastModify = Some(new DateTime().withZone(DateTimeZone.UTC).toString), created = Some(new DateTime().withZone(DateTimeZone.UTC).toString), lastModifyBy = Some(user.email), lastModifyByName = Some(user.fullName), createdBy = Some(user.email) )) table.putItem(item) val newStoryPackage = DynamoToScala.convertToStoryPackage(item) Logger.info(s"New story package created with id:${newStoryPackage.id} -> $newStoryPackage") newStoryPackage }) } def getPackage(id: String): Future[StoryPackage] = { val errorMessage = s"Unable to find story package with id $id" WithExceptionHandling(errorMessage, { val item = table.getItem("id", id) StoryPackagesMetrics.QueryCount.increment() DynamoToScala.convertToStoryPackage(item) }) } def scanAllPackages(isHidden: Boolean = false): Future[ReindexPage] = { val errorMessage = s"Exception in fetching all packages" WithExceptionHandling(errorMessage, { val values = new ValueMap() .withBoolean(":is_hidden", isHidden) val scanRequest = new ScanSpec() .withFilterExpression("isHidden = :is_hidden") .withValueMap(values) .withProjectionExpression("id,deleted,packageName") val outcome = table.scan(scanRequest) StoryPackagesMetrics.ScanCount.increment() val listIds = DynamoToScala.convertToListOfStoryPackages(outcome) val totalCount = math.max(listIds.size, outcome.getAccumulatedItemCount) ReindexPage( totalCount = totalCount, list = listIds, next = None, isHidden = isHidden ) }) } def removePackage(id: String): Future[StoryPackage] = { val errorMessage = s"Unable to delete story package $id" WithExceptionHandling(errorMessage, { val updateSpec = new UpdateItemSpec() .withPrimaryKey("id", id) .addAttributeUpdate(new AttributeUpdate("deleted").put(true)) .withReturnValues(ReturnValue.ALL_NEW) val outcome = table.updateItem(updateSpec) StoryPackagesMetrics.DeleteCount.increment() DynamoToScala.convertToStoryPackage(outcome.getItem) }) } def touchPackage(id: String, user: User, newName: Option[String] = None): Future[StoryPackage] = { val errorMessage = s"Unable to update modification metadata for story package $id" WithExceptionHandling(errorMessage, { val modifyDate = new DateTime().withZone(DateTimeZone.UTC) val updateSpec = new UpdateItemSpec() .withPrimaryKey("id", id) .addAttributeUpdate(new AttributeUpdate("lastModify").put(modifyDate.toString)) .addAttributeUpdate(new AttributeUpdate("lastModifyBy").put(user.email)) .addAttributeUpdate(new AttributeUpdate("lastModifyByName").put(user.fullName)) .withReturnValues(ReturnValue.ALL_NEW) if (newName.nonEmpty) { updateSpec.addAttributeUpdate(new AttributeUpdate("packageName").put(newName.get)) } val outcome = table.updateItem(updateSpec) StoryPackagesMetrics.UpdateCount.increment() DynamoToScala.convertToStoryPackage(outcome.getItem) }) } } private object WithExceptionHandling extends Logging { def apply[T](errorMessage: String, block: => T): Future[T] = { Try(block) match { case Success(result) => Future.successful(result) case Failure(t: Throwable) => Logger.error(errorMessage, t) StoryPackagesMetrics.ErrorCount.increment() Future.failed(t)}} } object DynamoToScala { implicit class RichItem(val item: Item) extends AnyVal { def withOptString(key: String, value: Option[String]) = { value.fold(item)(v => item.withString(key, v)) } } implicit val codec: DynamoCodec[StoryPackage] = new DynamoCodec[StoryPackage] { override def toItem(story: StoryPackage): Item = { lazy val now = new DateTime().withZone(DateTimeZone.UTC) new Item() .withPrimaryKey("id", story.id.getOrElse(IdGeneration.nextId)) .withOptString("packageName", story.name) .withOptString("searchName", story.name.map(_.toLowerCase)) .withBoolean("isHidden", story.isHidden.getOrElse(true)) .withString("lastModify", story.lastModify.getOrElse(now.toString)) .withOptString("lastModifyBy", story.lastModifyBy) .withOptString("lastModifyByName", story.lastModifyByName) .withOptString("createdBy", story.createdBy) .withOptString("created", story.created) .withBoolean("deleted", story.deleted.getOrElse(false)) } override def fromItem(item: Item): StoryPackage = { StoryPackage( id = Option(item.getString("id")), name = Option(item.getString("packageName")), isHidden = Option(if (item.hasAttribute("isHidden")) item.getBOOL("isHidden") else false), lastModify = Option(item.getString("lastModify")), lastModifyBy = Option(item.getString("lastModifyBy")), lastModifyByName = Option(item.getString("lastModifyByName")), createdBy = Option(item.getString("createdBy")), created = Option(item.getString("created")), deleted = if (item.hasAttribute("deleted")) Option(item.getBOOL("deleted")) else None ) } } def convertToStoryPackage(item: Item): StoryPackage = { deserialize[StoryPackage](item) } def convertToItem(story: StoryPackage): Item = { serialize(story) } def convertToListOfStoryPackages(collection: ItemCollection[ScanOutcome]): List[StoryPackage] = { val iterator = collection.iterator().asScala iterator.map(convertToStoryPackage).toList } private def serialize[T: DynamoCodec](t: T): Item = implicitly[DynamoCodec[T]].toItem(t) private def deserialize[T: DynamoCodec](item: Item): T = implicitly[DynamoCodec[T]].fromItem(item) } trait DynamoCodec[T] { def toItem(t: T): Item def fromItem(item: Item): T }