app/models/ProjectMetadata.scala (121 lines of code) (raw):

package models import play.api.Logger import slick.jdbc.PostgresProfile.api._ import slick.lifted.TableQuery import play.api.libs.functional.syntax._ import play.api.libs.json._ import scala.concurrent.Future import scala.util.{Failure, Success, Try} import scala.concurrent.ExecutionContext.Implicits.global case class ProjectMetadata (id:Option[Int],projectRef:Int,key:String,value:Option[String]){ val logger = Logger(getClass) def save(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[ProjectMetadata]] = id match { case None=> logger.debug("Inserting new record") val insertQuery = TableQuery[ProjectMetadataRow] returning TableQuery[ProjectMetadataRow].map(_.id) into ((item,id)=>item.copy(id=Some(id))) db.run( (insertQuery+=this).asTry ).map({ case Success(insertResult)=>Success(insertResult) case Failure(error)=>Failure(error) }) case Some(realEntityId)=> logger.debug("Updating record") db.run( TableQuery[ProjectMetadataRow].filter(_.id===realEntityId).update(this).asTry ).map({ case Success(rowsAffected)=>Success(this) case Failure(error)=>Failure(error) }) } } object ProjectMetadata extends ((Option[Int], Int, String, Option[String])=>ProjectMetadata){ private val logger = Logger(getClass) val ASSET_FOLDER_KEY = "created_asset_folder" /** * Gets a metadata entry for the given project * @param projectRef projectEntry ID * @param key metadata key. * @param db implicitly provided database object * @return a Future, containing an Option which has the key if necessary. Future will fail if a db error occurs */ def entryFor(projectRef:Int, key:String)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Option[ProjectMetadata]] = db.run( //unique constraint on the table ensures that there can only be zero or one responses TableQuery[ProjectMetadataRow].filter(_.projectRef===projectRef).filter(_.key===key).result ).map(_.headOption) def getOrCreate(projectRef:Int, key:String, newValue:Option[String], autoSave:Boolean=false)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[ProjectMetadata]] = db.run( //unique constraint on the table ensures that there can only be zero or one responses TableQuery[ProjectMetadataRow].filter(_.projectRef===projectRef).filter(_.key===key).result ).map(_.headOption).flatMap({ case Some(entry)=>Future(Success(entry.copy(value=newValue))) case None=> val newEntry = ProjectMetadata(None, projectRef, key, newValue) if(autoSave){ newEntry.save } else { Future(Success(newEntry)) } }) def deleteFor(projectRef: Int, key:String)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Int]] = { db.run( TableQuery[ProjectMetadataRow].filter(_.projectRef===projectRef).filter(_.key===key).delete.asTry ) } /** * Gets all metadata entries for the given project * @param projectRef projectEntry ID * @param db implicitly provided database object * @return a Future, containing a Try containing a Sequence of ProjectMetadata objects */ def allMetadataFor(projectRef:Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Seq[ProjectMetadata]]] = db.run( TableQuery[ProjectMetadataRow].filter(_.projectRef===projectRef).result.asTry ) def deleteAllMetadataFor(projectRef:Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Int]] = db.run( TableQuery[ProjectMetadataRow].filter(_.projectRef===projectRef).delete.asTry ) /** * Set (by upsert) entries in bulk * @param projectRef project ID to set entries for * @param data a Map[String,String] containing keys and values to set * @param db implicitly provided database object * @return a Future, containing an Int indicating the number of insert/updates. The future will fail if there is a database error */ def setBulk(projectRef:Int, data:Map[String,String])(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[Int]] = { def tryInsertWithRecovery(mdEntry:ProjectMetadata,onRetry:Boolean=false):Future[ProjectMetadata] = mdEntry.save.flatMap({ case Failure(err)=> val errorString = err.toString if (errorString.contains("violates foreign key constraint") || errorString.contains("Referential integrity constraint violation") || errorString.contains("violates unique constraint")) { ProjectMetadata.deleteFor(mdEntry.projectRef, mdEntry.key).flatMap({ case Failure(err)=> logger.error("Could not delete old metadata value", err) throw err case Success(rows)=> if (onRetry) throw err else tryInsertWithRecovery(mdEntry, onRetry = true) }) } else { throw err } case Success(savedEntry)=>Future(savedEntry) }) val createdObjects = Future.sequence(data.map(kvTuple=>ProjectMetadata.getOrCreate(projectRef,kvTuple._1,Some(kvTuple._2)))) val splitResultsFuture = createdObjects.map(_.partition(_.isSuccess)) splitResultsFuture.flatMap(resultTuple=>{ if(resultTuple._2.count(x=>true)>0){ val resultSeq = resultTuple._2.foldLeft("")((acc, failedTry)=>acc + failedTry.failed.get.toString).mkString("; ") Future(Failure(new RuntimeException(resultSeq)) ) //fixme: define a custom exception to hold the sequence instead } else { Future.sequence(resultTuple._1.map(successfulTry=>tryInsertWithRecovery(successfulTry.get))) .map(iterable=>Success(iterable.count(x=>true))) .recover({ case ex:Throwable=>Failure(ex) }) } }) } } class ProjectMetadataRow(tag:Tag) extends Table[ProjectMetadata](tag,"ProjectMetadata"){ def id=column[Int]("id",O.PrimaryKey,O.AutoInc) def projectRef=column[Int]("k_project_entry") def key=column[String]("s_key") def value=column[String]("s_value") def * = (id.?,projectRef,key,value.?) <> (ProjectMetadata.tupled, ProjectMetadata.unapply) } trait ProjectMetadataSerializer { implicit val projectMetadataWrites:Writes[ProjectMetadata] = ( (JsPath \ "id").writeNullable[Int] and (JsPath \ "projectEntryRef").write[Int] and (JsPath \ "key").write[String] and (JsPath \ "value").writeNullable[String] )(unlift(ProjectMetadata.unapply)) implicit val projectMetadataReads:Reads[ProjectMetadata] = ( (JsPath \ "id").readNullable[Int] and (JsPath \ "projectEntryRef").read[Int] and (JsPath \ "key").read[String] and (JsPath \ "value").readNullable[String] )(ProjectMetadata.apply _) }