diff --git a/designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala new file mode 100644 index 00000000000..2be4afa5d7d --- /dev/null +++ b/designer/server/src/main/scala/db/migration/V1_056__CreateScenarioActivitiesDefinition.scala @@ -0,0 +1,134 @@ +package db.migration + +import com.typesafe.scalalogging.LazyLogging +import db.migration.V1_056__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions +import pl.touk.nussknacker.ui.db.migration.SlickMigration +import shapeless.syntax.std.tuple._ +import slick.jdbc.JdbcProfile +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.sql.Timestamp +import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global + +trait V1_056__CreateScenarioActivitiesDefinition extends SlickMigration with LazyLogging { + + import profile.api._ + + private val definitions = new ScenarioActivitiesDefinitions(profile) + + override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { + logger.info("Starting migration V1_056__CreateScenarioActivitiesDefinition") + for { + _ <- definitions.scenarioActivitiesTable.schema.create + _ <- + sqlu"""ALTER TABLE "scenario_activities" ADD CONSTRAINT scenario_id_fk FOREIGN KEY ("scenario_id") REFERENCES "processes" ("id") ON DELETE CASCADE;""" + } yield logger.info("Execution finished for migration V1_056__CreateScenarioActivitiesDefinition") + } + +} + +object V1_056__CreateScenarioActivitiesDefinition { + + class ScenarioActivitiesDefinitions(val profile: JdbcProfile) { + import profile.api._ + + val scenarioActivitiesTable = TableQuery[ScenarioActivityEntity] + + class ScenarioActivityEntity(tag: Tag) extends Table[ScenarioActivityEntityData](tag, "scenario_activities") { + + def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) + + def activityType: Rep[String] = column[String]("activity_type", NotNull) + + def scenarioId: Rep[Long] = column[Long]("scenario_id", NotNull) + + def activityId: Rep[UUID] = column[UUID]("activity_id", NotNull, O.Unique) + + def userId: Rep[Option[String]] = column[Option[String]]("user_id") + + def userName: Rep[String] = column[String]("user_name", NotNull) + + def impersonatedByUserId: Rep[Option[String]] = column[Option[String]]("impersonated_by_user_id") + + def impersonatedByUserName: Rep[Option[String]] = column[Option[String]]("impersonated_by_user_name") + + def lastModifiedByUserName: Rep[Option[String]] = column[Option[String]]("last_modified_by_user_name") + + def lastModifiedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("last_modified_at") + + def createdAt: Rep[Timestamp] = column[Timestamp]("created_at", NotNull) + + def scenarioVersion: Rep[Option[Long]] = column[Option[Long]]("scenario_version") + + def comment: Rep[Option[String]] = column[Option[String]]("comment") + + def attachmentId: Rep[Option[Long]] = column[Option[Long]]("attachment_id") + + def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") + + def state: Rep[Option[String]] = column[Option[String]]("state") + + def errorMessage: Rep[Option[String]] = column[Option[String]]("error_message") + + def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") + + def additionalProperties: Rep[String] = column[String]("additional_properties", NotNull) + + def activityTypeIndex = index("activity_type_idx", activityType) + def createdAtIndex = index("created_at_idx", activityType) + def scenarioIdIndex = index("scenario_id_idx", activityType) + + def tupleWithoutAutoIncId = ( + activityType, + scenarioId, + activityId, + userId, + userName, + impersonatedByUserId, + impersonatedByUserName, + lastModifiedByUserName, + lastModifiedAt, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) + + override def * = + (id :: tupleWithoutAutoIncId.productElements).tupled <> ( + ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply + ) + + } + + } + + final case class ScenarioActivityEntityData( + id: Long, + activityType: String, + scenarioId: Long, + activityId: UUID, + userId: Option[String], + userName: String, + impersonatedByUserId: Option[String], + impersonatedByUserName: Option[String], + lastModifiedByUserName: Option[String], + lastModifiedAt: Option[Timestamp], + createdAt: Timestamp, + scenarioVersion: Option[Long], + comment: Option[String], + attachmentId: Option[Long], + finishedAt: Option[Timestamp], + state: Option[String], + errorMessage: Option[String], + buildInfo: Option[String], + additionalProperties: String, + ) + +} diff --git a/designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala b/designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala new file mode 100644 index 00000000000..bdbaa614db8 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.scala @@ -0,0 +1,268 @@ +package db.migration + +import com.typesafe.scalalogging.LazyLogging +import db.migration.V1_056__CreateScenarioActivitiesDefinition.ScenarioActivitiesDefinitions +import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition.Migration +import pl.touk.nussknacker.ui.db.entity.{ScenarioActivityEntityFactory, ScenarioActivityType} +import pl.touk.nussknacker.ui.db.migration.SlickMigration +import slick.jdbc.JdbcProfile +import slick.lifted.{ProvenShape, TableQuery => LTableQuery} +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.sql.Timestamp +import java.util.UUID +import scala.concurrent.ExecutionContext.Implicits.global + +trait V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends SlickMigration with LazyLogging { + + import profile.api._ + + override def migrateActions: DBIOAction[Any, NoStream, Effect.All] = { + new Migration(profile).migrate + } + +} + +object V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition extends LazyLogging { + + class Migration(val profile: JdbcProfile) extends ScenarioActivityEntityFactory { + + import profile.api._ + + private val scenarioActivitiesDefinitions = new ScenarioActivitiesDefinitions(profile) + private val processActionsDefinitions = new ProcessActionsDefinitions(profile) + private val commentsDefinitions = new CommentsDefinitions(profile) + + // Migrate old actions, with their corresponding comments + + def migrate: DBIOAction[Unit, NoStream, Effect.All] = for { + _ <- migrateActions + _ <- migrateComments + } yield () + + private def migrateActions: DBIOAction[Int, NoStream, Effect.All] = { + val insertQuery = + processActionsDefinitions.table + .joinLeft(commentsDefinitions.table) + .on(_.commentId === _.id) + .map { case (processAction, maybeComment) => + ( + activityType(processAction.actionName), // activityType - converted from action name + processAction.processId, // scenarioId + processAction.id, // activityId + None: Option[String], // userId - always absent in old actions + processAction.user, // userName + processAction.impersonatedByIdentity, // impersonatedByUserId + processAction.impersonatedByUsername, // impersonatedByUserName + processAction.user.?, // lastModifiedByUserName + processAction.createdAt.?, // lastModifiedAt + processAction.createdAt, // createdAt + processAction.processVersionId, // scenarioVersion + maybeComment.map(_.content).map(removeCommentPrefix), // comment + None: Option[Long], // attachmentId + processAction.performedAt, // finishedAt + processAction.state.?, // state + processAction.failureMessage, // errorMessage + processAction.buildInfo, // buildInfo + "{}" // additionalProperties always empty in old actions + ) + } + + // Slick generates single "insert from select" query and operation is performed solely on db + scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tupleWithoutAutoIncId).forceInsertQuery(insertQuery) + } + + // Migrate old comments, that were standalone, not assigned to actions + def migrateComments: DBIOAction[Int, NoStream, Effect.All] = { + val insertQuery = + commentsDefinitions.table + .joinLeft(processActionsDefinitions.table) + .on(_.id === _.commentId) + .filter { case (_, action) => action.isEmpty } + .map(_._1) + .map { comment => + ( + ScenarioActivityType.CommentAdded.entryName, // activityType - converted from action name + comment.processId, // scenarioId + UUID.randomUUID(), // activityId + None: Option[String], // userId - always absent in old actions + comment.user, // userName + comment.impersonatedByIdentity, // impersonatedByUserId + comment.impersonatedByUsername, // impersonatedByUserName + comment.user.?, // lastModifiedByUserName + comment.createDate.?, // lastModifiedAt + comment.createDate, // createdAt + comment.processVersionId.?, // scenarioVersion + comment.content.?, // comment + None: Option[Long], // attachmentId + comment.createDate.?, // finishedAt + None: Option[String], // state + None: Option[String], // errorMessage + None: Option[String], // buildInfo - always absent in old actions + "{}" // additionalProperties always empty in old actions + ) + } + + // Slick generates single "insert from select" query and operation is performed solely on db + scenarioActivitiesDefinitions.scenarioActivitiesTable.map(_.tupleWithoutAutoIncId).forceInsertQuery(insertQuery) + } + + private def activityType(actionNameRep: Rep[String]): Rep[String] = { + val customActionPrefix = s"CUSTOM_ACTION_[" + val customActionSuffix = "]" + Case + .If(actionNameRep === "DEPLOY") + .Then(ScenarioActivityType.ScenarioDeployed.entryName) + .If(actionNameRep === "CANCEL") + .Then(ScenarioActivityType.ScenarioCanceled.entryName) + .If(actionNameRep === "ARCHIVE") + .Then(ScenarioActivityType.ScenarioArchived.entryName) + .If(actionNameRep === "UNARCHIVE") + .Then(ScenarioActivityType.ScenarioUnarchived.entryName) + .If(actionNameRep === "PAUSE") + .Then(ScenarioActivityType.ScenarioPaused.entryName) + .If(actionNameRep === "RENAME") + .Then(ScenarioActivityType.ScenarioNameChanged.entryName) + .If(actionNameRep === "run now") + .Then(ScenarioActivityType.PerformedSingleExecution.entryName) + .Else(actionNameRep.reverseString.++(customActionPrefix.reverse).reverseString.++(customActionSuffix)) + } + + private def removeCommentPrefix(commentRep: Rep[String]): Rep[String] = { + val prefixDeploymentComment = "Deployment: " + val prefixCanceledComment = "Stop: " + val prefixRunNowComment = "Run now: " + Case + .If(commentRep.startsWith(prefixDeploymentComment)) + .Then(commentRep.substring(LiteralColumn(prefixDeploymentComment.length))) + .If(commentRep.startsWith(prefixCanceledComment)) + .Then(commentRep.substring(LiteralColumn(prefixCanceledComment.length))) + .If(commentRep.startsWith(prefixRunNowComment)) + .Then(commentRep.substring(LiteralColumn(prefixRunNowComment.length))) + .Else(commentRep) + } + + } + + class ProcessActionsDefinitions(val profile: JdbcProfile) { + import profile.api._ + + val table: LTableQuery[ProcessActionEntity] = LTableQuery(new ProcessActionEntity(_)) + + class ProcessActionEntity(tag: Tag) extends Table[ProcessActionEntityData](tag, "process_actions") { + def id: Rep[UUID] = column[UUID]("id", O.PrimaryKey) + + def processId: Rep[Long] = column[Long]("process_id") + + def processVersionId: Rep[Option[Long]] = column[Option[Long]]("process_version_id") + + def createdAt: Rep[Timestamp] = column[Timestamp]("created_at") + + def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") + + def user: Rep[String] = column[String]("user") + + def impersonatedByIdentity = column[Option[String]]("impersonated_by_identity") + + def impersonatedByUsername = column[Option[String]]("impersonated_by_username") + + def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") + + def actionName: Rep[String] = column[String]("action_name") + + def state: Rep[String] = column[String]("state") + + def failureMessage: Rep[Option[String]] = column[Option[String]]("failure_message") + + def commentId: Rep[Option[Long]] = column[Option[Long]]("comment_id") + + def * : ProvenShape[ProcessActionEntityData] = ( + id, + processId, + processVersionId, + user, + impersonatedByIdentity, + impersonatedByUsername, + createdAt, + performedAt, + actionName, + state, + failureMessage, + commentId, + buildInfo + ) <> ( + ProcessActionEntityData.apply _ tupled, ProcessActionEntityData.unapply + ) + + } + + } + + sealed case class ProcessActionEntityData( + id: UUID, + processId: Long, + processVersionId: Option[Long], + user: String, + impersonatedByIdentity: Option[String], + impersonatedByUsername: Option[String], + createdAt: Timestamp, + performedAt: Option[Timestamp], + actionName: String, + state: String, + failureMessage: Option[String], + commentId: Option[Long], + buildInfo: Option[String] + ) + + class CommentsDefinitions(val profile: JdbcProfile) { + import profile.api._ + val table: LTableQuery[CommentEntity] = LTableQuery(new CommentEntity(_)) + + class CommentEntity(tag: Tag) extends Table[CommentEntityData](tag, "process_comments") { + + def id: Rep[Long] = column[Long]("id", O.PrimaryKey) + + def processId: Rep[Long] = column[Long]("process_id", NotNull) + + def processVersionId: Rep[Long] = column[Long]("process_version_id", NotNull) + + def content: Rep[String] = column[String]("content", NotNull) + + def createDate: Rep[Timestamp] = column[Timestamp]("create_date", NotNull) + + def user: Rep[String] = column[String]("user", NotNull) + + def impersonatedByIdentity = column[Option[String]]("impersonated_by_identity") + + def impersonatedByUsername = column[Option[String]]("impersonated_by_username") + + override def * = + ( + id, + processId, + processVersionId, + content, + user, + impersonatedByIdentity, + impersonatedByUsername, + createDate + ) <> ( + CommentEntityData.apply _ tupled, CommentEntityData.unapply + ) + + } + + } + + final case class CommentEntityData( + id: Long, + processId: Long, + processVersionId: Long, + content: String, + user: String, + impersonatedByIdentity: Option[String], + impersonatedByUsername: Option[String], + createDate: Timestamp, + ) + +} diff --git a/designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala new file mode 100644 index 00000000000..25b0616f091 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/hsql/V1_056__CreateScenarioActivities.scala @@ -0,0 +1,8 @@ +package db.migration.hsql + +import db.migration.V1_056__CreateScenarioActivitiesDefinition +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +class V1_056__CreateScenarioActivities extends V1_056__CreateScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = HsqldbProfile +} diff --git a/designer/server/src/main/scala/db/migration/hsql/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/hsql/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..454d045a0a1 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/hsql/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,9 @@ +package db.migration.hsql + +import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +class V1_057__MigrateActionsAndCommentsToScenarioActivities + extends V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = HsqldbProfile +} diff --git a/designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala new file mode 100644 index 00000000000..00be93e0e77 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/postgres/V1_056__CreateScenarioActivities.scala @@ -0,0 +1,8 @@ +package db.migration.postgres + +import db.migration.V1_056__CreateScenarioActivitiesDefinition +import slick.jdbc.{JdbcProfile, PostgresProfile} + +class V1_056__CreateScenarioActivities extends V1_056__CreateScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = PostgresProfile +} diff --git a/designer/server/src/main/scala/db/migration/postgres/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/main/scala/db/migration/postgres/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..f8f47612d15 --- /dev/null +++ b/designer/server/src/main/scala/db/migration/postgres/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,9 @@ +package db.migration.postgres + +import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition +import slick.jdbc.{JdbcProfile, PostgresProfile} + +class V1_057__MigrateActionsAndCommentsToScenarioActivities + extends V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition { + override protected lazy val profile: JdbcProfile = PostgresProfile +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala index 71f8927d2b2..63f684bc3d4 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/BaseHttpService.scala @@ -20,9 +20,13 @@ abstract class BaseHttpService( private val allServerEndpoints = new AtomicReference(List.empty[NoRequirementServerEndpoint]) protected def expose(serverEndpoint: NoRequirementServerEndpoint): Unit = { + expose(List(serverEndpoint)) + } + + protected def expose(serverEndpoints: List[NoRequirementServerEndpoint]): Unit = { allServerEndpoints .accumulateAndGet( - List(serverEndpoint), + serverEndpoints, (l1, l2) => l1 ::: l2 ) } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ProcessesExportResources.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ProcessesExportResources.scala index bd88d3e38a4..deef86d81ef 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ProcessesExportResources.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ProcessesExportResources.scala @@ -8,15 +8,16 @@ import io.circe.syntax._ import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.process.{ProcessName, ProcessingType} import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess -import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps._ +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity +import pl.touk.nussknacker.ui.api.utils.ScenarioDetailsOps.ScenarioWithDetailsOps import pl.touk.nussknacker.ui.process.ProcessService import pl.touk.nussknacker.ui.process.label.ScenarioLabel import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.ProcessActivity +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.process.repository.{ + DBIOActionRunner, FetchingProcessRepository, - ProcessActivityRepository, ScenarioWithDetailsEntity } import pl.touk.nussknacker.ui.security.api.LoggedUser @@ -28,8 +29,9 @@ import scala.concurrent.{ExecutionContext, Future} class ProcessesExportResources( processRepository: FetchingProcessRepository[Future], protected val processService: ProcessService, - processActivityRepository: ProcessActivityRepository, - processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _] + scenarioActivityRepository: ScenarioActivityRepository, + processResolvers: ProcessingTypeDataProvider[UIProcessResolver, _], + dbioActionRunner: DBIOActionRunner, )(implicit val ec: ExecutionContext) extends Directives with FailFastCirceSupport @@ -74,7 +76,9 @@ class ProcessesExportResources( entity(as[String]) { svg => complete { processRepository.fetchProcessDetailsForId[ScenarioGraph](processId.id, versionId).flatMap { process => - processActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) + dbioActionRunner.run { + scenarioActivityRepository.findActivity(processId.id).map(exportProcessToPdf(svg, process, _)) + } } } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala index 062c88c1f41..c9299d272e1 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpService.scala @@ -2,17 +2,25 @@ package pl.touk.nussknacker.ui.api import cats.data.EitherT import com.typesafe.scalalogging.LazyLogging +import pl.touk.nussknacker.engine.api.deployment.{ + ScenarioActivity, + ScenarioActivityId, + ScenarioAttachment, + ScenarioComment +} import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName} import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.security.Permission.Permission import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ + NoActivity, NoComment, NoPermission, NoScenario } import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ -import pl.touk.nussknacker.ui.api.description.scenarioActivity.Endpoints -import pl.touk.nussknacker.ui.process.repository.{ProcessActivityRepository, UserComment} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.{Dtos, Endpoints} +import pl.touk.nussknacker.ui.process.repository.DBIOActionRunner +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioAttachmentService} import pl.touk.nussknacker.ui.security.api.{AuthManager, LoggedUser} import pl.touk.nussknacker.ui.server.HeadersSupport.ContentDisposition @@ -25,11 +33,12 @@ import scala.concurrent.{ExecutionContext, Future} class ScenarioActivityApiHttpService( authManager: AuthManager, - scenarioActivityRepository: ProcessActivityRepository, + scenarioActivityRepository: ScenarioActivityRepository, scenarioService: ProcessService, scenarioAuthorizer: AuthorizeProcess, attachmentService: ScenarioAttachmentService, streamEndpointProvider: TapirStreamEndpointProvider, + dbioActionRunner: DBIOActionRunner, )(implicit executionContext: ExecutionContext) extends BaseHttpService(authManager) with LazyLogging { @@ -45,27 +54,8 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(scenarioName) _ <- isAuthorized(scenarioId, Permission.Read) - processActivity <- EitherT.right(scenarioActivityRepository.findActivity(scenarioId)) - } yield Legacy.ProcessActivity( - comments = processActivity.comments.map { comment => - Legacy.Comment( - id = comment.id, - processVersionId = comment.processVersionId.value, - content = comment.content, - user = comment.user, - createDate = comment.createDate - ) - }, - attachments = processActivity.attachments.map { attachment => - Legacy.Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId.value, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDate - ) - } - ) + processActivity <- fetchProcessActivity(scenarioId) + } yield processActivity } } @@ -88,7 +78,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- deleteComment(request) + _ <- deleteComment(request, scenarioId) } yield () } } @@ -125,7 +115,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(scenarioName) _ <- isAuthorized(scenarioId, Permission.Read) - activities <- notImplemented[List[ScenarioActivity]] + activities <- fetchActivities(scenarioId) } yield ScenarioActivities(activities) } } @@ -137,7 +127,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(processName) _ <- isAuthorized(scenarioId, Permission.Read) - attachments <- notImplemented[ScenarioAttachments] + attachments <- fetchAttachments(scenarioId) } yield attachments } } @@ -161,7 +151,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- notImplemented[Unit] + _ <- addNewComment(request, scenarioId) } yield () } } @@ -173,7 +163,7 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- notImplemented[Unit] + _ <- editComment(request, scenarioId) } yield () } } @@ -185,14 +175,11 @@ class ScenarioActivityApiHttpService( for { scenarioId <- getScenarioIdByName(request.scenarioName) _ <- isAuthorized(scenarioId, Permission.Write) - _ <- notImplemented[Unit] + _ <- deleteComment(request, scenarioId) } yield () } } - private def notImplemented[T]: EitherT[Future, ScenarioActivityError, T] = - EitherT.leftT[Future, T](ScenarioActivityError.NotImplemented: ScenarioActivityError) - private def getScenarioIdByName(scenarioName: ProcessName) = { EitherT.fromOptionF( scenarioService.getProcessId(scenarioName), @@ -212,17 +199,316 @@ class ScenarioActivityApiHttpService( } ) + private def fetchProcessActivity( + scenarioId: ProcessId + ): EitherT[Future, ScenarioActivityError, Legacy.ProcessActivity] = + EitherT + .right( + dbioActionRunner.run( + scenarioActivityRepository.findActivity(scenarioId) + ) + ) + + private def fetchActivities( + scenarioId: ProcessId + ): EitherT[Future, ScenarioActivityError, List[Dtos.ScenarioActivity]] = + EitherT + .right( + dbioActionRunner.run( + scenarioActivityRepository.findActivities(scenarioId) + ) + ) + .map(_.map(toDto).toList) + + private def toDto(scenarioComment: ScenarioComment): Dtos.ScenarioActivityComment = { + scenarioComment match { + case ScenarioComment.Available(comment, lastModifiedByUserName, lastModifiedAt) => + Dtos.ScenarioActivityComment( + status = Dtos.ScenarioActivityCommentStatus.Available(comment), + lastModifiedBy = lastModifiedByUserName.value, + lastModifiedAt = lastModifiedAt, + ) + case ScenarioComment.Deleted(deletedByUserName, deletedAt) => + Dtos.ScenarioActivityComment( + status = Dtos.ScenarioActivityCommentStatus.Deleted, + lastModifiedBy = deletedByUserName.value, + lastModifiedAt = deletedAt, + ) + } + } + + private def toDto(attachment: ScenarioAttachment): Dtos.ScenarioActivityAttachment = { + attachment match { + case ScenarioAttachment.Available(attachmentId, attachmentFilename, lastModifiedByUserName, lastModifiedAt) => + Dtos.ScenarioActivityAttachment( + status = Dtos.ScenarioActivityAttachmentStatus.Available(attachmentId.value), + filename = attachmentFilename.value, + lastModifiedBy = lastModifiedByUserName.value, + lastModifiedAt = lastModifiedAt, + ) + case ScenarioAttachment.Deleted(attachmentFilename, deletedByUserName, deletedAt) => + Dtos.ScenarioActivityAttachment( + status = Dtos.ScenarioActivityAttachmentStatus.Deleted, + filename = attachmentFilename.value, + lastModifiedBy = deletedByUserName.value, + lastModifiedAt = deletedAt, + ) + } + } + + private def toDto(scenarioActivity: ScenarioActivity): Dtos.ScenarioActivity = { + scenarioActivity match { + case ScenarioActivity.ScenarioCreated(_, scenarioActivityId, user, date, scenarioVersion) => + Dtos.ScenarioActivity.ScenarioCreated( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value) + ) + case ScenarioActivity.ScenarioArchived(_, scenarioActivityId, user, date, scenarioVersion) => + Dtos.ScenarioActivity.ScenarioArchived( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value) + ) + case ScenarioActivity.ScenarioUnarchived(_, scenarioActivityId, user, date, scenarioVersion) => + Dtos.ScenarioActivity.ScenarioUnarchived( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value) + ) + case ScenarioActivity.ScenarioDeployed(_, scenarioActivityId, user, date, scenarioVersion, comment) => + Dtos.ScenarioActivity.ScenarioDeployed( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + ) + case ScenarioActivity.ScenarioPaused(_, scenarioActivityId, user, date, scenarioVersion, comment) => + Dtos.ScenarioActivity.ScenarioPaused( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + ) + case ScenarioActivity.ScenarioCanceled(_, scenarioActivityId, user, date, scenarioVersion, comment) => + Dtos.ScenarioActivity.ScenarioCanceled( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + ) + case ScenarioActivity.ScenarioModified(_, scenarioActivityId, user, date, scenarioVersion, comment) => + Dtos.ScenarioActivity.ScenarioModified( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + ) + case ScenarioActivity.ScenarioNameChanged(_, id, user, date, version, oldName, newName) => + Dtos.ScenarioActivity.ScenarioNameChanged( + id = id.value, + user = user.name.value, + date = date, + scenarioVersion = version.map(_.value), + oldName = oldName, + newName = newName, + ) + case ScenarioActivity.CommentAdded(_, scenarioActivityId, user, date, scenarioVersion, comment) => + Dtos.ScenarioActivity.CommentAdded( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + ) + case ScenarioActivity.AttachmentAdded(_, scenarioActivityId, user, date, scenarioVersion, attachment) => + Dtos.ScenarioActivity.AttachmentAdded( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + attachment = toDto(attachment), + ) + case ScenarioActivity.ChangedProcessingMode(_, scenarioActivityId, user, date, scenarioVersion, from, to) => + Dtos.ScenarioActivity.ChangedProcessingMode( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + from = from.entryName, + to = to.entryName + ) + case ScenarioActivity.IncomingMigration( + _, + scenarioActivityId, + user, + date, + scenarioVersion, + sourceEnvironment, + sourceScenarioVersion + ) => + Dtos.ScenarioActivity.IncomingMigration( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + sourceEnvironment = sourceEnvironment.name, + sourceScenarioVersion = sourceScenarioVersion.value.toString, + ) + case ScenarioActivity.OutgoingMigration( + _, + scenarioActivityId, + user, + date, + scenarioVersion, + comment, + destinationEnvironment + ) => + Dtos.ScenarioActivity.OutgoingMigration( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + destinationEnvironment = destinationEnvironment.name, + ) + case ScenarioActivity.PerformedSingleExecution( + _, + scenarioActivityId, + user, + date, + scenarioVersion, + comment, + dateFinished, + errorMessage + ) => + Dtos.ScenarioActivity.PerformedSingleExecution( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + comment = toDto(comment), + dateFinished = dateFinished, + errorMessage = errorMessage, + ) + case ScenarioActivity.PerformedScheduledExecution( + _, + scenarioActivityId, + user, + date, + scenarioVersion, + dateFinished, + errorMessage + ) => + Dtos.ScenarioActivity.PerformedScheduledExecution( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + dateFinished = dateFinished, + errorMessage = errorMessage, + ) + case ScenarioActivity.AutomaticUpdate( + _, + scenarioActivityId, + user, + date, + scenarioVersion, + dateFinished, + changes, + errorMessage + ) => + Dtos.ScenarioActivity.AutomaticUpdate( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + dateFinished = dateFinished, + changes = changes, + errorMessage = errorMessage, + ) + case ScenarioActivity.CustomAction(_, scenarioActivityId, user, date, scenarioVersion, actionName, comment) => + Dtos.ScenarioActivity.CustomAction( + id = scenarioActivityId.value, + user = user.name.value, + date = date, + scenarioVersion = scenarioVersion.map(_.value), + actionName = actionName, + comment = toDto(comment), + ) + } + } + private def addNewComment(request: AddCommentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser - ): EitherT[Future, ScenarioActivityError, Unit] = + ): EitherT[Future, ScenarioActivityError, ScenarioActivityId] = EitherT.right( - scenarioActivityRepository.addComment(scenarioId, request.versionId, UserComment(request.commentContent)) + dbioActionRunner.run( + scenarioActivityRepository.addComment(scenarioId, request.versionId, request.commentContent) + ) ) - private def deleteComment(request: DeprecatedDeleteCommentRequest): EitherT[Future, ScenarioActivityError, Unit] = + private def editComment(request: DeprecatedEditCommentRequest, scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = + EitherT( + dbioActionRunner.run( + scenarioActivityRepository.editComment(scenarioId, request.commentId, request.commentContent) + ) + ).leftMap(_ => NoComment(request.commentId)) + + private def editComment(request: EditCommentRequest, scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = + EitherT( + dbioActionRunner.run( + scenarioActivityRepository.editComment( + scenarioId, + ScenarioActivityId(request.scenarioActivityId), + request.commentContent + ) + ) + ).leftMap(_ => NoActivity(request.scenarioActivityId)) + + private def deleteComment(request: DeprecatedDeleteCommentRequest, scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = EitherT( - scenarioActivityRepository.deleteComment(request.commentId) - ).leftMap(_ => NoComment(request.commentId.toString)) + dbioActionRunner.run(scenarioActivityRepository.deleteComment(scenarioId, request.commentId)) + ).leftMap(_ => NoComment(request.commentId)) + + private def deleteComment(request: DeleteCommentRequest, scenarioId: ProcessId)( + implicit loggedUser: LoggedUser + ): EitherT[Future, ScenarioActivityError, Unit] = + EitherT( + dbioActionRunner.run( + scenarioActivityRepository.deleteComment(scenarioId, ScenarioActivityId(request.scenarioActivityId)) + ) + ).leftMap(_ => NoActivity(request.scenarioActivityId)) + + private def fetchAttachments(scenarioId: ProcessId): EitherT[Future, ScenarioActivityError, ScenarioAttachments] = { + EitherT + .right( + dbioActionRunner.run(scenarioActivityRepository.findAttachments(scenarioId)) + ) + .map(_.map { attachmentEntity => + Attachment( + id = attachmentEntity.id, + scenarioVersion = attachmentEntity.processVersionId.value, + fileName = attachmentEntity.fileName, + user = attachmentEntity.user, + createDate = attachmentEntity.createDateTime, + ) + }.toList) + .map(ScenarioAttachments.apply) + } private def saveAttachment(request: AddAttachmentRequest, scenarioId: ProcessId)( implicit loggedUser: LoggedUser diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala index 76e29b20b57..b156254eb5f 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Dtos.scala @@ -431,7 +431,8 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], - dateFinished: Instant, + comment: ScenarioActivityComment, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -440,7 +441,7 @@ object Dtos { user: String, date: Instant, scenarioVersion: Option[Long], - dateFinished: Instant, + dateFinished: Option[Instant], errorMessage: Option[String], ) extends ScenarioActivity @@ -462,6 +463,7 @@ object Dtos { date: Instant, scenarioVersion: Option[Long], actionName: String, + comment: ScenarioActivityComment, ) extends ScenarioActivity } @@ -531,12 +533,10 @@ object Dtos { object ScenarioActivityError { - // todo NU-1772 - remove this error when API is implemented - final case object NotImplemented extends ScenarioActivityError - final case class NoScenario(scenarioName: ProcessName) extends ScenarioActivityError final case object NoPermission extends ScenarioActivityError with CustomAuthorizationError - final case class NoComment(commentId: String) extends ScenarioActivityError + final case class NoActivity(scenarioActivityId: UUID) extends ScenarioActivityError + final case class NoComment(commentId: Long) extends ScenarioActivityError implicit val noScenarioCodec: Codec[String, NoScenario, CodecFormat.TextPlain] = BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoScenario](e => s"No scenario ${e.scenarioName} found") @@ -546,6 +546,11 @@ object Dtos { s"Unable to delete comment with id: ${e.commentId}" ) + implicit val noActivityCodec: Codec[String, NoActivity, CodecFormat.TextPlain] = + BaseEndpointDefinitions.toTextPlainCodecSerializationOnly[NoActivity](e => + s"Unable to delete comment for activity with id: ${e.scenarioActivityId.toString}" + ) + } object Legacy { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala index a706ebd4dc4..6bd986a7902 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Endpoints.scala @@ -63,8 +63,9 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .out(statusCode(Ok)) .errorOut( oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), - oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + oneOfVariant(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariant(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)), + oneOfVariant(InternalServerError, plainBody[NoActivity].example(Examples.activityNotFoundError)), ) ) .withSecurity(auth) @@ -121,8 +122,9 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .out(statusCode(Ok)) .errorOut( oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), - oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + oneOfVariant(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariant(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)), + oneOfVariant(InternalServerError, plainBody[NoActivity].example(Examples.activityNotFoundError)), ) ) .withSecurity(auth) @@ -139,8 +141,9 @@ class Endpoints(auth: EndpointInput[AuthCredentials], streamProvider: TapirStrea .out(statusCode(Ok)) .errorOut( oneOf[ScenarioActivityError]( - oneOfVariantFromMatchType(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), - oneOfVariantFromMatchType(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)) + oneOfVariant(NotFound, plainBody[NoScenario].example(Examples.noScenarioError)), + oneOfVariant(InternalServerError, plainBody[NoComment].example(Examples.commentNotFoundError)), + oneOfVariant(InternalServerError, plainBody[NoActivity].example(Examples.activityNotFoundError)), ) ) .withSecurity(auth) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala index 8088593b920..ffd86816e91 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/Examples.scala @@ -1,7 +1,11 @@ package pl.touk.nussknacker.ui.api.description.scenarioActivity import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} -import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{NoComment, NoScenario} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.ScenarioActivityError.{ + NoActivity, + NoComment, + NoScenario +} import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos._ import sttp.tapir.EndpointIO.Example @@ -176,7 +180,12 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), - dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + comment = ScenarioActivityComment( + status = ScenarioActivityCommentStatus.Available("Run campaign"), + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + dateFinished = Some(Instant.parse("2024-01-17T14:21:17Z")), errorMessage = Some("Execution error occurred"), ), ScenarioActivity.PerformedSingleExecution( @@ -184,7 +193,12 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), - dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + comment = ScenarioActivityComment( + status = ScenarioActivityCommentStatus.Deleted, + lastModifiedBy = "some user", + lastModifiedAt = Instant.parse("2024-01-17T14:21:17Z") + ), + dateFinished = Some(Instant.parse("2024-01-17T14:21:17Z")), errorMessage = None, ), ScenarioActivity.PerformedScheduledExecution( @@ -192,7 +206,7 @@ object Examples { user = "some user", date = Instant.parse("2024-01-17T14:21:17Z"), scenarioVersion = Some(1), - dateFinished = Instant.parse("2024-01-17T14:21:17Z"), + dateFinished = Some(Instant.parse("2024-01-17T14:21:17Z")), errorMessage = None, ), ScenarioActivity.AutomaticUpdate( @@ -230,7 +244,12 @@ object Examples { val commentNotFoundError: Example[NoComment] = Example.of( summary = Some("Unable to edit comment with id: {commentId}"), - value = NoComment("a76d6eba-9b6c-4d97-aaa1-984a23f88019") + value = NoComment(123L) + ) + + val activityNotFoundError: Example[NoActivity] = Example.of( + summary = Some("Unable to edit comment for activity with id: {commentId}"), + value = NoActivity(UUID.fromString("a76d6eba-9b6c-4d97-aaa1-984a23f88019")) ) } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala index 701f74bab3e..b475bab6015 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/scenarioActivity/InputOutput.scala @@ -21,10 +21,6 @@ object InputOutput { ) ) ), - oneOfVariantFromMatchType( - NotImplemented, - emptyOutputAs(ScenarioActivityError.NotImplemented), - ) ) } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/NuTables.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/NuTables.scala index a2afb3f5c59..034df3f48bc 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/NuTables.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/NuTables.scala @@ -6,10 +6,9 @@ import slick.jdbc.JdbcProfile trait NuTables extends ProcessEntityFactory - with CommentEntityFactory with ProcessVersionEntityFactory with EnvironmentsEntityFactory - with ProcessActionEntityFactory + with ScenarioActivityEntityFactory with ScenarioLabelsEntityFactory with AttachmentEntityFactory with DeploymentEntityFactory { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/CommentEntityFactory.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/CommentEntityFactory.scala deleted file mode 100644 index 589ad1f3946..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/CommentEntityFactory.scala +++ /dev/null @@ -1,56 +0,0 @@ -package pl.touk.nussknacker.ui.db.entity - -import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} -import slick.lifted.{TableQuery => LTableQuery} -import slick.sql.SqlProfile.ColumnOption.NotNull - -import java.sql.Timestamp -import java.time.Instant - -trait CommentEntityFactory extends BaseEntityFactory { - - import profile.api._ - - val commentsTable: LTableQuery[CommentEntityFactory#CommentEntity] = LTableQuery(new CommentEntity(_)) - - class CommentEntity(tag: Tag) extends Table[CommentEntityData](tag, "process_comments") { - - def id: Rep[Long] = column[Long]("id", O.PrimaryKey) - - def processId: Rep[ProcessId] = column[ProcessId]("process_id", NotNull) - - def processVersionId: Rep[VersionId] = column[VersionId]("process_version_id", NotNull) - - def content: Rep[String] = column[String]("content", NotNull) - - def createDate: Rep[Timestamp] = column[Timestamp]("create_date", NotNull) - - def user: Rep[String] = column[String]("user", NotNull) - - def impersonatedByIdentity = column[Option[String]]("impersonated_by_identity") - - // TODO impersonating user's name is added so it's easier to present the name on the fronted. - // Once we have a mechanism for fetching username by user's identity impersonated_by_username column could be deleted from database tables. - def impersonatedByUsername = column[Option[String]]("impersonated_by_username") - - override def * = - (id, processId, processVersionId, content, user, impersonatedByIdentity, impersonatedByUsername, createDate) <> ( - CommentEntityData.apply _ tupled, CommentEntityData.unapply - ) - - } - -} - -final case class CommentEntityData( - id: Long, - processId: ProcessId, - processVersionId: VersionId, - content: String, - user: String, - impersonatedByIdentity: Option[String], - impersonatedByUsername: Option[String], - createDate: Timestamp -) { - val createDateTime: Instant = createDate.toInstant -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ProcessActionEntityFactory.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ProcessActionEntityFactory.scala deleted file mode 100644 index 7b113ca9557..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ProcessActionEntityFactory.scala +++ /dev/null @@ -1,104 +0,0 @@ -package pl.touk.nussknacker.ui.db.entity - -import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState -import pl.touk.nussknacker.engine.api.deployment.{ProcessActionId, ScenarioActionName} -import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} -import slick.lifted.{ForeignKeyQuery, ProvenShape, TableQuery => LTableQuery} - -import java.sql.Timestamp -import java.time.Instant - -trait ProcessActionEntityFactory extends BaseEntityFactory with CommentEntityFactory { - - import profile.api._ - - val processActionsTable: LTableQuery[ProcessActionEntityFactory#ProcessActionEntity] = - LTableQuery(new ProcessActionEntity(_)) - - val processVersionsTable: LTableQuery[ProcessVersionEntityFactory#ProcessVersionEntity] - - class ProcessActionEntity(tag: Tag) extends Table[ProcessActionEntityData](tag, "process_actions") { - def id: Rep[ProcessActionId] = column[ProcessActionId]("id", O.PrimaryKey) - - def processId: Rep[ProcessId] = column[ProcessId]("process_id") - - def processVersionId: Rep[Option[VersionId]] = column[Option[VersionId]]("process_version_id") - - def createdAt: Rep[Timestamp] = column[Timestamp]("created_at") - - def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") - - def user: Rep[String] = column[String]("user") - - def impersonatedByIdentity = column[Option[String]]("impersonated_by_identity") - - // TODO impersonating user's name is added so it's easier to present the name on the fronted. - // Once we have a mechanism for fetching username by user's identity impersonated_by_username column could be deleted from database tables. - def impersonatedByUsername = column[Option[String]]("impersonated_by_username") - - def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") - - def actionName: Rep[ScenarioActionName] = column[ScenarioActionName]("action_name") - - def state: Rep[ProcessActionState] = column[ProcessActionState]("state") - - def failureMessage: Rep[Option[String]] = column[Option[String]]("failure_message") - - def commentId: Rep[Option[Long]] = column[Option[Long]]("comment_id") - - def processes_fk: ForeignKeyQuery[ProcessVersionEntityFactory#ProcessVersionEntity, ProcessVersionEntityData] = - foreignKey("process_actions_version_fk", (processId, processVersionId), processVersionsTable)( - procV => (procV.processId, procV.id ?), - onUpdate = ForeignKeyAction.Cascade, - onDelete = ForeignKeyAction.NoAction - ) - - def comment_fk: ForeignKeyQuery[CommentEntityFactory#CommentEntity, CommentEntityData] = - foreignKey("process_actions_comment_fk", commentId, commentsTable)( - _.id.?, - onUpdate = ForeignKeyAction.Cascade, - onDelete = ForeignKeyAction.SetNull - ) - - def * : ProvenShape[ProcessActionEntityData] = ( - id, - processId, - processVersionId, - user, - impersonatedByIdentity, - impersonatedByUsername, - createdAt, - performedAt, - actionName, - state, - failureMessage, - commentId, - buildInfo - ) <> ( - ProcessActionEntityData.apply _ tupled, ProcessActionEntityData.unapply - ) - - } - -} - -final case class ProcessActionEntityData( - id: ProcessActionId, - processId: ProcessId, - processVersionId: Option[VersionId], - user: String, - impersonatedByIdentity: Option[String], - impersonatedByUsername: Option[String], - createdAt: Timestamp, - performedAt: Option[Timestamp], - actionName: ScenarioActionName, - state: ProcessActionState, - failureMessage: Option[String], - commentId: Option[Long], - buildInfo: Option[String] -) { - - lazy val createdAtTime: Instant = createdAt.toInstant - lazy val performedAtTime: Option[Instant] = performedAt.map(_.toInstant) - lazy val isDeployed: Boolean = actionName == ScenarioActionName.Deploy -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ScenarioActivityEntityFactory.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ScenarioActivityEntityFactory.scala new file mode 100644 index 00000000000..a5b58359ce7 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/db/entity/ScenarioActivityEntityFactory.scala @@ -0,0 +1,200 @@ +package pl.touk.nussknacker.ui.db.entity + +import enumeratum.EnumEntry.UpperSnakecase +import enumeratum._ +import io.circe.Decoder +import io.circe.syntax.EncoderOps +import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState +import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.process.ProcessId +import slick.lifted.{TableQuery => LTableQuery} +import slick.sql.SqlProfile.ColumnOption.NotNull + +import java.sql.Timestamp +import java.util.UUID +import scala.collection.immutable +import scala.util.matching.Regex + +trait ScenarioActivityEntityFactory extends BaseEntityFactory { + + import profile.api._ + + val scenarioActivityTable: LTableQuery[ScenarioActivityEntityFactory#ScenarioActivityEntity] = LTableQuery( + new ScenarioActivityEntity(_) + ) + + class ScenarioActivityEntity(tag: Tag) extends Table[ScenarioActivityEntityData](tag, "scenario_activities") { + + def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) + + def activityType: Rep[ScenarioActivityType] = column[ScenarioActivityType]("activity_type", NotNull) + + def scenarioId: Rep[ProcessId] = column[ProcessId]("scenario_id", NotNull) + + def activityId: Rep[ScenarioActivityId] = column[ScenarioActivityId]("activity_id", NotNull, O.Unique) + + def userId: Rep[Option[String]] = column[Option[String]]("user_id") + + def userName: Rep[String] = column[String]("user_name", NotNull) + + def impersonatedByUserId: Rep[Option[String]] = column[Option[String]]("impersonated_by_user_id") + + def impersonatedByUserName: Rep[Option[String]] = column[Option[String]]("impersonated_by_user_name") + + def lastModifiedByUserName: Rep[Option[String]] = column[Option[String]]("last_modified_by_user_name") + + def lastModifiedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("last_modified_at") + + def createdAt: Rep[Timestamp] = column[Timestamp]("created_at", NotNull) + + def scenarioVersion: Rep[Option[ScenarioVersion]] = column[Option[ScenarioVersion]]("scenario_version") + + def comment: Rep[Option[String]] = column[Option[String]]("comment") + + def attachmentId: Rep[Option[Long]] = column[Option[Long]]("attachment_id") + + def performedAt: Rep[Option[Timestamp]] = column[Option[Timestamp]]("performed_at") + + def state: Rep[Option[ProcessActionState]] = column[Option[ProcessActionState]]("state") + + def errorMessage: Rep[Option[String]] = column[Option[String]]("error_message") + + def buildInfo: Rep[Option[String]] = column[Option[String]]("build_info") + + def additionalProperties: Rep[AdditionalProperties] = column[AdditionalProperties]("additional_properties") + + override def * = + ( + id, + activityType, + scenarioId, + activityId, + userId, + userName, + impersonatedByUserId, + impersonatedByUserName, + lastModifiedByUserName, + lastModifiedAt, + createdAt, + scenarioVersion, + comment, + attachmentId, + performedAt, + state, + errorMessage, + buildInfo, + additionalProperties, + ) <> ( + ScenarioActivityEntityData.apply _ tupled, ScenarioActivityEntityData.unapply + ) + + } + + implicit def scenarioActivityTypeMapper: BaseColumnType[ScenarioActivityType] = + MappedColumnType.base[ScenarioActivityType, String]( + _.entryName, + name => + ScenarioActivityType + .withEntryNameOption(name) + .getOrElse(throw new IllegalArgumentException(s"Invalid ScenarioActivityType $name")) + ) + + implicit def scenarioIdMapper: BaseColumnType[ScenarioId] = + MappedColumnType.base[ScenarioId, Long](_.value, ScenarioId.apply) + + implicit def scenarioActivityIdMapper: BaseColumnType[ScenarioActivityId] = + MappedColumnType.base[ScenarioActivityId, UUID](_.value, ScenarioActivityId.apply) + + implicit def scenarioVersionMapper: BaseColumnType[ScenarioVersion] = + MappedColumnType.base[ScenarioVersion, Long](_.value, ScenarioVersion.apply) + + implicit def additionalPropertiesMapper: BaseColumnType[AdditionalProperties] = + MappedColumnType.base[AdditionalProperties, String]( + _.properties.asJson.noSpaces, + jsonStr => + io.circe.parser.parse(jsonStr).flatMap(Decoder[Map[String, String]].decodeJson) match { + case Right(rawParams) => AdditionalProperties(rawParams) + case Left(error) => throw error + } + ) + +} + +sealed trait ScenarioActivityType extends EnumEntry with UpperSnakecase + +object ScenarioActivityType extends Enum[ScenarioActivityType] { + + case object ScenarioCreated extends ScenarioActivityType + case object ScenarioArchived extends ScenarioActivityType + case object ScenarioUnarchived extends ScenarioActivityType + case object ScenarioDeployed extends ScenarioActivityType + case object ScenarioPaused extends ScenarioActivityType + case object ScenarioCanceled extends ScenarioActivityType + case object ScenarioModified extends ScenarioActivityType + case object ScenarioNameChanged extends ScenarioActivityType + case object CommentAdded extends ScenarioActivityType + case object AttachmentAdded extends ScenarioActivityType + case object ChangedProcessingMode extends ScenarioActivityType + case object IncomingMigration extends ScenarioActivityType + case object OutgoingMigration extends ScenarioActivityType + case object PerformedSingleExecution extends ScenarioActivityType + case object PerformedScheduledExecution extends ScenarioActivityType + case object AutomaticUpdate extends ScenarioActivityType + + final case class CustomAction(value: String) extends ScenarioActivityType { + override def entryName: String = s"CUSTOM_ACTION_[$value]" + } + + object CustomAction { + private val pattern: Regex = """CUSTOM_ACTION_\[(.+?)]""".r + + def withEntryNameOption(entryName: String): Option[CustomAction] = { + entryName match { + case pattern(value) => Some(CustomAction(value)) + case _ => None + } + } + + } + + def withEntryNameOption(entryName: String): Option[ScenarioActivityType] = + withNameOption(entryName) + .orElse(CustomAction.withEntryNameOption(entryName)) + + override def values: immutable.IndexedSeq[ScenarioActivityType] = findValues + +} + +final case class AdditionalProperties(properties: Map[String, String]) { + + def withProperty(key: String, value: String): AdditionalProperties = { + AdditionalProperties(properties ++ Map((key, value))) + } + +} + +object AdditionalProperties { + def empty: AdditionalProperties = AdditionalProperties(Map.empty) +} + +final case class ScenarioActivityEntityData( + id: Long, + activityType: ScenarioActivityType, + scenarioId: ProcessId, + activityId: ScenarioActivityId, + userId: Option[String], + userName: String, + impersonatedByUserId: Option[String], + impersonatedByUserName: Option[String], + lastModifiedByUserName: Option[String], + lastModifiedAt: Option[Timestamp], + createdAt: Timestamp, + scenarioVersion: Option[ScenarioVersion], + comment: Option[String], + attachmentId: Option[Long], + finishedAt: Option[Timestamp], + state: Option[ProcessActionState], + errorMessage: Option[String], + buildInfo: Option[String], + additionalProperties: AdditionalProperties, +) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/initialization/Initialization.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/initialization/Initialization.scala index 520ad354d98..ff31e533475 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/initialization/Initialization.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/initialization/Initialization.scala @@ -5,7 +5,6 @@ import cats.syntax.traverse._ import com.typesafe.scalalogging.LazyLogging import db.util.DBIOActionInstances._ import pl.touk.nussknacker.engine.api.graph.ScenarioGraph -import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.migration.ProcessMigrations import pl.touk.nussknacker.ui.db.entity.EnvironmentsEntityData import pl.touk.nussknacker.ui.db.{DbRef, NuTables} @@ -13,10 +12,12 @@ import pl.touk.nussknacker.ui.process.ScenarioQuery import pl.touk.nussknacker.ui.process.migrate.ProcessModelMigrator import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository._ +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.{LoggedUser, NussknackerInternalUser} import slick.dbio.DBIOAction import slick.jdbc.JdbcProfile +import java.time.Clock import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext} @@ -27,13 +28,14 @@ object Initialization { def init( migrations: ProcessingTypeDataProvider[ProcessMigrations, _], db: DbRef, + clock: Clock, fetchingRepository: DBFetchingProcessRepository[DB], - commentRepository: CommentRepository, + scenarioActivityRepository: ScenarioActivityRepository, scenarioLabelsRepository: ScenarioLabelsRepository, - environment: String + environment: String, )(implicit ec: ExecutionContext): Unit = { val processRepository = - new DBProcessRepository(db, commentRepository, scenarioLabelsRepository, migrations.mapValues(_.version)) + new DBProcessRepository(db, clock, scenarioActivityRepository, scenarioLabelsRepository, migrations.mapValues(_.version)) val operations: List[InitialOperation] = List( new EnvironmentInsert(environment, db), diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/NotificationService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/NotificationService.scala index 44e2a4c1dc0..40e13ac38fa 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/NotificationService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/notifications/NotificationService.scala @@ -1,14 +1,12 @@ package pl.touk.nussknacker.ui.notifications -import db.util.DBIOActionInstances.DB import pl.touk.nussknacker.engine.api.deployment.{ProcessActionState, ScenarioActionName} -import pl.touk.nussknacker.ui.db.entity.ProcessActionEntityData -import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, DbProcessActionRepository} +import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, ScenarioActionRepository} import pl.touk.nussknacker.ui.security.api.LoggedUser -import java.time.{Clock, Instant} -import scala.concurrent.{ExecutionContext, Future} +import java.time.Clock import scala.concurrent.duration.FiniteDuration +import scala.concurrent.{ExecutionContext, Future} final case class NotificationConfig(duration: FiniteDuration) @@ -19,7 +17,7 @@ trait NotificationService { } class NotificationServiceImpl( - actionRepository: DbProcessActionRepository, + scenarioActionRepository: ScenarioActionRepository, dbioRunner: DBIOActionRunner, config: NotificationConfig, clock: Clock = Clock.systemUTC() @@ -30,60 +28,30 @@ class NotificationServiceImpl( val limit = now.minusMillis(config.duration.toMillis) dbioRunner .run( - actionRepository.getUserActionsAfter( + scenarioActionRepository.getUserActionsAfter( user, Set(ScenarioActionName.Deploy, ScenarioActionName.Cancel), ProcessActionState.FinishedStates + ProcessActionState.Failed, limit ) ) - .map(_.map { - case ( - ProcessActionEntityData(id, _, _, _, _, _, _, _, actionName, ProcessActionState.Finished, _, _, _), - processName - ) => - Notification.actionFinishedNotification(id.toString, actionName, processName) - case ( - ProcessActionEntityData( - id, - _, - _, - _, - _, - _, - _, - _, - actionName, - ProcessActionState.ExecutionFinished, - _, - _, - _ - ), - processName - ) => - Notification.actionExecutionFinishedNotification(id.toString, actionName, processName) - case ( - ProcessActionEntityData( - id, - _, - _, - _, - _, - _, - _, - _, - actionName, - ProcessActionState.Failed, - failureMessageOpt, - _, - _ - ), - processName - ) => - Notification.actionFailedNotification(id.toString, actionName, processName, failureMessageOpt) - case (a, processName) => - throw new IllegalStateException(s"Unexpected action returned by query: $a, for scenario: $processName") - }.toList) + .map(_.map { case (action, scenarioName) => + action.state match { + case ProcessActionState.Finished => + Notification + .actionFinishedNotification(action.id.toString, action.actionName, scenarioName) + case ProcessActionState.Failed => + Notification + .actionFailedNotification(action.id.toString, action.actionName, scenarioName, action.failureMessage) + case ProcessActionState.ExecutionFinished => + Notification + .actionExecutionFinishedNotification(action.id.toString, action.actionName, scenarioName) + case ProcessActionState.InProgress => + throw new IllegalStateException( + s"Unexpected action returned by query: $action, for scenario: $scenarioName" + ) + } + }) } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ProcessService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ProcessService.scala index 2deecf2877c..5602b973464 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ProcessService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ProcessService.scala @@ -8,7 +8,7 @@ import com.typesafe.scalalogging.LazyLogging import db.util.DBIOActionInstances.DB import io.circe.generic.JsonCodec import pl.touk.nussknacker.engine.api.component.ProcessingMode -import pl.touk.nussknacker.engine.api.deployment.{DataFreshnessPolicy, ProcessAction, ProcessState, ScenarioActionName} +import pl.touk.nussknacker.engine.api.deployment.{DataFreshnessPolicy, ProcessAction, ScenarioActionName} import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.process._ import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess @@ -160,7 +160,7 @@ class DBProcessService( processResolverByProcessingType: ProcessingTypeDataProvider[UIProcessResolver, _], dbioRunner: DBIOActionRunner, fetchingProcessRepository: FetchingProcessRepository[Future], - processActionRepository: ProcessActionRepository, + scenarioActionRepository: ScenarioActionRepository, processRepository: ProcessRepository[DB] )(implicit ec: ExecutionContext) extends ProcessService @@ -341,7 +341,7 @@ class DBProcessService( .runInTransaction( DBIOAction.seq( processRepository.archive(processId = process.idWithNameUnsafe, isArchived = false), - processActionRepository + scenarioActionRepository .markProcessAsUnArchived(processId = process.processIdUnsafe, process.processVersionId) ) ) @@ -480,7 +480,7 @@ class DBProcessService( .runInTransaction( DBIOAction.seq( processRepository.archive(processId = process.idWithNameUnsafe, isArchived = true), - processActionRepository.markProcessAsArchived(processId = process.processIdUnsafe, process.processVersionId) + scenarioActionRepository.markProcessAsArchived(processId = process.processIdUnsafe, process.processVersionId) ) ) @@ -493,7 +493,7 @@ class DBProcessService( } override def getProcessActions(id: ProcessId): Future[List[ProcessAction]] = { - dbioRunner.runInTransaction(processActionRepository.getFinishedProcessActions(id, None)) + dbioRunner.runInTransaction(scenarioActionRepository.getFinishedProcessActions(id, None)) } private def toProcessResponse(processName: ProcessName, created: ProcessCreated): ProcessResponse = diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentService.scala index 3184205b94c..f2c58768f4a 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentService.scala @@ -6,14 +6,19 @@ import org.apache.commons.io.input.BoundedInputStream import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} import pl.touk.nussknacker.ui.config.AttachmentsConfig import pl.touk.nussknacker.ui.process.ScenarioAttachmentService.{AttachmentDataWithName, AttachmentToAdd} -import pl.touk.nussknacker.ui.process.repository.ProcessActivityRepository +import pl.touk.nussknacker.ui.process.repository.DBIOActionRunner +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.LoggedUser import java.io.InputStream import scala.concurrent.{ExecutionContext, Future} import scala.util.Using -class ScenarioAttachmentService(config: AttachmentsConfig, scenarioActivityRepository: ProcessActivityRepository)( +class ScenarioAttachmentService( + config: AttachmentsConfig, + scenarioActivityRepository: ScenarioActivityRepository, + DBIOActionRunner: DBIOActionRunner, +)( implicit ec: ExecutionContext ) extends LazyLogging { @@ -26,24 +31,29 @@ class ScenarioAttachmentService(config: AttachmentsConfig, scenarioActivityRepos Future .apply(new BoundedInputStream(inputStream, config.maxSizeInBytes + 1)) .map(Using.resource(_) { isResource => IOUtils.toByteArray(isResource) }) - .flatMap(bytes => { + .flatMap { bytes => if (bytes.length > config.maxSizeInBytes) { Future.failed( new IllegalArgumentException(s"Maximum (${config.maxSizeInBytes} bytes) attachment size exceeded.") ) } else { - scenarioActivityRepository.addAttachment( - AttachmentToAdd(scenarioId, scenarioVersionId, originalFileName, bytes) - ) + DBIOActionRunner + .run { + scenarioActivityRepository.addAttachment( + AttachmentToAdd(scenarioId, scenarioVersionId, originalFileName, bytes) + ) + } + .map(_ => ()) } - }) + } } - def readAttachment(attachmentId: Long, scenarioId: ProcessId): Future[Option[AttachmentDataWithName]] = { - scenarioActivityRepository - .findAttachment(attachmentId, scenarioId) - .map(_.map(attachment => (attachment.fileName, attachment.data))) - } + def readAttachment(attachmentId: Long, scenarioId: ProcessId): Future[Option[AttachmentDataWithName]] = + DBIOActionRunner.run { + scenarioActivityRepository + .findAttachment(scenarioId, attachmentId) + .map(_.map(attachment => (attachment.fileName, attachment.data))) + } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala index 29da6fc6dd9..e7cdb55416d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DefaultProcessingTypeDeployedScenariosProvider.scala @@ -85,8 +85,7 @@ object DefaultProcessingTypeDeployedScenariosProvider { val dumbModelInfoProvier = ProcessingTypeDataProvider.withEmptyCombinedData( Map(processingType -> ValueWithRestriction.anyUser(Map.empty[String, String])) ) - val commentRepository = new CommentRepository(dbRef) - val actionRepository = new DbProcessActionRepository(dbRef, commentRepository, dumbModelInfoProvier) + val actionRepository = new DbScenarioActionRepository(dbRef, dumbModelInfoProvier) val scenarioLabelsRepository = new ScenarioLabelsRepository(dbRef) val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository, scenarioLabelsRepository) val futureProcessRepository = diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentService.scala index ac5b90196c8..61282df4a2d 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentService.scala @@ -47,7 +47,7 @@ import scala.util.{Failure, Success} class DeploymentService( dispatcher: DeploymentManagerDispatcher, processRepository: FetchingProcessRepository[DB], - actionRepository: DbProcessActionRepository, + actionRepository: DbScenarioActionRepository, dbioRunner: DBIOActionRunner, processValidator: ProcessingTypeDataProvider[UIProcessValidator, _], scenarioResolver: ProcessingTypeDataProvider[ScenarioResolver, _], @@ -216,7 +216,7 @@ class DeploymentService( // TODO: this is temporary step: we want ParameterValidator here. The aim is to align deployment and custom actions // and validate deployment comment (and other action parameters) the same way as in node expressions or additional properties. - private def validateDeploymentComment(comment: Option[Comment]): Future[Option[DeploymentComment]] = + private def validateDeploymentComment(comment: Option[Comment]): Future[Option[Comment]] = Future.fromTry(DeploymentComment.createDeploymentComment(comment, deploymentCommentSettings).toEither.toTry) protected def validateBeforeDeploy( @@ -323,7 +323,7 @@ class DeploymentService( private def runActionAndHandleResults[T, PS: ScenarioShapeFetchStrategy]( actionName: ScenarioActionName, - deploymentComment: Option[DeploymentComment], + deploymentComment: Option[Comment], ctx: CommandContext[PS] )(runAction: => Future[T])(implicit user: LoggedUser): Future[T] = { implicit val listenerUser: ListenerUser = ListenerApiUser(user) @@ -353,12 +353,11 @@ class DeploymentService( .map { versionOnWhichActionIsDone => logger.info(s"Finishing $actionString") val performedAt = clock.instant() - val comment = deploymentComment.map(_.toComment(actionName)) processChangeListener.handle( OnActionSuccess( ctx.latestScenarioDetails.processId, versionOnWhichActionIsDone, - comment, + deploymentComment, performedAt, actionName ) @@ -370,7 +369,7 @@ class DeploymentService( actionName, versionOnWhichActionIsDone, performedAt, - comment, + deploymentComment, ctx.buildInfoProcessingType ) ) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActivityService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActivityService.scala index e436b4ef51b..2241a77acf9 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActivityService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newactivity/ActivityService.scala @@ -1,26 +1,28 @@ package pl.touk.nussknacker.ui.process.newactivity import cats.data.EitherT -import cats.implicits.toTraverseOps -import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName +import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} import pl.touk.nussknacker.ui.api.DeploymentCommentSettings import pl.touk.nussknacker.ui.listener.Comment import pl.touk.nussknacker.ui.process.newactivity.ActivityService._ import pl.touk.nussknacker.ui.process.newdeployment.DeploymentService.RunDeploymentError import pl.touk.nussknacker.ui.process.newdeployment.{DeploymentService, RunDeploymentCommand} -import pl.touk.nussknacker.ui.process.repository.{CommentRepository, DBIOActionRunner, DeploymentComment} +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository +import pl.touk.nussknacker.ui.process.repository.{DBIOActionRunner, DeploymentComment} import pl.touk.nussknacker.ui.security.api.LoggedUser +import java.time.{Clock, Instant} import scala.concurrent.{ExecutionContext, Future} // TODO: This service in the future should handle all activities that modify anything in application. // These activities should be stored in the dedicated table (not in comments table) class ActivityService( deploymentCommentSettings: Option[DeploymentCommentSettings], - commentRepository: CommentRepository, + scenarioActivityRepository: ScenarioActivityRepository, deploymentService: DeploymentService, - dbioRunner: DBIOActionRunner + dbioRunner: DBIOActionRunner, + clock: Clock, )(implicit ec: ExecutionContext) { def processCommand[Command, ErrorType](command: Command, comment: Option[Comment])( @@ -41,15 +43,14 @@ class ActivityService( } } - private def validateDeploymentCommentWhenPassed(comment: Option[Comment]) = { - EitherT - .fromEither[Future]( - DeploymentComment - .createDeploymentComment(comment, deploymentCommentSettings) - .toEither - ) - .map(_.map(_.toComment(ScenarioActionName.Deploy))) - .leftMap[ActivityError[RunDeploymentError]](err => CommentValidationError(err.message)) + private def validateDeploymentCommentWhenPassed( + comment: Option[Comment] + ): EitherT[Future, ActivityError[RunDeploymentError], Option[Comment]] = EitherT.fromEither { + DeploymentComment + .createDeploymentComment(comment, deploymentCommentSettings) + .toEither + .left + .map[ActivityError[RunDeploymentError]](err => CommentValidationError(err.message)) } private def runDeployment(command: RunDeploymentCommand) = @@ -60,15 +61,34 @@ class ActivityService( commentOpt: Option[Comment], scenarioId: ProcessId, scenarioGraphVersionId: VersionId, - user: LoggedUser - ) = + loggedUser: LoggedUser + ): EitherT[Future, ActivityError[ErrorType], Unit] = { + val now = clock.instant() EitherT.right[ActivityError[ErrorType]]( - commentOpt - .map(comment => - dbioRunner.run(commentRepository.saveComment(scenarioId, scenarioGraphVersionId, user, comment)) + dbioRunner + .run( + scenarioActivityRepository.addActivity( + ScenarioActivity.ScenarioDeployed( + scenarioId = ScenarioId(scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = ScenarioUser( + id = Some(UserId(loggedUser.id)), + name = UserName(loggedUser.username), + impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), + impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) + ), + date = now, + scenarioVersion = Some(ScenarioVersion(scenarioGraphVersionId.value)), + comment = commentOpt match { + case Some(comment) => ScenarioComment.Available(comment.value, UserName(loggedUser.username), now) + case None => ScenarioComment.Deleted(UserName(loggedUser.username), now) + }, + ) + )(loggedUser) ) - .sequence + .map(_ => ()) ) + } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentService.scala index ad42bccb7ff..b316dc6b160 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentService.scala @@ -8,7 +8,7 @@ import pl.touk.nussknacker.engine.api.component.NodesDeploymentData import pl.touk.nussknacker.engine.api.deployment._ import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} import pl.touk.nussknacker.engine.api.{ProcessVersion => RuntimeVersionData} -import pl.touk.nussknacker.engine.deployment.{DeploymentData, DeploymentId => LegacyDeploymentId, ExternalDeploymentId} +import pl.touk.nussknacker.engine.deployment.{DeploymentData, DeploymentId => LegacyDeploymentId} import pl.touk.nussknacker.engine.newdeployment.DeploymentId import pl.touk.nussknacker.restmodel.validation.ValidationResults.ValidationErrors import pl.touk.nussknacker.security.Permission diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/CommentRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/CommentRepository.scala deleted file mode 100644 index 1c3ba445697..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/CommentRepository.scala +++ /dev/null @@ -1,47 +0,0 @@ -package pl.touk.nussknacker.ui.process.repository - -import db.util.DBIOActionInstances.DB -import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} -import pl.touk.nussknacker.ui.db.entity.CommentEntityData -import pl.touk.nussknacker.ui.db.{DbRef, NuTables} -import pl.touk.nussknacker.ui.listener.Comment -import pl.touk.nussknacker.ui.security.api.{ImpersonatedUser, LoggedUser, RealLoggedUser} -import slick.jdbc.JdbcProfile - -import java.sql.Timestamp -import java.time.Instant -import scala.concurrent.ExecutionContext - -class CommentRepository(protected val dbRef: DbRef)(implicit ec: ExecutionContext) extends NuTables { - - override protected val profile: JdbcProfile = dbRef.profile - - import profile.api._ - - def saveComment( - scenarioId: ProcessId, - scenarioGraphVersionId: VersionId, - user: LoggedUser, - comment: Comment - ): DB[CommentEntityData] = { - for { - newId <- nextId - entityData = CommentEntityData( - id = newId, - processId = scenarioId, - processVersionId = scenarioGraphVersionId, - content = comment.value, - user = user.username, - impersonatedByIdentity = user.impersonatingUserId, - impersonatedByUsername = user.impersonatingUserName, - createDate = Timestamp.from(Instant.now()) - ) - _ <- commentsTable += entityData - } yield entityData - } - - private def nextId[T <: JdbcProfile]: DBIO[Long] = { - Sequence[Long]("process_comments_id_sequence").next.result - } - -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala index 2d62ce7cdd2..c0ff82ef570 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepository.scala @@ -22,14 +22,14 @@ object DBFetchingProcessRepository { def create( dbRef: DbRef, - actionRepository: ProcessActionRepository, + actionRepository: ScenarioActionRepository, scenarioLabelsRepository: ScenarioLabelsRepository )(implicit ec: ExecutionContext) = new DBFetchingProcessRepository[DB](dbRef, actionRepository, scenarioLabelsRepository) with DbioRepository def createFutureRepository( dbRef: DbRef, - actionRepository: ProcessActionRepository, + actionRepository: ScenarioActionRepository, scenarioLabelsRepository: ScenarioLabelsRepository )( implicit ec: ExecutionContext @@ -43,7 +43,7 @@ object DBFetchingProcessRepository { // to the resource on the services side abstract class DBFetchingProcessRepository[F[_]: Monad]( protected val dbRef: DbRef, - actionRepository: ProcessActionRepository, + actionRepository: ScenarioActionRepository, scenarioLabelsRepository: ScenarioLabelsRepository, )(protected implicit val ec: ExecutionContext) extends FetchingProcessRepository[F] diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DbProcessActivityRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DbProcessActivityRepository.scala deleted file mode 100644 index 647b6319ea0..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DbProcessActivityRepository.scala +++ /dev/null @@ -1,174 +0,0 @@ -package pl.touk.nussknacker.ui.process.repository - -import com.typesafe.scalalogging.LazyLogging -import io.circe.generic.JsonCodec -import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} -import pl.touk.nussknacker.ui.db.entity.{AttachmentEntityData, CommentEntityData} -import pl.touk.nussknacker.ui.db.{DbRef, NuTables} -import pl.touk.nussknacker.ui.listener.{Comment => CommentValue} -import pl.touk.nussknacker.ui.process.ScenarioAttachmentService.AttachmentToAdd -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{Attachment, Comment, ProcessActivity} -import pl.touk.nussknacker.ui.security.api.{ImpersonatedUser, LoggedUser, RealLoggedUser} -import pl.touk.nussknacker.ui.statistics.{AttachmentsTotal, CommentsTotal} - -import java.sql.Timestamp -import java.time.Instant -import scala.concurrent.{ExecutionContext, Future} - -trait ProcessActivityRepository { - - def addComment(processId: ProcessId, processVersionId: VersionId, comment: CommentValue)( - implicit ec: ExecutionContext, - loggedUser: LoggedUser - ): Future[Unit] - - def deleteComment(commentId: Long)(implicit ec: ExecutionContext): Future[Either[Exception, Unit]] - def findActivity(processId: ProcessId)(implicit ec: ExecutionContext): Future[ProcessActivity] - - def addAttachment( - attachmentToAdd: AttachmentToAdd - )(implicit ec: ExecutionContext, loggedUser: LoggedUser): Future[Unit] - - def findAttachment(attachmentId: Long, scenarioId: ProcessId)( - implicit ec: ExecutionContext - ): Future[Option[AttachmentEntityData]] - - def getActivityStats(implicit ec: ExecutionContext): Future[Map[String, Int]] - -} - -final case class DbProcessActivityRepository(protected val dbRef: DbRef, commentRepository: CommentRepository)( - protected implicit val ec: ExecutionContext -) extends ProcessActivityRepository - with LazyLogging - with BasicRepository - with NuTables { - - import profile.api._ - - override def addComment(processId: ProcessId, processVersionId: VersionId, comment: CommentValue)( - implicit ec: ExecutionContext, - loggedUser: LoggedUser - ): Future[Unit] = { - run(commentRepository.saveComment(processId, processVersionId, loggedUser, comment)).map(_ => ()) - } - - override def deleteComment(commentId: Long)(implicit ec: ExecutionContext): Future[Either[Exception, Unit]] = { - val commentToDelete = commentsTable.filter(_.id === commentId) - val deleteAction = commentToDelete.delete - run(deleteAction).map { deletedRowsCount => - logger.info(s"Tried to delete comment with id: $commentId. Deleted rows count: $deletedRowsCount") - if (deletedRowsCount == 0) { - Left(new RuntimeException(s"Unable to delete comment with id: $commentId")) - } else { - Right(()) - } - } - } - - override def findActivity(processId: ProcessId)(implicit ec: ExecutionContext): Future[ProcessActivity] = { - val findProcessActivityAction = for { - fetchedComments <- commentsTable.filter(_.processId === processId).sortBy(_.createDate.desc).result - fetchedAttachments <- attachmentsTable.filter(_.processId === processId).sortBy(_.createDate.desc).result - comments = fetchedComments.map(c => Comment(c)).toList - attachments = fetchedAttachments.map(c => Attachment(c)).toList - } yield ProcessActivity(comments, attachments) - - run(findProcessActivityAction) - } - - override def getActivityStats(implicit ec: ExecutionContext): Future[Map[String, Int]] = { - val findScenarioProcessActivityStats = for { - attachmentsTotal <- attachmentsTable.length.result - commentsTotal <- commentsTable.length.result - } yield Map( - AttachmentsTotal -> attachmentsTotal, - CommentsTotal -> commentsTotal, - ).map { case (k, v) => (k.toString, v) } - - run(findScenarioProcessActivityStats) - } - - override def addAttachment( - attachmentToAdd: AttachmentToAdd - )(implicit ec: ExecutionContext, loggedUser: LoggedUser): Future[Unit] = { - val addAttachmentAction = for { - _ <- attachmentsTable += AttachmentEntityData( - id = -1L, - processId = attachmentToAdd.scenarioId, - processVersionId = attachmentToAdd.scenarioVersionId, - fileName = attachmentToAdd.fileName, - data = attachmentToAdd.data, - user = loggedUser.username, - impersonatedByIdentity = loggedUser.impersonatingUserId, - impersonatedByUsername = loggedUser.impersonatingUserName, - createDate = Timestamp.from(Instant.now()) - ) - } yield () - - run(addAttachmentAction) - } - - override def findAttachment( - attachmentId: Long, - scenarioId: ProcessId - )(implicit ec: ExecutionContext): Future[Option[AttachmentEntityData]] = { - val findAttachmentAction = attachmentsTable - .filter(_.id === attachmentId) - .filter(_.processId === scenarioId) - .result - .headOption - run(findAttachmentAction) - } - -} - -object DbProcessActivityRepository { - - @JsonCodec final case class ProcessActivity(comments: List[Comment], attachments: List[Attachment]) - - @JsonCodec final case class Attachment( - id: Long, - processVersionId: VersionId, - fileName: String, - user: String, - createDate: Instant - ) - - object Attachment { - - def apply(attachment: AttachmentEntityData): Attachment = { - Attachment( - id = attachment.id, - processVersionId = attachment.processVersionId, - fileName = attachment.fileName, - user = attachment.user, - createDate = attachment.createDateTime - ) - } - - } - - @JsonCodec final case class Comment( - id: Long, - processVersionId: VersionId, - content: String, - user: String, - createDate: Instant - ) - - object Comment { - - def apply(comment: CommentEntityData): Comment = { - Comment( - id = comment.id, - processVersionId = comment.processVersionId, - content = comment.content, - user = comment.user, - createDate = comment.createDateTime - ) - } - - } - -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DeploymentComment.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DeploymentComment.scala index 08b2bbf7ca4..0f1522a791c 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DeploymentComment.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/DeploymentComment.scala @@ -2,46 +2,16 @@ package pl.touk.nussknacker.ui.process.repository import cats.data.Validated import cats.data.Validated.{Invalid, Valid} -import pl.touk.nussknacker.engine.api.deployment.ScenarioActionName -import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction import pl.touk.nussknacker.ui.BadRequestError import pl.touk.nussknacker.ui.api.DeploymentCommentSettings import pl.touk.nussknacker.ui.listener.Comment -import pl.touk.nussknacker.ui.process.repository.DeploymentComment._ - -// TODO: it does not refer to "deployment" only, rename to ValidatedComment -class DeploymentComment private (value: Comment) { - - def toComment(actionName: ScenarioActionName): Comment = { - // TODO: remove this prefixes after adding custom icons - // ... or after changing how the history of user activities is displayed (so far they are displayed as - // comments and versions panels). Prefix seems to be a workaround to indicate that it is related somehow to - // some action the user requested, and is used as visualization decoration. Comment should be saved "as is", - // without modifications. - val prefix = actionName match { - case ScenarioActionName.Deploy => PrefixDeployedDeploymentComment - case ScenarioActionName.Cancel => PrefixCanceledDeploymentComment - case InstantBatchCustomAction.name => PrefixRunNowDeploymentComment - case _ => NoPrefix - } - new Comment { - override def value: String = prefix + DeploymentComment.this.value.value - } - } - -} object DeploymentComment { - private val PrefixDeployedDeploymentComment = "Deployment: " - private val PrefixCanceledDeploymentComment = "Stop: " - private val PrefixRunNowDeploymentComment = "Run now: " - private val NoPrefix = "" - def createDeploymentComment( comment: Option[Comment], deploymentCommentSettings: Option[DeploymentCommentSettings] - ): Validated[CommentValidationError, Option[DeploymentComment]] = { + ): Validated[CommentValidationError, Option[Comment]] = { (comment.filterNot(_.value.isEmpty), deploymentCommentSettings) match { case (None, Some(_)) => @@ -51,16 +21,14 @@ object DeploymentComment { case (Some(comment), Some(deploymentCommentSettings)) => Validated.cond( comment.value.matches(deploymentCommentSettings.validationPattern), - Some(new DeploymentComment(comment)), + Some(comment), CommentValidationError(comment, deploymentCommentSettings) ) case (Some(comment), None) => - Valid(Some(new DeploymentComment(comment))) + Valid(Some(comment)) } } - def unsafe(comment: Comment): DeploymentComment = new DeploymentComment(comment) - } final case class CommentValidationError(message: String) extends BadRequestError(message) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessActionRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessActionRepository.scala deleted file mode 100644 index b6012f57d26..00000000000 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessActionRepository.scala +++ /dev/null @@ -1,391 +0,0 @@ -package pl.touk.nussknacker.ui.process.repository - -import cats.implicits.toTraverseOps -import com.typesafe.scalalogging.LazyLogging -import db.util.DBIOActionInstances._ -import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState -import pl.touk.nussknacker.engine.api.deployment.{ - ProcessAction, - ProcessActionId, - ProcessActionState, - ScenarioActionName -} -import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, ProcessingType, VersionId} -import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap -import pl.touk.nussknacker.ui.app.BuildInfo -import pl.touk.nussknacker.ui.db.entity.{CommentEntityData, ProcessActionEntityData} -import pl.touk.nussknacker.ui.db.{DbRef, NuTables} -import pl.touk.nussknacker.ui.listener.Comment -import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.security.api.{ImpersonatedUser, LoggedUser, RealLoggedUser} -import slick.dbio.DBIOAction - -import java.sql.Timestamp -import java.time.Instant -import java.util.UUID -import scala.concurrent.ExecutionContext - -//TODO: Add missing methods: markProcessAsDeployed and markProcessAsCancelled -trait ProcessActionRepository { - def markProcessAsArchived(processId: ProcessId, processVersion: VersionId)(implicit user: LoggedUser): DB[_] - def markProcessAsUnArchived(processId: ProcessId, processVersion: VersionId)(implicit user: LoggedUser): DB[_] - def getFinishedProcessAction(actionId: ProcessActionId)(implicit ec: ExecutionContext): DB[Option[ProcessAction]] - - def getFinishedProcessActions(processId: ProcessId, actionNamesOpt: Option[Set[ScenarioActionName]])( - implicit ec: ExecutionContext - ): DB[List[ProcessAction]] - - def getLastActionPerProcess( - actionState: Set[ProcessActionState], - actionNamesOpt: Option[Set[ScenarioActionName]] - ): DB[Map[ProcessId, ProcessAction]] - -} - -class DbProcessActionRepository( - protected val dbRef: DbRef, - commentRepository: CommentRepository, - buildInfos: ProcessingTypeDataProvider[Map[String, String], _] -)(implicit ec: ExecutionContext) - extends DbioRepository - with NuTables - with ProcessActionRepository - with LazyLogging { - - import profile.api._ - - def addInProgressAction( - processId: ProcessId, - actionName: ScenarioActionName, - processVersion: Option[VersionId], - buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[ProcessActionId] = { - val now = Instant.now() - run( - insertAction( - None, - processId, - processVersion = processVersion, - actionName = actionName, - state = ProcessActionState.InProgress, - createdAt = now, - performedAt = None, - failure = None, - commentId = None, - buildInfoProcessingType = buildInfoProcessingType - ).map(_.id) - ) - } - - // We add comment during marking action as finished because we don't want to show this comment for in progress actions - // Also we pass all other parameters here because in_progress action can be invalidated and we have to revert it back - def markActionAsFinished( - actionId: ProcessActionId, - processId: ProcessId, - actionName: ScenarioActionName, - processVersion: VersionId, - performedAt: Instant, - comment: Option[Comment], - buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[Unit] = { - run(for { - comment <- saveCommentWhenPassed(processId, processVersion, comment) - updated <- updateAction(actionId, ProcessActionState.Finished, Some(performedAt), None, comment.map(_.id)) - _ <- - if (updated) { - DBIOAction.successful(()) - } else { - // we have to revert action - in progress action was probably invalidated - insertAction( - Some(actionId), - processId, - Some(processVersion), - actionName, - ProcessActionState.Finished, - performedAt, - Some(performedAt), - None, - comment.map(_.id), - buildInfoProcessingType - ) - } - } yield ()) - } - - // We pass all parameters here because in_progress action can be invalidated and we have to revert it back - def markActionAsFailed( - actionId: ProcessActionId, - processId: ProcessId, - actionName: ScenarioActionName, - processVersion: Option[VersionId], - performedAt: Instant, - failureMessage: String, - buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[Unit] = { - val failureMessageOpt = Option(failureMessage).map(_.take(1022)) // crop to not overflow column size) - run(for { - updated <- updateAction(actionId, ProcessActionState.Failed, Some(performedAt), failureMessageOpt, None) - _ <- - if (updated) { - DBIOAction.successful(()) - } else { - // we have to revert action - in progress action was probably invalidated - insertAction( - Some(actionId), - processId, - processVersion, - actionName, - ProcessActionState.Failed, - performedAt, - Some(performedAt), - failureMessageOpt, - None, - buildInfoProcessingType - ) - } - } yield ()) - } - - def markFinishedActionAsExecutionFinished(actionId: ProcessActionId): DB[Boolean] = { - run( - processActionsTable - .filter(a => a.id === actionId && a.state === ProcessActionState.Finished) - .map(_.state) - .update(ProcessActionState.ExecutionFinished) - .map(_ == 1) - ) - } - - def removeAction(actionId: ProcessActionId): DB[Unit] = { - run(processActionsTable.filter(a => a.id === actionId).delete.map(_ => ())) - } - - override def markProcessAsArchived(processId: ProcessId, processVersion: VersionId)( - implicit user: LoggedUser - ): DB[ProcessAction] = - addInstantAction(processId, processVersion, ScenarioActionName.Archive, None, None) - - override def markProcessAsUnArchived(processId: ProcessId, processVersion: VersionId)( - implicit user: LoggedUser - ): DB[ProcessAction] = - addInstantAction(processId, processVersion, ScenarioActionName.UnArchive, None, None) - - def addInstantAction( - processId: ProcessId, - processVersion: VersionId, - actionName: ScenarioActionName, - comment: Option[Comment], - buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[ProcessAction] = { - val now = Instant.now() - run(for { - comment <- saveCommentWhenPassed(processId, processVersion, comment) - result <- insertAction( - None, - processId, - Some(processVersion), - actionName, - ProcessActionState.Finished, - now, - Some(now), - None, - comment.map(_.id), - buildInfoProcessingType - ) - } yield toFinishedProcessAction(result, comment)) - } - - private def insertAction( - actionIdOpt: Option[ProcessActionId], - processId: ProcessId, - processVersion: Option[VersionId], - actionName: ScenarioActionName, - state: ProcessActionState, - createdAt: Instant, - performedAt: Option[Instant], - failure: Option[String], - commentId: Option[Long], - buildInfoProcessingType: Option[ProcessingType] - )(implicit user: LoggedUser): DB[ProcessActionEntityData] = { - val actionId = actionIdOpt.getOrElse(ProcessActionId(UUID.randomUUID())) - val buildInfoJsonOpt = buildInfoProcessingType.flatMap(buildInfos.forProcessingType).map(BuildInfo.writeAsJson) - val processActionData = ProcessActionEntityData( - id = actionId, - processId = processId, - processVersionId = processVersion, - user = user.username, // TODO: it should be user.id not name - impersonatedByIdentity = user.impersonatingUserId, - impersonatedByUsername = user.impersonatingUserName, - createdAt = Timestamp.from(createdAt), - performedAt = performedAt.map(Timestamp.from), - actionName = actionName, - state = state, - failureMessage = failure, - commentId = commentId, - buildInfo = buildInfoJsonOpt - ) - (processActionsTable += processActionData).map { insertCount => - if (insertCount != 1) - throw new IllegalArgumentException(s"Action with id: $actionId can't be inserted") - processActionData - } - } - - private def updateAction( - actionId: ProcessActionId, - state: ProcessActionState, - performedAt: Option[Instant], - failure: Option[String], - commentId: Option[Long] - ): DB[Boolean] = { - for { - updateCount <- processActionsTable - .filter(_.id === actionId) - .map(a => (a.performedAt, a.state, a.failureMessage, a.commentId)) - .update((performedAt.map(Timestamp.from), state, failure, commentId)) - } yield updateCount == 1 - } - - // we use "select for update where false" query syntax to lock the table - it is useful if you plan to insert something in a critical section - def lockActionsTable: DB[Unit] = { - run(processActionsTable.filter(_ => false).forUpdate.result.map(_ => ())) - } - - def getInProgressActionNames(processId: ProcessId): DB[Set[ScenarioActionName]] = { - val query = processActionsTable - .filter(action => action.processId === processId && action.state === ProcessActionState.InProgress) - .map(_.actionName) - .distinct - run(query.result.map(_.toSet)) - } - - def getInProgressActionNames( - allowedActionNames: Set[ScenarioActionName] - ): DB[Map[ProcessId, Set[ScenarioActionName]]] = { - val query = processActionsTable - .filter(action => - action.state === ProcessActionState.InProgress && - action.actionName - .inSet(allowedActionNames) - ) - .map(pa => (pa.processId, pa.actionName)) - run( - query.result - .map(_.groupBy { case (process_id, _) => process_id } - .mapValuesNow(_.map(_._2).toSet)) - ) - } - - def getUserActionsAfter( - user: LoggedUser, - possibleActionNames: Set[ScenarioActionName], - possibleStates: Set[ProcessActionState], - limit: Instant - ): DB[Seq[(ProcessActionEntityData, ProcessName)]] = { - run( - processActionsTable - .filter(a => - a.user === user.username && a.state.inSet(possibleStates) && a.actionName.inSet( - possibleActionNames - ) && a.performedAt > Timestamp.from(limit) - ) - .join(processesTable) - .on((a, p) => p.id === a.processId) - .map { case (a, p) => - (a, p.name) - } - .sortBy(_._1.performedAt) - .result - ) - } - - def deleteInProgressActions(): DB[Unit] = { - run(processActionsTable.filter(_.state === ProcessActionState.InProgress).delete.map(_ => ())) - } - - override def getLastActionPerProcess( - actionState: Set[ProcessActionState], - actionNamesOpt: Option[Set[ScenarioActionName]] - ): DB[Map[ProcessId, ProcessAction]] = { - val queryWithActionNamesFilter = actionNamesOpt - .map(actionNames => processActionsTable.filter { action => action.actionName.inSet(actionNames) }) - .getOrElse(processActionsTable) - - val finalQuery = queryWithActionNamesFilter - .filter(_.state.inSet(actionState)) - .groupBy(_.processId) - .map { case (processId, group) => (processId, group.map(_.performedAt).max) } - .join(processActionsTable) - .on { case ((processId, maxPerformedAt), action) => - action.processId === processId && action.state.inSet(actionState) && action.performedAt === maxPerformedAt - } // We fetch exactly this one with max deployment - .map { case ((processId, _), action) => processId -> action } - .joinLeft(commentsTable) - .on { case ((_, action), comment) => action.commentId === comment.id } - .map { case ((processId, action), comment) => processId -> (action, comment) } - - run( - finalQuery.result.map(_.toMap.mapValuesNow(toFinishedProcessAction)) - ) - } - - override def getFinishedProcessAction( - actionId: ProcessActionId - )(implicit ec: ExecutionContext): DB[Option[ProcessAction]] = - run( - processActionsTable - .filter(a => a.id === actionId && a.state.inSet(ProcessActionState.FinishedStates)) - .joinLeft(commentsTable) - .on { case (action, comment) => action.commentId === comment.id } - .result - .headOption - .map(_.map(toFinishedProcessAction)) - ) - - override def getFinishedProcessActions(processId: ProcessId, actionNamesOpt: Option[Set[ScenarioActionName]])( - implicit ec: ExecutionContext - ): DB[List[ProcessAction]] = { - val query = processActionsTable - .filter(p => p.processId === processId && p.state.inSet(ProcessActionState.FinishedStates)) - .joinLeft(commentsTable) - .on { case (action, comment) => action.commentId === comment.id } - .sortBy(_._1.performedAt.desc) - run( - actionNamesOpt - .map(actionNames => query.filter { case (entity, _) => entity.actionName.inSet(actionNames) }) - .getOrElse(query) - .result - .map(_.toList.map(toFinishedProcessAction)) - ) - } - - private def toFinishedProcessAction(actionData: (ProcessActionEntityData, Option[CommentEntityData])): ProcessAction = - ProcessAction( - id = actionData._1.id, - processId = actionData._1.processId, - processVersionId = actionData._1.processVersionId - .getOrElse(throw new AssertionError(s"Process version not available for finished action: ${actionData._1}")), - createdAt = actionData._1.createdAtTime, - performedAt = actionData._1.performedAtTime - .getOrElse(throw new AssertionError(s"PerformedAt not available for finished action: ${actionData._1}")), - user = actionData._1.user, - actionName = actionData._1.actionName, - state = actionData._1.state, - failureMessage = actionData._1.failureMessage, - commentId = actionData._2.map(_.id), - comment = actionData._2.map(_.content), - buildInfo = actionData._1.buildInfo.flatMap(BuildInfo.parseJson).getOrElse(BuildInfo.empty) - ) - - private def saveCommentWhenPassed( - scenarioId: ProcessId, - scenarioGraphVersionId: => VersionId, - commentOpt: Option[Comment] - )( - implicit user: LoggedUser - ): DB[Option[CommentEntityData]] = - commentOpt - .map(commentRepository.saveComment(scenarioId, scenarioGraphVersionId, user, _)) - .sequence - -} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessRepository.scala index bf0bdaa63e9..570cc0fe401 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessRepository.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ProcessRepository.scala @@ -5,6 +5,7 @@ import cats.implicits.toTraverseOps import com.typesafe.scalalogging.LazyLogging import db.util.DBIOActionInstances._ import io.circe.generic.JsonCodec +import pl.touk.nussknacker.engine.api.deployment.{ScenarioVersion, _} import pl.touk.nussknacker.engine.api.process._ import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.migration.ProcessMigrations @@ -20,11 +21,12 @@ import pl.touk.nussknacker.ui.process.repository.ProcessRepository.{ ProcessUpdated, UpdateProcessAction } +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.LoggedUser import slick.dbio.DBIOAction import java.sql.Timestamp -import java.time.Instant +import java.time.{Clock, Instant} import scala.concurrent.ExecutionContext.Implicits.global import scala.language.higherKinds @@ -44,11 +46,12 @@ object ProcessRepository { def create( dbRef: DbRef, - commentRepository: CommentRepository, + clock: Clock, + scenarioActivityRepository: ScenarioActivityRepository, scenarioLabelsRepository: ScenarioLabelsRepository, - migrations: ProcessingTypeDataProvider[ProcessMigrations, _] + migrations: ProcessingTypeDataProvider[ProcessMigrations, _], ): DBProcessRepository = - new DBProcessRepository(dbRef, commentRepository, scenarioLabelsRepository, migrations.mapValues(_.version)) + new DBProcessRepository(dbRef, clock, scenarioActivityRepository, scenarioLabelsRepository, migrations.mapValues(_.version)) final case class CreateProcessAction( processName: ProcessName, @@ -91,9 +94,10 @@ trait ProcessRepository[F[_]] { class DBProcessRepository( protected val dbRef: DbRef, - commentRepository: CommentRepository, + clock: Clock, + scenarioActivityRepository: ScenarioActivityRepository, scenarioLabelsRepository: ScenarioLabelsRepository, - modelVersion: ProcessingTypeDataProvider[Int, _] + modelVersion: ProcessingTypeDataProvider[Int, _], ) extends ProcessRepository[DB] with NuTables with LazyLogging @@ -157,9 +161,29 @@ class DBProcessRepository( val userName = updateProcessAction.forwardedUserName.map(_.name).getOrElse(loggedUser.username) def addNewCommentToVersion(scenarioId: ProcessId, scenarioGraphVersionId: VersionId) = { - updateProcessAction.comment - .map(commentRepository.saveComment(scenarioId, scenarioGraphVersionId, loggedUser, _)) - .sequence + updateProcessAction.comment.map { comment => + run( + scenarioActivityRepository.addActivity( + ScenarioActivity.ScenarioModified( + scenarioId = ScenarioId(scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = ScenarioUser( + id = Some(UserId(loggedUser.id)), + name = UserName(loggedUser.username), + impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), + impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) + ), + date = Instant.now(), + scenarioVersion = Some(ScenarioVersion(scenarioGraphVersionId.value)), + comment = ScenarioComment.Available( + comment = comment.value, + lastModifiedByUserName = UserName(loggedUser.username), + lastModifiedAt = clock.instant(), + ) + ) + ) + ) + }.sequence } for { @@ -279,11 +303,21 @@ class DBProcessRepository( .headOption .flatMap { case Some(version) => - commentRepository.saveComment( - process.id, - version.id, - loggedUser, - UpdateProcessComment(s"Rename: [${process.name}] -> [$newName]") + scenarioActivityRepository.addActivity( + ScenarioActivity.ScenarioNameChanged( + scenarioId = ScenarioId(process.id.value), + scenarioActivityId = ScenarioActivityId.random, + user = ScenarioUser( + id = Some(UserId(loggedUser.id)), + name = UserName(loggedUser.username), + impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), + impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) + ), + date = Instant.now(), + scenarioVersion = Some(ScenarioVersion(version.id.value)), + oldName = process.name.value, + newName = newName.value + ) ) case None => DBIO.successful(()) } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala new file mode 100644 index 00000000000..e4e70de5c1c --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/ScenarioActionRepository.scala @@ -0,0 +1,498 @@ +package pl.touk.nussknacker.ui.process.repository + +import cats.data.NonEmptyList +import com.typesafe.scalalogging.LazyLogging +import db.util.DBIOActionInstances._ +import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState +import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, ProcessingType, VersionId} +import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction +import pl.touk.nussknacker.engine.util.Implicits.RichScalaMap +import pl.touk.nussknacker.ui.app.BuildInfo +import pl.touk.nussknacker.ui.db.entity.{AdditionalProperties, ScenarioActivityEntityData, ScenarioActivityType} +import pl.touk.nussknacker.ui.db.{DbRef, NuTables} +import pl.touk.nussknacker.ui.listener.Comment +import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider +import pl.touk.nussknacker.ui.security.api.LoggedUser +import slick.dbio.DBIOAction + +import java.sql.Timestamp +import java.time.Instant +import java.util.UUID +import scala.concurrent.ExecutionContext + +//TODO: Add missing methods: markProcessAsDeployed and markProcessAsCancelled +trait ScenarioActionRepository { + + def markProcessAsArchived( + processId: ProcessId, + processVersion: VersionId + )(implicit user: LoggedUser): DB[_] + + def markProcessAsUnArchived( + processId: ProcessId, + processVersion: VersionId + )(implicit user: LoggedUser): DB[_] + + def getFinishedProcessAction( + actionId: ProcessActionId + ): DB[Option[ProcessAction]] + + def getFinishedProcessActions( + processId: ProcessId, + actionNamesOpt: Option[Set[ScenarioActionName]] + ): DB[List[ProcessAction]] + + def getLastActionPerProcess( + actionState: Set[ProcessActionState], + actionNamesOpt: Option[Set[ScenarioActionName]] + ): DB[Map[ProcessId, ProcessAction]] + + def getUserActionsAfter( + user: LoggedUser, + possibleActionNames: Set[ScenarioActionName], + possibleStates: Set[ProcessActionState], + limit: Instant + ): DB[List[(ProcessAction, ProcessName)]] + +} + +class DbScenarioActionRepository( + protected val dbRef: DbRef, + buildInfos: ProcessingTypeDataProvider[Map[String, String], _] +)(implicit ec: ExecutionContext) + extends DbioRepository + with NuTables + with ScenarioActionRepository + with LazyLogging { + + import profile.api._ + + def addInProgressAction( + processId: ProcessId, + actionName: ScenarioActionName, + processVersion: Option[VersionId], + buildInfoProcessingType: Option[ProcessingType] + )(implicit user: LoggedUser): DB[ProcessActionId] = { + val now = Instant.now() + run( + insertAction( + None, + processId, + processVersion = processVersion, + actionName = actionName, + state = ProcessActionState.InProgress, + createdAt = now, + performedAt = None, + failure = None, + comment = None, + buildInfoProcessingType = buildInfoProcessingType + ).map(_.activityId.value).map(ProcessActionId.apply) + ) + } + + // We add comment during marking action as finished because we don't want to show this comment for in progress actions + // Also we pass all other parameters here because in_progress action can be invalidated and we have to revert it back + def markActionAsFinished( + actionId: ProcessActionId, + processId: ProcessId, + actionName: ScenarioActionName, + processVersion: VersionId, + performedAt: Instant, + comment: Option[Comment], + buildInfoProcessingType: Option[ProcessingType] + )(implicit user: LoggedUser): DB[Unit] = { + run(for { + updated <- updateAction(actionId, ProcessActionState.Finished, Some(performedAt), None, comment) + _ <- + if (updated) { + DBIOAction.successful(()) + } else { + // we have to revert action - in progress action was probably invalidated + insertAction( + Some(actionId), + processId, + Some(processVersion), + actionName, + ProcessActionState.Finished, + performedAt, + Some(performedAt), + None, + comment, + buildInfoProcessingType + ) + } + } yield ()) + } + + // We pass all parameters here because in_progress action can be invalidated and we have to revert it back + def markActionAsFailed( + actionId: ProcessActionId, + processId: ProcessId, + actionName: ScenarioActionName, + processVersion: Option[VersionId], + performedAt: Instant, + failureMessage: String, + buildInfoProcessingType: Option[ProcessingType] + )(implicit user: LoggedUser): DB[Unit] = { + val failureMessageOpt = Option(failureMessage).map(_.take(1022)) // crop to not overflow column size) + run(for { + updated <- updateAction(actionId, ProcessActionState.Failed, Some(performedAt), failureMessageOpt, None) + _ <- + if (updated) { + DBIOAction.successful(()) + } else { + // we have to revert action - in progress action was probably invalidated + insertAction( + Some(actionId), + processId, + processVersion, + actionName, + ProcessActionState.Failed, + performedAt, + Some(performedAt), + failureMessageOpt, + None, + buildInfoProcessingType + ) + } + } yield ()) + } + + def markFinishedActionAsExecutionFinished(actionId: ProcessActionId): DB[Boolean] = { + run( + scenarioActivityTable + .filter(a => a.activityId === activityId(actionId) && a.state === ProcessActionState.Finished) + .map(_.state) + .update(Some(ProcessActionState.ExecutionFinished)) + .map(_ == 1) + ) + } + + def removeAction(actionId: ProcessActionId): DB[Unit] = { + run(scenarioActivityTable.filter(a => a.activityId === activityId(actionId)).delete.map(_ => ())) + } + + override def markProcessAsArchived(processId: ProcessId, processVersion: VersionId)( + implicit user: LoggedUser + ): DB[ProcessAction] = + addInstantAction(processId, processVersion, ScenarioActionName.Archive, None, None) + + override def markProcessAsUnArchived(processId: ProcessId, processVersion: VersionId)( + implicit user: LoggedUser + ): DB[ProcessAction] = + addInstantAction(processId, processVersion, ScenarioActionName.UnArchive, None, None) + + def addInstantAction( + processId: ProcessId, + processVersion: VersionId, + actionName: ScenarioActionName, + comment: Option[Comment], + buildInfoProcessingType: Option[ProcessingType] + )(implicit user: LoggedUser): DB[ProcessAction] = { + val now = Instant.now() + run( + insertAction( + None, + processId, + Some(processVersion), + actionName, + ProcessActionState.Finished, + now, + Some(now), + None, + comment, + buildInfoProcessingType + ).map(a => toFinishedProcessAction(a)) + ) + } + + private def insertAction( + actionIdOpt: Option[ProcessActionId], + processId: ProcessId, + processVersion: Option[VersionId], + actionName: ScenarioActionName, + state: ProcessActionState, + createdAt: Instant, + performedAt: Option[Instant], + failure: Option[String], + comment: Option[Comment], + buildInfoProcessingType: Option[ProcessingType] + )(implicit user: LoggedUser): DB[ScenarioActivityEntityData] = { + val actionId = actionIdOpt.getOrElse(ProcessActionId(UUID.randomUUID())) + val buildInfoJsonOpt = buildInfoProcessingType.flatMap(buildInfos.forProcessingType).map(BuildInfo.writeAsJson) + + val activityType = actionName match { + case ScenarioActionName.Deploy => + ScenarioActivityType.ScenarioDeployed + case ScenarioActionName.Cancel => + ScenarioActivityType.ScenarioCanceled + case ScenarioActionName.Archive => + ScenarioActivityType.ScenarioArchived + case ScenarioActionName.UnArchive => + ScenarioActivityType.ScenarioUnarchived + case ScenarioActionName.Pause => + ScenarioActivityType.ScenarioPaused + case ScenarioActionName.Rename => + ScenarioActivityType.ScenarioNameChanged + case InstantBatchCustomAction.name => + ScenarioActivityType.PerformedSingleExecution + case otherCustomName => + ScenarioActivityType.CustomAction(otherCustomName.value) + } + val entity = ScenarioActivityEntityData( + id = -1, + activityType = activityType, + scenarioId = processId, + activityId = ScenarioActivityId(actionIdOpt.map(_.value).getOrElse(UUID.randomUUID())), + userId = Some(user.id), + userName = user.username, + impersonatedByUserId = user.impersonatingUserId, + impersonatedByUserName = user.impersonatingUserName, + lastModifiedByUserName = Some(user.username), + lastModifiedAt = Some(Timestamp.from(createdAt)), + createdAt = Timestamp.from(createdAt), + scenarioVersion = processVersion.map(_.value).map(ScenarioVersion.apply), + comment = comment.map(_.value), + attachmentId = None, + finishedAt = performedAt.map(Timestamp.from), + state = Some(state), + errorMessage = failure, + buildInfo = buildInfoJsonOpt, + additionalProperties = AdditionalProperties(Map.empty) + ) + (scenarioActivityTable += entity).map { insertCount => + if (insertCount != 1) + throw new IllegalArgumentException(s"Action with id: $actionId can't be inserted") + entity + } + } + + private def updateAction( + actionId: ProcessActionId, + state: ProcessActionState, + performedAt: Option[Instant], + failure: Option[String], + comment: Option[Comment], + ): DB[Boolean] = { + for { + updateCount <- scenarioActivityTable + .filter(_.activityId === activityId(actionId)) + .map(a => (a.performedAt, a.state, a.errorMessage, a.comment)) + .update( + (performedAt.map(Timestamp.from), Some(state), failure, comment.map(_.value)) + ) + } yield updateCount == 1 + } + + // we use "select for update where false" query syntax to lock the table - it is useful if you plan to insert something in a critical section + def lockActionsTable: DB[Unit] = { + run(scenarioActivityTable.filter(_ => false).forUpdate.result.map(_ => ())) + } + + def getInProgressActionNames(processId: ProcessId): DB[Set[ScenarioActionName]] = { + val query = scenarioActivityTable + .filter(action => action.scenarioId === processId && action.state === ProcessActionState.InProgress) + .map(_.activityType) + .distinct + run(query.result.map(_.toSet.flatMap(actionName))) + } + + def getInProgressActionNames( + allowedActionNames: Set[ScenarioActionName] + ): DB[Map[ProcessId, Set[ScenarioActionName]]] = { + val query = scenarioActivityTable + .filter(action => + action.state === ProcessActionState.InProgress && + action.activityType + .inSet(activityTypes(allowedActionNames)) + ) + .map(pa => (pa.scenarioId, pa.activityType)) + run( + query.result + .map(_.groupBy { case (process_id, _) => ProcessId(process_id.value) } + .mapValuesNow(_.map(_._2).toSet.flatMap(actionName))) + ) + } + + def getUserActionsAfter( + loggedUser: LoggedUser, + possibleActionNames: Set[ScenarioActionName], + possibleStates: Set[ProcessActionState], + limit: Instant + ): DB[List[(ProcessAction, ProcessName)]] = { + run( + scenarioActivityTable + .filter(a => + a.userId === loggedUser.id && a.state.inSet(possibleStates) && a.activityType.inSet( + activityTypes(possibleActionNames) + ) && a.performedAt > Timestamp.from(limit) + ) + .join(processesTable) + .on((a, p) => p.id === a.scenarioId) + .map { case (a, p) => + (a, p.name) + } + .sortBy(_._1.performedAt) + .result + .map(_.map { case (data, name) => + (toFinishedProcessAction(data), name) + }.toList) + ) + } + + def deleteInProgressActions(): DB[Unit] = { + run(scenarioActivityTable.filter(_.state === ProcessActionState.InProgress).delete.map(_ => ())) + } + + override def getLastActionPerProcess( + actionState: Set[ProcessActionState], + actionNamesOpt: Option[Set[ScenarioActionName]] + ): DB[Map[ProcessId, ProcessAction]] = { + val activityTypes = actionNamesOpt.getOrElse(Set.empty).map(activityType).toList + + val queryWithActionNamesFilter = NonEmptyList.fromList(activityTypes) match { + case Some(activityTypes) => + scenarioActivityTable.filter { action => action.activityType.inSet(activityTypes.toList) } + case None => + scenarioActivityTable + } + + val finalQuery = queryWithActionNamesFilter + .filter(_.state.inSet(actionState)) + .groupBy(_.scenarioId) + .map { case (processId, group) => (processId, group.map(_.performedAt).max) } + .join(scenarioActivityTable) + .on { case ((scenarioId, maxPerformedAt), action) => + action.scenarioId === scenarioId && action.state.inSet(actionState) && action.performedAt === maxPerformedAt + } // We fetch exactly this one with max deployment + .map { case ((scenarioId, _), activity) => scenarioId -> activity } + + run( + finalQuery.result.map(_.map { case (scenarioId, action) => + (ProcessId(scenarioId.value), toFinishedProcessAction(action)) + }.toMap) + ) + } + + override def getFinishedProcessAction( + actionId: ProcessActionId + ): DB[Option[ProcessAction]] = + run( + scenarioActivityTable + .filter(a => + a.activityId === ScenarioActivityId(actionId.value) && a.state.inSet( + ProcessActionState.FinishedStates + ) + ) + .result + .headOption + .map(_.map(toFinishedProcessAction)) + ) + + override def getFinishedProcessActions( + processId: ProcessId, + actionNamesOpt: Option[Set[ScenarioActionName]] + ): DB[List[ProcessAction]] = { + val query = scenarioActivityTable + .filter(p => p.scenarioId === processId && p.state.inSet(ProcessActionState.FinishedStates)) + .sortBy(_.performedAt.desc) + run( + actionNamesOpt + .map(actionNames => query.filter { entity => entity.activityType.inSet(activityTypes(actionNames)) }) + .getOrElse(query) + .result + .map(_.toList.map(toFinishedProcessAction)) + ) + } + + private def toFinishedProcessAction(activityEntity: ScenarioActivityEntityData): ProcessAction = { + ProcessAction( + id = ProcessActionId(activityEntity.activityId.value), + processId = ProcessId(activityEntity.scenarioId.value), + processVersionId = activityEntity.scenarioVersion + .map(_.value) + .map(VersionId.apply) + .getOrElse(throw new AssertionError(s"Process version not available for finished action: $activityEntity")), + createdAt = activityEntity.createdAt.toInstant, + performedAt = activityEntity.finishedAt + .map(_.toInstant) + .getOrElse(throw new AssertionError(s"PerformedAt not available for finished action: $activityEntity")), + user = activityEntity.userName.value, + actionName = actionName(activityEntity.activityType) + .getOrElse(throw new AssertionError(s"ActionName not available for finished action: $activityEntity")), + state = activityEntity.state + .getOrElse(throw new AssertionError(s"State not available for finished action: $activityEntity")), + failureMessage = activityEntity.errorMessage, + commentId = activityEntity.comment.map(_ => activityEntity.id), + comment = activityEntity.comment.map(_.value), + buildInfo = activityEntity.buildInfo.flatMap(BuildInfo.parseJson).getOrElse(BuildInfo.empty) + ) + } + + private def activityId(actionId: ProcessActionId) = + ScenarioActivityId(actionId.value) + + private def actionName(activityType: ScenarioActivityType): Option[ScenarioActionName] = { + activityType match { + case ScenarioActivityType.ScenarioCreated => + None + case ScenarioActivityType.ScenarioArchived => + Some(ScenarioActionName.Archive) + case ScenarioActivityType.ScenarioUnarchived => + Some(ScenarioActionName.UnArchive) + case ScenarioActivityType.ScenarioDeployed => + Some(ScenarioActionName.Deploy) + case ScenarioActivityType.ScenarioPaused => + Some(ScenarioActionName.Pause) + case ScenarioActivityType.ScenarioCanceled => + Some(ScenarioActionName.Cancel) + case ScenarioActivityType.ScenarioModified => + None + case ScenarioActivityType.ScenarioNameChanged => + Some(ScenarioActionName.Rename) + case ScenarioActivityType.CommentAdded => + None + case ScenarioActivityType.AttachmentAdded => + None + case ScenarioActivityType.ChangedProcessingMode => + None + case ScenarioActivityType.IncomingMigration => + None + case ScenarioActivityType.OutgoingMigration => + None + case ScenarioActivityType.PerformedSingleExecution => + None + case ScenarioActivityType.PerformedScheduledExecution => + None + case ScenarioActivityType.AutomaticUpdate => + None + case ScenarioActivityType.CustomAction(name) => + Some(ScenarioActionName(name)) + } + } + + private def activityTypes(actionNames: Set[ScenarioActionName]): Set[ScenarioActivityType] = { + actionNames.map(activityType) + } + + private def activityType(actionName: ScenarioActionName): ScenarioActivityType = { + actionName match { + case ScenarioActionName.Deploy => + ScenarioActivityType.ScenarioDeployed + case ScenarioActionName.Cancel => + ScenarioActivityType.ScenarioCanceled + case ScenarioActionName.Archive => + ScenarioActivityType.ScenarioArchived + case ScenarioActionName.UnArchive => + ScenarioActivityType.ScenarioUnarchived + case ScenarioActionName.Pause => + ScenarioActivityType.ScenarioPaused + case ScenarioActionName.Rename => + ScenarioActivityType.ScenarioNameChanged + case otherCustomAction => + ScenarioActivityType.CustomAction(otherCustomAction.value) + } + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala new file mode 100644 index 00000000000..7b89d3d1818 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/DbScenarioActivityRepository.scala @@ -0,0 +1,933 @@ +package pl.touk.nussknacker.ui.process.repository.activities + +import cats.implicits.catsSyntaxEitherId +import com.typesafe.scalalogging.LazyLogging +import db.util.DBIOActionInstances.DB +import pl.touk.nussknacker.engine.api.component.ProcessingMode +import pl.touk.nussknacker.engine.api.deployment.ProcessActionState.ProcessActionState +import pl.touk.nussknacker.engine.api.deployment.ScenarioAttachment.{AttachmentFilename, AttachmentId} +import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy +import pl.touk.nussknacker.ui.db.entity.{ + AdditionalProperties, + AttachmentEntityData, + ScenarioActivityEntityData, + ScenarioActivityType +} +import pl.touk.nussknacker.ui.db.{DbRef, NuTables} +import pl.touk.nussknacker.ui.process.ScenarioAttachmentService.AttachmentToAdd +import pl.touk.nussknacker.ui.process.repository.DbioRepository +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository.ModifyCommentError +import pl.touk.nussknacker.ui.security.api.LoggedUser +import pl.touk.nussknacker.ui.statistics.{AttachmentsTotal, CommentsTotal} + +import java.sql.Timestamp +import java.time.Clock +import scala.concurrent.ExecutionContext +import scala.util.Try + +class DbScenarioActivityRepository(override protected val dbRef: DbRef, clock: Clock)( + implicit executionContext: ExecutionContext, +) extends DbioRepository + with NuTables + with ScenarioActivityRepository + with LazyLogging { + + import dbRef.profile.api._ + + def findActivities( + scenarioId: ProcessId, + ): DB[Seq[ScenarioActivity]] = { + doFindActivities(scenarioId).map(_.map(_._2)) + } + + def addActivity( + scenarioActivity: ScenarioActivity, + )(implicit user: LoggedUser): DB[ScenarioActivityId] = { + insertActivity(scenarioActivity).map(_.activityId) + } + + def addComment( + scenarioId: ProcessId, + processVersionId: VersionId, + comment: String, + )(implicit user: LoggedUser): DB[ScenarioActivityId] = { + val now = clock.instant() + insertActivity( + ScenarioActivity.CommentAdded( + scenarioId = ScenarioId(scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = toUser(user), + date = now, + scenarioVersion = Some(ScenarioVersion(processVersionId.value)), + comment = ScenarioComment.Available( + comment = comment, + lastModifiedByUserName = UserName(user.username), + lastModifiedAt = now, + ) + ), + ).map(_.activityId) + } + + def editComment( + scenarioId: ProcessId, + rowId: Long, + comment: String + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivityByRowId( + rowId = rowId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = validateCommentExists(scenarioId), + modify = doEditComment(comment), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def editComment( + scenarioId: ProcessId, + activityId: ScenarioActivityId, + comment: String + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivityByActivityId( + activityId = activityId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = validateCommentExists(scenarioId), + modify = doEditComment(comment), + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def deleteComment( + scenarioId: ProcessId, + rowId: Long, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivityByRowId( + rowId = rowId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = validateCommentExists(scenarioId), + modify = doDeleteComment, + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def deleteComment( + scenarioId: ProcessId, + activityId: ScenarioActivityId, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] = { + modifyActivityByActivityId( + activityId = activityId, + activityDoesNotExistError = ModifyCommentError.ActivityDoesNotExist, + validateCurrentValue = validateCommentExists(scenarioId), + modify = doDeleteComment, + couldNotModifyError = ModifyCommentError.CouldNotModifyComment, + ) + } + + def addAttachment( + attachmentToAdd: AttachmentToAdd + )(implicit user: LoggedUser): DB[ScenarioActivityId] = { + val now = clock.instant() + for { + attachment <- attachmentInsertQuery += AttachmentEntityData( + id = -1L, + processId = attachmentToAdd.scenarioId, + processVersionId = attachmentToAdd.scenarioVersionId, + fileName = attachmentToAdd.fileName, + data = attachmentToAdd.data, + user = user.username, + impersonatedByIdentity = user.impersonatingUserId, + impersonatedByUsername = user.impersonatingUserName, + createDate = Timestamp.from(now) + ) + activity <- insertActivity( + ScenarioActivity.AttachmentAdded( + scenarioId = ScenarioId(attachmentToAdd.scenarioId.value), + scenarioActivityId = ScenarioActivityId.random, + user = toUser(user), + date = now, + scenarioVersion = Some(ScenarioVersion(attachmentToAdd.scenarioVersionId.value)), + attachment = ScenarioAttachment.Available( + attachmentId = AttachmentId(attachment.id), + attachmentFilename = AttachmentFilename(attachmentToAdd.fileName), + lastModifiedByUserName = UserName(user.username), + lastModifiedAt = now, + ) + ), + ) + } yield activity.activityId + } + + def findAttachments( + scenarioId: ProcessId, + ): DB[Seq[AttachmentEntityData]] = { + attachmentsTable + .filter(_.processId === scenarioId) + .result + } + + def findAttachment( + scenarioId: ProcessId, + attachmentId: Long, + ): DB[Option[AttachmentEntityData]] = { + attachmentsTable + .filter(_.id === attachmentId) + .filter(_.processId === scenarioId) + .result + .headOption + } + + def findActivity( + processId: ProcessId + ): DB[Legacy.ProcessActivity] = { + for { + attachmentEntities <- findAttachments(processId) + attachments = attachmentEntities.map(toDto) + activities <- doFindActivities(processId) + comments = activities.flatMap { case (id, activity) => toComment(id, activity) } + } yield Legacy.ProcessActivity( + comments = comments.toList, + attachments = attachments.toList, + ) + } + + def getActivityStats: DB[Map[String, Int]] = { + val findScenarioProcessActivityStats = for { + attachmentsTotal <- attachmentsTable.length.result + commentsTotal <- scenarioActivityTable.filter(_.comment.isDefined).length.result + } yield Map( + AttachmentsTotal -> attachmentsTotal, + CommentsTotal -> commentsTotal, + ).map { case (k, v) => (k.toString, v) } + run(findScenarioProcessActivityStats) + } + + private def doFindActivities( + scenarioId: ProcessId, + ): DB[Seq[(Long, ScenarioActivity)]] = { + scenarioActivityTable + .filter(_.scenarioId === scenarioId) + .result + .map(_.map(fromEntity)) + .map { + _.flatMap { + case Left(error) => + logger.warn(s"Ignoring invalid scenario activity: [$error]") + None + case Right(activity) => + Some(activity) + } + } + } + + private def validateCommentExists(scenarioId: ProcessId)(entity: ScenarioActivityEntityData) = { + for { + _ <- Either.cond(entity.scenarioId == scenarioId, (), ModifyCommentError.CommentDoesNotExist) + _ <- entity.comment.toRight(ModifyCommentError.CommentDoesNotExist) + } yield () + } + + private def toComment( + id: Long, + scenarioActivity: ScenarioActivity, + comment: ScenarioComment, + prefix: Option[String] + ): Option[Legacy.Comment] = { + for { + scenarioVersion <- scenarioActivity.scenarioVersion + content <- comment match { + case ScenarioComment.Available(comment, _, _) => Some(comment) + case ScenarioComment.Deleted(_, _) => None + } + } yield Legacy.Comment( + id = id, + processVersionId = scenarioVersion.value, + content = prefix.getOrElse("") + content, + user = scenarioActivity.user.name.value, + createDate = scenarioActivity.date, + ) + } + + private def toComment(id: Long, scenarioActivity: ScenarioActivity): Option[Legacy.Comment] = { + scenarioActivity match { + case _: ScenarioActivity.ScenarioCreated => + None + case _: ScenarioActivity.ScenarioArchived => + None + case _: ScenarioActivity.ScenarioUnarchived => + None + case activity: ScenarioActivity.ScenarioDeployed => + toComment(id, activity, activity.comment, Some("Deployment: ")) + case activity: ScenarioActivity.ScenarioPaused => + toComment(id, activity, activity.comment, Some("Pause: ")) + case activity: ScenarioActivity.ScenarioCanceled => + toComment(id, activity, activity.comment, Some("Stop: ")) + case activity: ScenarioActivity.ScenarioModified => + toComment(id, activity, activity.comment, None) + case activity: ScenarioActivity.ScenarioNameChanged => + toComment( + id, + activity, + ScenarioComment + .Available(s"Rename: [${activity.oldName}] -> [${activity.newName}]", UserName(""), activity.date), + None + ) + case activity: ScenarioActivity.CommentAdded => + toComment(id, activity, activity.comment, None) + case _: ScenarioActivity.AttachmentAdded => + None + case _: ScenarioActivity.ChangedProcessingMode => + None + case _: ScenarioActivity.IncomingMigration => + None + case _: ScenarioActivity.OutgoingMigration => + None + case activity: ScenarioActivity.PerformedSingleExecution => + toComment(id, activity, activity.comment, Some("Run now: ")) + case _: ScenarioActivity.PerformedScheduledExecution => + None + case _: ScenarioActivity.AutomaticUpdate => + None + case _: ScenarioActivity.CustomAction => + None + } + } + + private def toUser(loggedUser: LoggedUser) = { + ScenarioUser( + id = Some(UserId(loggedUser.id)), + name = UserName(loggedUser.username), + impersonatedByUserId = loggedUser.impersonatingUserId.map(UserId.apply), + impersonatedByUserName = loggedUser.impersonatingUserName.map(UserName.apply) + ) + } + + private lazy val activityByRowIdCompiled = Compiled { rowId: Rep[Long] => + scenarioActivityTable.filter(_.id === rowId) + } + + private lazy val activityByIdCompiled = Compiled { activityId: Rep[ScenarioActivityId] => + scenarioActivityTable.filter(_.activityId === activityId) + } + + private lazy val attachmentInsertQuery = + attachmentsTable returning attachmentsTable.map(_.id) into ((item, id) => item.copy(id = id)) + + private def modifyActivityByActivityId[ERROR]( + activityId: ScenarioActivityId, + activityDoesNotExistError: ERROR, + validateCurrentValue: ScenarioActivityEntityData => Either[ERROR, Unit], + modify: ScenarioActivityEntityData => ScenarioActivityEntityData, + couldNotModifyError: ERROR, + ): DB[Either[ERROR, Unit]] = { + modifyActivity[ScenarioActivityId, ERROR]( + key = activityId, + fetchActivity = activityByIdCompiled(_).result.headOption, + updateRow = (id: ScenarioActivityId, updatedEntity) => activityByIdCompiled(id).update(updatedEntity), + activityDoesNotExistError = activityDoesNotExistError, + validateCurrentValue = validateCurrentValue, + modify = modify, + couldNotModifyError = couldNotModifyError + ) + } + + private def modifyActivityByRowId[ERROR]( + rowId: Long, + activityDoesNotExistError: ERROR, + validateCurrentValue: ScenarioActivityEntityData => Either[ERROR, Unit], + modify: ScenarioActivityEntityData => ScenarioActivityEntityData, + couldNotModifyError: ERROR, + ): DB[Either[ERROR, Unit]] = { + modifyActivity[Long, ERROR]( + key = rowId, + fetchActivity = activityByRowIdCompiled(_).result.headOption, + updateRow = (id: Long, updatedEntity) => activityByRowIdCompiled(id).update(updatedEntity), + activityDoesNotExistError = activityDoesNotExistError, + validateCurrentValue = validateCurrentValue, + modify = modify, + couldNotModifyError = couldNotModifyError + ) + } + + private def modifyActivity[KEY, ERROR]( + key: KEY, + fetchActivity: KEY => DB[Option[ScenarioActivityEntityData]], + updateRow: (KEY, ScenarioActivityEntityData) => DB[Int], + activityDoesNotExistError: ERROR, + validateCurrentValue: ScenarioActivityEntityData => Either[ERROR, Unit], + modify: ScenarioActivityEntityData => ScenarioActivityEntityData, + couldNotModifyError: ERROR, + ): DB[Either[ERROR, Unit]] = { + val action = for { + fetchedActivity <- fetchActivity(key) + result <- { + val modifiedEntity = for { + entity <- fetchedActivity.toRight(activityDoesNotExistError) + _ <- validateCurrentValue(entity) + modifiedEntity = modify(entity) + } yield modifiedEntity + + modifiedEntity match { + case Left(error) => + DBIO.successful(Left(error)) + case Right(modifiedEntity) => + for { + rowsAffected <- updateRow(key, modifiedEntity) + res <- DBIO.successful(Either.cond(rowsAffected != 0, (), couldNotModifyError)) + } yield res + } + } + } yield result + action.transactionally + } + + private def insertActivity( + activity: ScenarioActivity, + ): DB[ScenarioActivityEntityData] = { + val entity = toEntity(activity) + (scenarioActivityTable += entity).map { insertCount => + if (insertCount == 1) { + entity + } else { + throw new RuntimeException(s"Unable to insert activity") + } + } + } + + private def doEditComment(comment: String)( + entity: ScenarioActivityEntityData + )(implicit user: LoggedUser): ScenarioActivityEntityData = { + entity.copy( + comment = Some(comment), + lastModifiedByUserName = Some(user.username), + additionalProperties = entity.additionalProperties.withProperty( + key = s"comment_replaced_by_${user.username}_at_${clock.instant()}", + value = entity.comment.getOrElse(""), + ) + ) + } + + private def doDeleteComment( + entity: ScenarioActivityEntityData + )(implicit user: LoggedUser): ScenarioActivityEntityData = { + entity.copy( + comment = None, + lastModifiedByUserName = Some(user.username), + additionalProperties = entity.additionalProperties.withProperty( + key = s"comment_deleted_by_${user.username}_at_${clock.instant()}", + value = entity.comment.getOrElse(""), + ) + ) + } + + private def createEntity(scenarioActivity: ScenarioActivity)( + attachmentId: Option[Long] = None, + comment: Option[String] = None, + lastModifiedByUserName: Option[String] = None, + finishedAt: Option[Timestamp] = None, + state: Option[ProcessActionState] = None, + errorMessage: Option[String] = None, + buildInfo: Option[String] = None, + additionalProperties: AdditionalProperties = AdditionalProperties.empty, + ): ScenarioActivityEntityData = { + val now = Timestamp.from(clock.instant()) + val activityType = scenarioActivity match { + case _: ScenarioActivity.ScenarioCreated => ScenarioActivityType.ScenarioCreated + case _: ScenarioActivity.ScenarioArchived => ScenarioActivityType.ScenarioArchived + case _: ScenarioActivity.ScenarioUnarchived => ScenarioActivityType.ScenarioUnarchived + case _: ScenarioActivity.ScenarioDeployed => ScenarioActivityType.ScenarioDeployed + case _: ScenarioActivity.ScenarioPaused => ScenarioActivityType.ScenarioPaused + case _: ScenarioActivity.ScenarioCanceled => ScenarioActivityType.ScenarioCanceled + case _: ScenarioActivity.ScenarioModified => ScenarioActivityType.ScenarioModified + case _: ScenarioActivity.ScenarioNameChanged => ScenarioActivityType.ScenarioNameChanged + case _: ScenarioActivity.CommentAdded => ScenarioActivityType.CommentAdded + case _: ScenarioActivity.AttachmentAdded => ScenarioActivityType.AttachmentAdded + case _: ScenarioActivity.ChangedProcessingMode => ScenarioActivityType.ChangedProcessingMode + case _: ScenarioActivity.IncomingMigration => ScenarioActivityType.IncomingMigration + case _: ScenarioActivity.OutgoingMigration => ScenarioActivityType.OutgoingMigration + case _: ScenarioActivity.PerformedSingleExecution => ScenarioActivityType.PerformedSingleExecution + case _: ScenarioActivity.PerformedScheduledExecution => ScenarioActivityType.PerformedScheduledExecution + case _: ScenarioActivity.AutomaticUpdate => ScenarioActivityType.AutomaticUpdate + case activity: ScenarioActivity.CustomAction => ScenarioActivityType.CustomAction(activity.actionName) + } + ScenarioActivityEntityData( + id = -1, + activityType = activityType, + scenarioId = ProcessId(scenarioActivity.scenarioId.value), + activityId = ScenarioActivityId.random, + userId = scenarioActivity.user.id.map(_.value), + userName = scenarioActivity.user.name.value, + impersonatedByUserId = scenarioActivity.user.impersonatedByUserId.map(_.value), + impersonatedByUserName = scenarioActivity.user.impersonatedByUserName.map(_.value), + lastModifiedByUserName = lastModifiedByUserName, + lastModifiedAt = Some(now), + createdAt = now, + scenarioVersion = scenarioActivity.scenarioVersion, + comment = comment, + attachmentId = attachmentId, + finishedAt = finishedAt, + state = state, + errorMessage = errorMessage, + buildInfo = buildInfo, + additionalProperties = additionalProperties, + ) + } + + private def comment(scenarioComment: ScenarioComment): Option[String] = { + scenarioComment match { + case ScenarioComment.Available(comment, _, _) => Some(comment.value) + case ScenarioComment.Deleted(_, _) => None + } + } + + private def lastModifiedByUserName(scenarioComment: ScenarioComment): Option[String] = { + val userName = scenarioComment match { + case ScenarioComment.Available(_, lastModifiedByUserName, _) => lastModifiedByUserName + case ScenarioComment.Deleted(deletedByUserName, _) => deletedByUserName + } + Some(userName.value) + } + + private def lastModifiedByUserName(scenarioAttachment: ScenarioAttachment): Option[String] = { + val userName = scenarioAttachment match { + case ScenarioAttachment.Available(_, _, lastModifiedByUserName, _) => + Some(lastModifiedByUserName.value) + case ScenarioAttachment.Deleted(_, deletedByUserName, _) => + Some(deletedByUserName.value) + } + Some(userName.value) + } + + private def toEntity(scenarioActivity: ScenarioActivity): ScenarioActivityEntityData = { + scenarioActivity match { + case _: ScenarioActivity.ScenarioCreated => + createEntity(scenarioActivity)() + case _: ScenarioActivity.ScenarioArchived => + createEntity(scenarioActivity)() + case _: ScenarioActivity.ScenarioUnarchived => + createEntity(scenarioActivity)() + case activity: ScenarioActivity.ScenarioDeployed => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + ) + case activity: ScenarioActivity.ScenarioPaused => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + ) + case activity: ScenarioActivity.ScenarioCanceled => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + ) + case activity: ScenarioActivity.ScenarioModified => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + ) + case activity: ScenarioActivity.ScenarioNameChanged => + createEntity(scenarioActivity)( + additionalProperties = AdditionalProperties( + Map( + "oldName" -> activity.oldName, + "newName" -> activity.newName, + ) + ) + ) + case activity: ScenarioActivity.CommentAdded => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + additionalProperties = AdditionalProperties.empty, + ) + case activity: ScenarioActivity.AttachmentAdded => + val (attachmentId, attachmentFilename) = activity.attachment match { + case ScenarioAttachment.Available(id, filename, _, _) => (Some(id.value), Some(filename.value)) + case ScenarioAttachment.Deleted(filename, _, _) => (None, Some(filename.value)) + } + createEntity(scenarioActivity)( + attachmentId = attachmentId, + lastModifiedByUserName = lastModifiedByUserName(activity.attachment), + additionalProperties = AdditionalProperties( + attachmentFilename.map("attachmentFilename" -> _).toMap + ) + ) + case activity: ScenarioActivity.ChangedProcessingMode => + createEntity(scenarioActivity)( + additionalProperties = AdditionalProperties( + Map( + "fromProcessingMode" -> activity.from.entryName, + "toProcessingMode" -> activity.to.entryName, + ) + ) + ) + case activity: ScenarioActivity.IncomingMigration => + createEntity(scenarioActivity)( + additionalProperties = AdditionalProperties( + Map( + "sourceEnvironment" -> activity.sourceEnvironment.name, + "sourceScenarioVersion" -> activity.sourceScenarioVersion.value.toString, + ) + ) + ) + case activity: ScenarioActivity.OutgoingMigration => + createEntity(scenarioActivity)( + comment = comment(activity.comment), + lastModifiedByUserName = lastModifiedByUserName(activity.comment), + additionalProperties = AdditionalProperties( + Map( + "destinationEnvironment" -> activity.destinationEnvironment.name, + ) + ) + ) + case activity: ScenarioActivity.PerformedSingleExecution => + createEntity(scenarioActivity)( + finishedAt = activity.dateFinished.map(Timestamp.from), + errorMessage = activity.errorMessage, + ) + case activity: ScenarioActivity.PerformedScheduledExecution => + createEntity(scenarioActivity)( + finishedAt = activity.dateFinished.map(Timestamp.from), + errorMessage = activity.errorMessage, + ) + case activity: ScenarioActivity.AutomaticUpdate => + createEntity(scenarioActivity)( + finishedAt = Some(Timestamp.from(activity.dateFinished)), + errorMessage = activity.errorMessage, + additionalProperties = AdditionalProperties( + Map( + "description" -> activity.changes.mkString(",\n"), + ) + ) + ) + case _: ScenarioActivity.CustomAction => + createEntity(scenarioActivity)() + } + } + + private def userFromEntity(entity: ScenarioActivityEntityData): ScenarioUser = { + ScenarioUser( + id = entity.userId.map(UserId), + name = UserName(entity.userName), + impersonatedByUserId = entity.impersonatedByUserId.map(UserId.apply), + impersonatedByUserName = entity.impersonatedByUserName.map(UserName.apply), + ) + } + + private def scenarioIdFromEntity(entity: ScenarioActivityEntityData): ScenarioId = { + ScenarioId(entity.scenarioId.value) + } + + private def commentFromEntity(entity: ScenarioActivityEntityData): Either[String, ScenarioComment] = { + for { + lastModifiedByUserName <- entity.lastModifiedByUserName.toRight("Missing lastModifiedByUserName field") + lastModifiedAt <- entity.lastModifiedAt.toRight("Missing lastModifiedAt field") + } yield { + entity.comment match { + case Some(comment) => + ScenarioComment.Available( + comment = comment, + lastModifiedByUserName = UserName(lastModifiedByUserName), + lastModifiedAt = lastModifiedAt.toInstant + ) + case None => + ScenarioComment.Deleted( + deletedByUserName = UserName(lastModifiedByUserName), + deletedAt = lastModifiedAt.toInstant + ) + } + } + } + + private def attachmentFromEntity(entity: ScenarioActivityEntityData): Either[String, ScenarioAttachment] = { + for { + lastModifiedByUserName <- entity.lastModifiedByUserName.toRight("Missing lastModifiedByUserName field") + filename <- additionalPropertyFromEntity(entity, "attachmentFilename") + lastModifiedAt <- entity.lastModifiedAt.toRight("Missing lastModifiedAt field") + } yield { + entity.attachmentId match { + case Some(id) => + ScenarioAttachment.Available( + attachmentId = AttachmentId(id), + attachmentFilename = AttachmentFilename(filename), + lastModifiedByUserName = UserName(lastModifiedByUserName), + lastModifiedAt = lastModifiedAt.toInstant, + ) + case None => + ScenarioAttachment.Deleted( + attachmentFilename = AttachmentFilename(filename), + deletedByUserName = UserName(lastModifiedByUserName), + deletedAt = lastModifiedAt.toInstant, + ) + } + } + } + + private def additionalPropertyFromEntity(entity: ScenarioActivityEntityData, name: String): Either[String, String] = { + entity.additionalProperties.properties.get(name).toRight(s"Missing additional property $name") + } + + private def fromEntity(entity: ScenarioActivityEntityData): Either[String, (Long, ScenarioActivity)] = { + entity.activityType match { + case ScenarioActivityType.ScenarioCreated => + ScenarioActivity + .ScenarioCreated( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion + ) + .asRight + .map((entity.id, _)) + case ScenarioActivityType.ScenarioArchived => + ScenarioActivity + .ScenarioArchived( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion + ) + .asRight + .map((entity.id, _)) + case ScenarioActivityType.ScenarioUnarchived => + ScenarioActivity + .ScenarioUnarchived( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion + ) + .asRight + .map((entity.id, _)) + case ScenarioActivityType.ScenarioDeployed => + commentFromEntity(entity) + .map { comment => + ScenarioActivity.ScenarioDeployed( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + ) + } + .map((entity.id, _)) + case ScenarioActivityType.ScenarioPaused => + commentFromEntity(entity) + .map { comment => + ScenarioActivity.ScenarioPaused( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + ) + } + .map((entity.id, _)) + case ScenarioActivityType.ScenarioCanceled => + commentFromEntity(entity) + .map { comment => + ScenarioActivity.ScenarioCanceled( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + ) + } + .map((entity.id, _)) + case ScenarioActivityType.ScenarioModified => + commentFromEntity(entity) + .map { comment => + ScenarioActivity.ScenarioModified( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + ) + } + .map((entity.id, _)) + case ScenarioActivityType.ScenarioNameChanged => + (for { + oldNameAndNewName <- extractOldNameAndNewNameForRename(entity) + } yield ScenarioActivity.ScenarioNameChanged( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + oldName = oldNameAndNewName._1, + newName = oldNameAndNewName._2 + )).map((entity.id, _)) + case ScenarioActivityType.CommentAdded => + (for { + comment <- commentFromEntity(entity) + } yield ScenarioActivity.CommentAdded( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + )).map((entity.id, _)) + case ScenarioActivityType.AttachmentAdded => + (for { + attachment <- attachmentFromEntity(entity) + } yield ScenarioActivity.AttachmentAdded( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + attachment = attachment, + )).map((entity.id, _)) + case ScenarioActivityType.ChangedProcessingMode => + (for { + from <- additionalPropertyFromEntity(entity, "fromProcessingMode").flatMap( + ProcessingMode.withNameEither(_).left.map(_.getMessage()) + ) + to <- additionalPropertyFromEntity(entity, "toProcessingMode").flatMap( + ProcessingMode.withNameEither(_).left.map(_.getMessage()) + ) + } yield ScenarioActivity.ChangedProcessingMode( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + from = from, + to = to, + )).map((entity.id, _)) + case ScenarioActivityType.IncomingMigration => + (for { + sourceEnvironment <- additionalPropertyFromEntity(entity, "sourceEnvironment") + sourceScenarioVersion <- additionalPropertyFromEntity(entity, "sourceScenarioVersion").flatMap( + toLongOption(_).toRight("sourceScenarioVersion is not a valid Long") + ) + } yield ScenarioActivity.IncomingMigration( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + sourceEnvironment = Environment(sourceEnvironment), + sourceScenarioVersion = ScenarioVersion(sourceScenarioVersion) + )).map((entity.id, _)) + case ScenarioActivityType.OutgoingMigration => + (for { + comment <- commentFromEntity(entity) + destinationEnvironment <- additionalPropertyFromEntity(entity, "destinationEnvironment") + } yield ScenarioActivity.OutgoingMigration( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + destinationEnvironment = Environment(destinationEnvironment), + )).map((entity.id, _)) + case ScenarioActivityType.PerformedSingleExecution => + (for { + comment <- commentFromEntity(entity) + } yield ScenarioActivity.PerformedSingleExecution( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + comment = comment, + dateFinished = entity.finishedAt.map(_.toInstant), + errorMessage = entity.errorMessage, + )).map((entity.id, _)) + case ScenarioActivityType.PerformedScheduledExecution => + ScenarioActivity + .PerformedScheduledExecution( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + dateFinished = entity.finishedAt.map(_.toInstant), + errorMessage = entity.errorMessage, + ) + .asRight + .map((entity.id, _)) + case ScenarioActivityType.AutomaticUpdate => + (for { + finishedAt <- entity.finishedAt.map(_.toInstant).toRight("Missing finishedAt") + description <- additionalPropertyFromEntity(entity, "description") + } yield ScenarioActivity.AutomaticUpdate( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + dateFinished = finishedAt, + errorMessage = entity.errorMessage, + changes = description, + )).map((entity.id, _)) + + case ScenarioActivityType.CustomAction(actionName) => + (for { + comment <- commentFromEntity(entity) + } yield ScenarioActivity + .CustomAction( + scenarioId = scenarioIdFromEntity(entity), + scenarioActivityId = entity.activityId, + user = userFromEntity(entity), + date = entity.createdAt.toInstant, + scenarioVersion = entity.scenarioVersion, + actionName = actionName, + comment = comment, + )).map((entity.id, _)) + } + } + + // todo NU-1772: in next phase the legacy comments will be fully migrated to scenario activities, + // until next PR is merged there is parsing from comment content to preserve full compatibility + private def extractOldNameAndNewNameForRename( + entity: ScenarioActivityEntityData + ): Either[String, (String, String)] = { + val fromAdditionalProperties = for { + oldName <- additionalPropertyFromEntity(entity, "oldName") + newName <- additionalPropertyFromEntity(entity, "newName") + } yield (oldName, newName) + + val legacyCommentPattern = """Rename: \[(.+?)\] -> \[(.+?)\]""".r + + val fromLegacyComment = for { + comment <- entity.comment.toRight("Legacy comment not present") + oldNameAndNewName <- comment match { + case legacyCommentPattern(oldName, newName) => Right((oldName, newName)) + case _ => Left("Could not retrieve oldName and newName from legacy comment") + } + } yield oldNameAndNewName + + (fromAdditionalProperties, fromLegacyComment) match { + case (Right(valuesFromAdditionalProperties), _) => Right(valuesFromAdditionalProperties) + case (Left(_), Right(valuesFromLegacyComment)) => Right(valuesFromLegacyComment) + case (Left(error), Left(legacyError)) => Left(s"$error, $legacyError") + } + } + + private def toDto(attachmentEntityData: AttachmentEntityData): Legacy.Attachment = { + Legacy.Attachment( + id = attachmentEntityData.id, + processVersionId = attachmentEntityData.processVersionId.value, + fileName = attachmentEntityData.fileName, + user = attachmentEntityData.user, + createDate = attachmentEntityData.createDateTime, + ) + } + + private def toLongOption(str: String) = Try(str.toLong).toOption + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/ScenarioActivityRepository.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/ScenarioActivityRepository.scala new file mode 100644 index 00000000000..5a7b8c453c4 --- /dev/null +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/repository/activities/ScenarioActivityRepository.scala @@ -0,0 +1,81 @@ +package pl.touk.nussknacker.ui.process.repository.activities + +import db.util.DBIOActionInstances.DB +import pl.touk.nussknacker.engine.api.deployment.{ScenarioActivity, ScenarioActivityId} +import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy +import pl.touk.nussknacker.ui.db.entity.AttachmentEntityData +import pl.touk.nussknacker.ui.process.ScenarioAttachmentService.AttachmentToAdd +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository.ModifyCommentError +import pl.touk.nussknacker.ui.security.api.LoggedUser + +trait ScenarioActivityRepository { + + def findActivities( + scenarioId: ProcessId, + ): DB[Seq[ScenarioActivity]] + + def addActivity( + scenarioActivity: ScenarioActivity, + )(implicit user: LoggedUser): DB[ScenarioActivityId] + + def addComment( + scenarioId: ProcessId, + processVersionId: VersionId, + comment: String + )(implicit user: LoggedUser): DB[ScenarioActivityId] + + def editComment( + scenarioId: ProcessId, + scenarioActivityId: ScenarioActivityId, + comment: String, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def editComment( + scenarioId: ProcessId, + commentId: Long, + comment: String, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def deleteComment( + scenarioId: ProcessId, + commentId: Long, + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def deleteComment( + scenarioId: ProcessId, + scenarioActivityId: ScenarioActivityId + )(implicit user: LoggedUser): DB[Either[ModifyCommentError, Unit]] + + def addAttachment( + attachmentToAdd: AttachmentToAdd + )(implicit user: LoggedUser): DB[ScenarioActivityId] + + def findAttachments( + scenarioId: ProcessId, + ): DB[Seq[AttachmentEntityData]] + + def findAttachment( + scenarioId: ProcessId, + attachmentId: Long, + ): DB[Option[AttachmentEntityData]] + + def findActivity( + processId: ProcessId + ): DB[Legacy.ProcessActivity] + + def getActivityStats: DB[Map[String, Int]] + +} + +object ScenarioActivityRepository { + + sealed trait ModifyCommentError + + object ModifyCommentError { + case object ActivityDoesNotExist extends ModifyCommentError + case object CommentDoesNotExist extends ModifyCommentError + case object CouldNotModifyComment extends ModifyCommentError + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala index 602c90db1e9..94087cbd276 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/server/AkkaHttpBasedRouteProvider.scala @@ -66,6 +66,7 @@ import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeData import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypeDataLoader import pl.touk.nussknacker.ui.process.processingtype.provider.ReloadableProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository._ +import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRepository import pl.touk.nussknacker.ui.process.test.{PreliminaryScenarioTestDataSerDe, ScenarioTestService} import pl.touk.nussknacker.ui.process.version.{ScenarioGraphVersionRepository, ScenarioGraphVersionService} import pl.touk.nussknacker.ui.processreport.ProcessCounter @@ -159,15 +160,15 @@ class AkkaHttpBasedRouteProvider( val modelBuildInfo = processingTypeDataProvider.mapValues(_.designerModelData.modelData.buildInfo) implicit val implicitDbioRunner: DBIOActionRunner = dbioRunner - val commentRepository = new CommentRepository(dbRef) - val actionRepository = new DbProcessActionRepository(dbRef, commentRepository, modelBuildInfo) - val scenarioLabelsRepository = new ScenarioLabelsRepository(dbRef) + val scenarioActivityRepository = new DbScenarioActivityRepository(dbRef, designerClock) + val actionRepository = new DbScenarioActionRepository(dbRef, modelBuildInfo) + val scenarioLabelsRepository = new ScenarioLabelsRepository(dbRef) val processRepository = DBFetchingProcessRepository.create(dbRef, actionRepository, scenarioLabelsRepository) // TODO: get rid of Future based repositories - it is easier to use everywhere one implementation - DBIOAction based which allows transactions handling val futureProcessRepository = DBFetchingProcessRepository.createFutureRepository(dbRef, actionRepository, scenarioLabelsRepository) val writeProcessRepository = - ProcessRepository.create(dbRef, commentRepository, scenarioLabelsRepository, migrations) + ProcessRepository.create(dbRef, designerClock, scenarioActivityRepository, scenarioLabelsRepository, migrations) val fragmentRepository = new DefaultFragmentRepository(futureProcessRepository) val fragmentResolver = new FragmentResolver(fragmentRepository) @@ -245,7 +246,7 @@ class AkkaHttpBasedRouteProvider( // correct classloader and that won't cause further delays during handling requests processingTypeDataProvider.reloadAll().unsafeRunSync() - val processActivityRepository = new DbProcessActivityRepository(dbRef, commentRepository) + val processActivityRepository = new DbScenarioActivityRepository(dbRef, designerClock) val authenticationResources = AuthenticationResources(resolvedConfig, getClass.getClassLoader, sttpBackend) val authManager = new AuthManager(authenticationResources) @@ -253,8 +254,9 @@ class AkkaHttpBasedRouteProvider( Initialization.init( migrations, dbRef, + designerClock, processRepository, - commentRepository, + processActivityRepository, scenarioLabelsRepository, environment ) @@ -380,14 +382,16 @@ class AkkaHttpBasedRouteProvider( val scenarioActivityApiHttpService = new ScenarioActivityApiHttpService( authManager = authManager, - scenarioActivityRepository = processActivityRepository, + scenarioActivityRepository = scenarioActivityRepository, scenarioService = processService, scenarioAuthorizer = processAuthorizer, new ScenarioAttachmentService( AttachmentsConfig.create(resolvedConfig), - processActivityRepository + scenarioActivityRepository, + dbioRunner, ), - new AkkaHttpBasedTapirStreamEndpointProvider() + new AkkaHttpBasedTapirStreamEndpointProvider(), + dbioRunner, ) val scenarioParametersHttpService = new ScenarioParametersApiHttpService( authManager = authManager, @@ -422,13 +426,13 @@ class AkkaHttpBasedRouteProvider( dbioRunner, Clock.systemDefaultZone() ) - val commentRepository = new CommentRepository(dbRef) val activityService = new ActivityService( featureTogglesConfig.deploymentCommentSettings, - commentRepository, + scenarioActivityRepository, deploymentService, - dbioRunner + dbioRunner, + designerClock, ) new DeploymentApiHttpService(authManager, activityService, deploymentService) } @@ -447,8 +451,9 @@ class AkkaHttpBasedRouteProvider( new ProcessesExportResources( futureProcessRepository, processService, - processActivityRepository, - processResolver + scenarioActivityRepository, + processResolver, + dbioRunner, ), new ManagementResources( processAuthorizer, @@ -522,7 +527,8 @@ class AkkaHttpBasedRouteProvider( .values .flatten .toList, - designerClock + designerClock, + dbioRunner, ) val statisticUrlConfig = diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/statistics/UsageStatisticsReportsSettingsService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/statistics/UsageStatisticsReportsSettingsService.scala index 8149fede504..4988b353806 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/statistics/UsageStatisticsReportsSettingsService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/statistics/UsageStatisticsReportsSettingsService.scala @@ -15,7 +15,8 @@ import pl.touk.nussknacker.ui.definition.component.ComponentService import pl.touk.nussknacker.ui.process.ProcessService.GetScenarioWithDetailsOptions import pl.touk.nussknacker.ui.process.processingtype.DeploymentManagerType import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.process.repository.ProcessActivityRepository +import pl.touk.nussknacker.ui.process.repository.DBIOActionRunner +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.process.{ProcessService, ScenarioQuery} import pl.touk.nussknacker.ui.security.api.{LoggedUser, NussknackerInternalUser} @@ -30,11 +31,12 @@ object UsageStatisticsReportsSettingsService extends LazyLogging { // TODO: Instead of passing deploymentManagerTypes next to processService, we should split domain ScenarioWithDetails from DTOs - see comment in ScenarioWithDetails deploymentManagerTypes: ProcessingTypeDataProvider[DeploymentManagerType, _], fingerprintService: FingerprintService, - scenarioActivityRepository: ProcessActivityRepository, + scenarioActivityRepository: ScenarioActivityRepository, componentService: ComponentService, statisticsRepository: FEStatisticsRepository[Future], componentList: List[ComponentDefinitionWithImplementation], - designerClock: Clock + designerClock: Clock, + dbioRunner: DBIOActionRunner, )(implicit ec: ExecutionContext): UsageStatisticsReportsSettingsService = { val ignoringErrorsFEStatisticsRepository = new IgnoringErrorsFEStatisticsRepository(statisticsRepository) implicit val user: LoggedUser = NussknackerInternalUser.instance @@ -68,7 +70,7 @@ object UsageStatisticsReportsSettingsService extends LazyLogging { ) } } - def fetchActivity(): Future[Map[String, Int]] = { + def fetchActivity(): Future[Map[String, Int]] = dbioRunner.run { scenarioActivityRepository.getActivityStats } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/util/PdfExporter.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/util/PdfExporter.scala index 764d484e93e..cfaa3ed0750 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/util/PdfExporter.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/util/PdfExporter.scala @@ -19,7 +19,7 @@ import pl.touk.nussknacker.engine.graph.service.ServiceRef import pl.touk.nussknacker.engine.graph.sink.SinkRef import pl.touk.nussknacker.engine.graph.source.SourceRef import pl.touk.nussknacker.engine.graph.fragment.FragmentRef -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.ProcessActivity +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity import pl.touk.nussknacker.ui.process.repository.ScenarioWithDetailsEntity import scala.xml.{Elem, NodeSeq, XML} diff --git a/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala new file mode 100644 index 00000000000..4b861777356 --- /dev/null +++ b/designer/server/src/test/scala/db/migration/V1_057__MigrateActionsAndCommentsToScenarioActivities.scala @@ -0,0 +1,379 @@ +package db.migration + +import db.migration.V1_056__CreateScenarioActivitiesDefinition.{ + ScenarioActivitiesDefinitions, + ScenarioActivityEntityData +} +import db.migration.V1_057__MigrateActionsAndCommentsToScenarioActivitiesDefinition._ +import io.circe.syntax.EncoderOps +import org.scalatest.freespec.AnyFreeSpecLike +import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.deployment.ScenarioComment.Available +import pl.touk.nussknacker.engine.api.deployment._ +import pl.touk.nussknacker.engine.api.process.{ProcessId, ProcessName, VersionId} +import pl.touk.nussknacker.engine.api.{MetaData, ProcessAdditionalFields, RequestResponseMetaData} +import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess +import pl.touk.nussknacker.engine.management.periodic.InstantBatchCustomAction +import pl.touk.nussknacker.restmodel.component.ScenarioComponentsUsages +import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting +import pl.touk.nussknacker.test.base.it.NuItTest +import pl.touk.nussknacker.test.config.WithSimplifiedDesignerConfig +import pl.touk.nussknacker.test.utils.domain.TestFactory.newDBIOActionRunner +import pl.touk.nussknacker.ui.db.NuTables +import pl.touk.nussknacker.ui.db.entity.{AdditionalProperties, ProcessEntityData, ProcessVersionEntityData} +import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRepository +import slick.jdbc.{HsqldbProfile, JdbcProfile} + +import java.sql.Timestamp +import java.time.Instant +import java.util.UUID +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.Duration + +class V1_057__MigrateActionsAndCommentsToScenarioActivities + extends AnyFreeSpecLike + with Matchers + with NuItTest + with WithSimplifiedDesignerConfig + with WithHsqlDbTesting + with NuTables { + + override protected val profile: JdbcProfile = HsqldbProfile + + import profile.api._ + + private val runner = newDBIOActionRunner(testDbRef) + + private val migration = new Migration(HsqldbProfile) + private val processActionsDefinitions = new ProcessActionsDefinitions(profile) + private val commentsDefinitions = new CommentsDefinitions(profile) + private val activitiesDefinitions = new ScenarioActivitiesDefinitions(profile) + + private val processInsertQuery = processesTable returning + processesTable.map(_.id) into ((item, id) => item.copy(id = id)) + private val commentInsertQuery = commentsDefinitions.table returning + commentsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) + private val actionInsertQuery = processActionsDefinitions.table returning + processActionsDefinitions.table.map(_.id) into ((item, id) => item.copy(id = id)) + + private val scenarioActivityRepository = new DbScenarioActivityRepository(testDbRef, clock) + + private val now: Timestamp = Timestamp.from(Instant.now) + private val user = "John Doe" + private val processVersionId = 5L + + "When data is present in old actions and comments tables" - { + "migrate 100000 DEPLOY actions with comments to scenario_activities table" in { + val (createdProcess, actionsBeingMigrated, activitiesAfterMigration) = run( + for { + process <- processInsertQuery += processEntity(user, now) + _ <- processVersionsTable += processVersionEntity(process) + comments <- + commentInsertQuery ++= List + .range(1L, 100001L) + .map(id => commentEntity(process, id, s"Deployment: Very important change $id")) + actions <- + actionInsertQuery ++= comments.map(c => processActionEntity(process, ScenarioActionName.Deploy, Some(c.id))) + _ <- migration.migrate + activities <- activitiesDefinitions.scenarioActivitiesTable.result + } yield (process, actions, activities) + ) + + actionsBeingMigrated.length shouldBe 100000 + activitiesAfterMigration.length shouldBe 100000 + + val headActivity = + activitiesAfterMigration.head + val expectedOldCommentIdForHeadActivity = + headActivity.comment.map(_.filter(_.isDigit).toLong).get + val expectedActionIdForHeadActivity = + actionsBeingMigrated.find(_.commentId.contains(expectedOldCommentIdForHeadActivity)).map(_.id).get + + headActivity shouldBe ScenarioActivityEntityData( + id = headActivity.id, + activityType = "SCENARIO_DEPLOYED", + scenarioId = createdProcess.id.value, + activityId = expectedActionIdForHeadActivity, + userId = None, + userName = user, + impersonatedByUserId = None, + impersonatedByUserName = None, + lastModifiedByUserName = Some(user), + lastModifiedAt = Some(now), + createdAt = now, + scenarioVersion = Some(processVersionId), + comment = Some(s"Very important change $expectedOldCommentIdForHeadActivity"), + attachmentId = None, + finishedAt = None, + state = Some("IN_PROGRESS"), + errorMessage = None, + buildInfo = None, + additionalProperties = AdditionalProperties.empty.properties.asJson.noSpaces, + ) + } + "migrate DEPLOY action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.Deploy, + actionComment = Some("Deployment: Deployment with scenario fix"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioDeployed( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + comment = Available("Deployment with scenario fix", user.name, date) + ) + ) + } + "migrate CANCEL action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.Cancel, + actionComment = Some("Stop: I'm canceling this scenario, it causes problems"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioCanceled( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + comment = Available("I'm canceling this scenario, it causes problems", user.name, date) + ) + ) + } + "migrate ARCHIVE action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.Archive, + actionComment = None, + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioArchived( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + ) + ) + } + "migrate UNARCHIVE action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.UnArchive, + actionComment = None, + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioUnarchived( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + ) + ) + } + "migrate PAUSE action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.Pause, + actionComment = Some("Paused because marketing campaign is paused for now"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioPaused( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + comment = Available("Paused because marketing campaign is paused for now", user.name, date) + ) + ) + } + "migrate RENAME action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName.Rename, + actionComment = Some("Rename: [marketing-campaign] -> [marketing-campaign-plus]"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.ScenarioNameChanged( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + oldName = "marketing-campaign", + newName = "marketing-campaign-plus", + ) + ) + } + "migrate custom action 'run now' with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = InstantBatchCustomAction.name, + actionComment = Some("Run now: Deployed at the request of business"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.PerformedSingleExecution( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + dateFinished = None, + errorMessage = None, + comment = Available("Deployed at the request of business", user.name, date) + ) + ) + } + "migrate custom action with comment to scenario_activities table" in { + testMigratingActionWithComment( + scenarioActionName = ScenarioActionName("special action"), + actionComment = Some("Special action needed to be executed"), + expectedActivity = (sid, sad, user, date, sv) => + ScenarioActivity.CustomAction( + scenarioId = sid, + scenarioActivityId = sad, + user = user, + date = date, + scenarioVersion = sv, + actionName = "special action", + comment = Available("Special action needed to be executed", user.name, date) + ) + ) + } + "migrate standalone comment (not assigned to any action) to scenario_activities table" in { + val comment = "ABC" + val (process, entities) = run( + for { + process <- processInsertQuery += processEntity(user, now) + _ <- processVersionsTable += processVersionEntity(process) + _ <- commentInsertQuery += commentEntity(process, 1L, comment) + _ <- migration.migrate + entities <- activitiesDefinitions.scenarioActivitiesTable.result + } yield (process, entities) + ) + val activities = run(scenarioActivityRepository.findActivities(process.id)) + + activities shouldBe Vector( + ScenarioActivity.CommentAdded( + scenarioId = ScenarioId(process.id.value), + scenarioActivityId = ScenarioActivityId(entities.head.activityId), + user = ScenarioUser(None, UserName("John Doe"), None, None), + date = now.toInstant, + scenarioVersion = Some(ScenarioVersion(processVersionId)), + comment = Available("ABC", UserName(user), now.toInstant) + ) + ) + } + } + + private def testMigratingActionWithComment( + scenarioActionName: ScenarioActionName, + actionComment: Option[String], + expectedActivity: ( + ScenarioId, + ScenarioActivityId, + ScenarioUser, + Instant, + Option[ScenarioVersion] + ) => ScenarioActivity, + ): Unit = { + val (process, action) = run( + for { + process <- processInsertQuery += processEntity(user, now) + _ <- processVersionsTable += processVersionEntity(process) + comment <- actionComment.map(commentInsertQuery += commentEntity(process, 1L, _)) match { + case Some(commentEntity) => commentEntity.map(Some(_)) + case None => DBIO.successful(None) + } + action <- actionInsertQuery += processActionEntity(process, scenarioActionName, comment.map(_.id)) + _ <- migration.migrate + _ <- activitiesDefinitions.scenarioActivitiesTable.result + } yield (process, action) + ) + val activities = run(scenarioActivityRepository.findActivities(process.id)) + + activities shouldBe Vector( + expectedActivity( + ScenarioId(process.id.value), + ScenarioActivityId(action.id), + ScenarioUser(None, UserName("John Doe"), None, None), + now.toInstant, + Some(ScenarioVersion(processVersionId)), + ) + ) + } + + private def run[T](action: DBIO[T]): T = Await.result(runner.run(action), Duration.Inf) + + private def processEntity(user: String, timestamp: Timestamp) = ProcessEntityData( + id = ProcessId(-1L), + name = ProcessName("2023_Q1_1234_STREAMING_SERVICE"), + processCategory = "test-category", + description = None, + processingType = "BatchPeriodic", + isFragment = false, + isArchived = false, + createdAt = timestamp, + createdBy = user, + impersonatedByIdentity = None, + impersonatedByUsername = None + ) + + private def processVersionEntity( + processEntity: ProcessEntityData, + ) = + ProcessVersionEntityData( + id = VersionId(processVersionId), + processId = processEntity.id, + json = Some( + CanonicalProcess( + metaData = MetaData( + "test-id", + ProcessAdditionalFields( + description = None, + properties = Map.empty, + metaDataType = RequestResponseMetaData.typeName, + showDescription = true + ) + ), + nodes = List.empty, + additionalBranches = List.empty + ) + ), + createDate = now, + user = user, + modelVersion = None, + componentsUsages = Some(ScenarioComponentsUsages.Empty), + ) + + private def commentEntity( + processEntity: ProcessEntityData, + commentId: Long, + content: String, + ) = + CommentEntityData( + id = commentId, + processId = processEntity.id.value, + processVersionId = processVersionId, + content = content, + user = user, + impersonatedByIdentity = None, + impersonatedByUsername = None, + createDate = now, + ) + + private def processActionEntity( + processEntity: ProcessEntityData, + scenarioActionName: ScenarioActionName, + commentId: Option[Long], + ) = ProcessActionEntityData( + id = UUID.randomUUID(), + processId = processEntity.id.value, + processVersionId = Some(processVersionId), + user = user, + impersonatedByIdentity = None, + impersonatedByUsername = None, + createdAt = now, + performedAt = None, + actionName = scenarioActionName.value, + state = "IN_PROGRESS", + failureMessage = None, + commentId = commentId, + buildInfo = None + ) + +} diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala index d9f42595808..e785b29827b 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/db/DbTesting.scala @@ -9,6 +9,7 @@ import org.testcontainers.utility.DockerImageName import pl.touk.nussknacker.test.PatientScalaFutures import pl.touk.nussknacker.ui.db.{DatabaseInitializer, DbRef} +import java.time.Clock import scala.jdk.CollectionConverters._ import scala.util.{Try, Using} @@ -84,8 +85,7 @@ trait DbTesting extends BeforeAndAfterEach with BeforeAndAfterAll { def cleanDB(): Try[Unit] = Using(testDbRef.db.createSession()) { session => session.prepareStatement("""delete from "process_attachments"""").execute() - session.prepareStatement("""delete from "process_comments"""").execute() - session.prepareStatement("""delete from "process_actions"""").execute() + session.prepareStatement("""delete from "scenario_activities"""").execute() session.prepareStatement("""delete from "process_versions"""").execute() session.prepareStatement("""delete from "scenario_labels"""").execute() session.prepareStatement("""delete from "environments"""").execute() diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala index 3cffd84ba90..7b67706cb47 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/NuResourcesTest.scala @@ -5,6 +5,7 @@ import akka.http.scaladsl.server.{Directives, Route} import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller import cats.effect.IO +import cats.effect.unsafe.implicits.global import com.typesafe.config.Config import com.typesafe.scalalogging.LazyLogging import db.util.DBIOActionInstances.DB @@ -36,6 +37,7 @@ import pl.touk.nussknacker.test.mock.{MockDeploymentManager, MockManagerProvider import pl.touk.nussknacker.test.utils.domain.TestFactory._ import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} import pl.touk.nussknacker.test.utils.scalas.AkkaHttpExtensions.toRequestEntity +import pl.touk.nussknacker.ui.LoadableConfigBasedNussknackerConfig import pl.touk.nussknacker.ui.api._ import pl.touk.nussknacker.ui.config.FeatureTogglesConfig import pl.touk.nussknacker.ui.config.scenariotoolbar.CategoriesScenarioToolbarsConfigParser @@ -49,19 +51,20 @@ import pl.touk.nussknacker.ui.process.processingtype.loader.ProcessingTypesConfi import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction import pl.touk.nussknacker.ui.process.repository._ +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.process.test.{PreliminaryScenarioTestDataSerDe, ScenarioTestService} import pl.touk.nussknacker.ui.processreport.ProcessCounter import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} import pl.touk.nussknacker.ui.util.{MultipartUtils, NuPathMatchers} import slick.dbio.DBIOAction -import cats.effect.unsafe.implicits.global -import pl.touk.nussknacker.ui.LoadableConfigBasedNussknackerConfig + import java.net.URI import scala.concurrent.{ExecutionContext, Future} // TODO: Consider using NuItTest with NuScenarioConfigurationHelper instead. This one will be removed in the future. trait NuResourcesTest extends WithHsqlDbTesting + with WithClock with WithSimplifiedDesignerConfig with WithSimplifiedConfigScenarioHelper with EitherValuesDetailedMessage @@ -85,13 +88,13 @@ trait NuResourcesTest protected val processAuthorizer: AuthorizeProcess = new AuthorizeProcess(futureFetchingScenarioRepository) - protected val writeProcessRepository: DBProcessRepository = newWriteProcessRepository(testDbRef) + protected val writeProcessRepository: DBProcessRepository = newWriteProcessRepository(testDbRef, clock) protected val fragmentRepository: DefaultFragmentRepository = newFragmentRepository(testDbRef) - protected val actionRepository: DbProcessActionRepository = newActionProcessRepository(testDbRef) + protected val actionRepository: DbScenarioActionRepository = newActionProcessRepository(testDbRef) - protected val processActivityRepository: DbProcessActivityRepository = newProcessActivityRepository(testDbRef) + protected val scenarioActivityRepository: ScenarioActivityRepository = newScenarioActivityRepository(testDbRef, clock) protected val processChangeListener = new TestProcessChangeListener() @@ -170,7 +173,7 @@ trait NuResourcesTest ) protected val processActivityRoute = - new TestResource.ProcessActivityResource(processActivityRepository, processService, processAuthorizer) + new TestResource.ProcessActivityResource(scenarioActivityRepository, processService, processAuthorizer, dbioRunner) protected val processActivityRouteWithAllPermissions: Route = withAllPermissions(processActivityRoute) @@ -663,9 +666,10 @@ object TestResource { // The tests are still using akka based testing and it is not easy to integrate tapir route with this kind of tests. // should be replaced with rest call: GET /api/process/{scenarioName}/activity class ProcessActivityResource( - processActivityRepository: ProcessActivityRepository, + scenarioActivityRepository: ScenarioActivityRepository, protected val processService: ProcessService, - val processAuthorizer: AuthorizeProcess + val processAuthorizer: AuthorizeProcess, + dbioActionRunner: DBIOActionRunner, )(implicit val ec: ExecutionContext) extends Directives with FailFastCirceSupport @@ -678,7 +682,7 @@ object TestResource { path("processes" / ProcessNameSegment / "activity") { processName => (get & processId(processName)) { processId => complete { - processActivityRepository.findActivity(processId.id) + dbioActionRunner.run(scenarioActivityRepository.findActivity(processId.id)) } } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithAccessControlCheckingConfigScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithAccessControlCheckingConfigScenarioHelper.scala index 9f99290faea..b42189581fc 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithAccessControlCheckingConfigScenarioHelper.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithAccessControlCheckingConfigScenarioHelper.scala @@ -10,9 +10,9 @@ import pl.touk.nussknacker.test.utils.domain.ScenarioHelper import scala.concurrent.ExecutionContext.Implicits.global trait WithAccessControlCheckingConfigScenarioHelper { - this: WithTestDb with WithAccessControlCheckingDesignerConfig => + this: WithTestDb with WithClock with WithAccessControlCheckingDesignerConfig => - private val rawScenarioHelper = new ScenarioHelper(testDbRef, designerConfig) + private val rawScenarioHelper = new ScenarioHelper(testDbRef, clock, designerConfig) def createEmptyScenario(scenarioName: ProcessName, category: TestCategory): ProcessId = { rawScenarioHelper.createEmptyScenario(scenarioName, category.stringify, isFragment = false) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithBatchConfigScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithBatchConfigScenarioHelper.scala index fb764618aa4..6c7207bab35 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithBatchConfigScenarioHelper.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithBatchConfigScenarioHelper.scala @@ -10,9 +10,9 @@ import pl.touk.nussknacker.test.utils.domain.ScenarioHelper import scala.concurrent.ExecutionContext.Implicits.global trait WithBatchConfigScenarioHelper { - this: WithTestDb with WithBatchDesignerConfig => + this: WithTestDb with WithClock with WithBatchDesignerConfig => - private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, designerConfig) + private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, clock, designerConfig) private val usedCategory = TestCategory.Category1 def createSavedScenario(scenario: CanonicalProcess): ProcessId = { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithCategoryUsedMoreThanOnceConfigScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithCategoryUsedMoreThanOnceConfigScenarioHelper.scala index f5d2a3b9cff..c3318685833 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithCategoryUsedMoreThanOnceConfigScenarioHelper.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithCategoryUsedMoreThanOnceConfigScenarioHelper.scala @@ -10,9 +10,9 @@ import pl.touk.nussknacker.test.utils.domain.ScenarioHelper import scala.concurrent.ExecutionContext.Implicits.global trait WithCategoryUsedMoreThanOnceConfigScenarioHelper { - this: WithTestDb with WithCategoryUsedMoreThanOnceDesignerConfig => + this: WithTestDb with WithClock with WithCategoryUsedMoreThanOnceDesignerConfig => - private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, designerConfig) + private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, clock, designerConfig) private val usedCategory = TestCategory.Category1 def createSavedScenario(scenario: CanonicalProcess): ProcessId = { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithSimplifiedConfigScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithSimplifiedConfigScenarioHelper.scala index a18a77f9cfe..93beb1f8d1e 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithSimplifiedConfigScenarioHelper.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/base/it/WithSimplifiedConfigScenarioHelper.scala @@ -10,9 +10,9 @@ import pl.touk.nussknacker.test.utils.domain.ScenarioHelper import scala.concurrent.ExecutionContext.Implicits.global trait WithSimplifiedConfigScenarioHelper { - this: WithTestDb with WithSimplifiedDesignerConfig => + this: WithTestDb with WithClock with WithSimplifiedDesignerConfig => - private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, designerConfig) + private lazy val rawScenarioHelper = new ScenarioHelper(testDbRef, clock, designerConfig) private val usedCategory = TestCategory.Category1 def createSavedScenario(scenario: CanonicalProcess): ProcessId = { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ScenarioHelper.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ScenarioHelper.scala index 3e2efd504e4..6fc02ff3977 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ScenarioHelper.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/ScenarioHelper.scala @@ -17,22 +17,24 @@ import pl.touk.nussknacker.ui.process.processingtype.ValueWithRestriction import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction import pl.touk.nussknacker.ui.process.repository._ +import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} import slick.dbio.DBIOAction +import java.time.Clock import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters._ -private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implicit executionContext: ExecutionContext) - extends PatientScalaFutures { +private[test] class ScenarioHelper(dbRef: DbRef, clock: Clock, designerConfig: Config)( + implicit executionContext: ExecutionContext +) extends PatientScalaFutures { private implicit val user: LoggedUser = RealLoggedUser("admin", "admin", Map.empty, isAdmin = true) private val dbioRunner: DBIOActionRunner = new DBIOActionRunner(dbRef) - private val actionRepository: DbProcessActionRepository = new DbProcessActionRepository( + private val actionRepository: DbScenarioActionRepository = new DbScenarioActionRepository( dbRef, - new CommentRepository(dbRef), mapProcessingTypeDataProvider(Map("engine-version" -> "0.1")) ) with DbioRepository @@ -40,7 +42,8 @@ private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implici private val writeScenarioRepository: DBProcessRepository = new DBProcessRepository( dbRef, - new CommentRepository(dbRef), + clock, + new DbScenarioActivityRepository(dbRef, clock), scenarioLabelsRepository, mapProcessingTypeDataProvider(1) ) @@ -134,7 +137,7 @@ private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implici private def prepareDeploy(scenarioId: ProcessId, processingType: String): Future[_] = { val actionName = ScenarioActionName.Deploy - val comment = DeploymentComment.unsafe(UserComment("Deploy comment")).toComment(actionName) + val comment = UserComment("Deploy comment") dbioRunner.run( actionRepository.addInstantAction( scenarioId, @@ -148,7 +151,7 @@ private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implici private def prepareCancel(scenarioId: ProcessId): Future[_] = { val actionName = ScenarioActionName.Cancel - val comment = DeploymentComment.unsafe(UserComment("Cancel comment")).toComment(actionName) + val comment = UserComment("Cancel comment") dbioRunner.run( actionRepository.addInstantAction(scenarioId, VersionId.initialVersionId, actionName, Some(comment), None) ) @@ -156,7 +159,7 @@ private[test] class ScenarioHelper(dbRef: DbRef, designerConfig: Config)(implici private def prepareCustomAction(scenarioId: ProcessId): Future[_] = { val actionName = ScenarioActionName("Custom") - val comment = DeploymentComment.unsafe(UserComment("Execute custom action")).toComment(actionName) + val comment = UserComment("Execute custom action") dbioRunner.run( actionRepository.addInstantAction(scenarioId, VersionId.initialVersionId, actionName, Some(comment), None) ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/TestFactory.scala b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/TestFactory.scala index 53dab64cc12..c3be03d54a9 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/TestFactory.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/test/utils/domain/TestFactory.scala @@ -36,6 +36,7 @@ import pl.touk.nussknacker.ui.process.processingtype.{ ValueWithRestriction } import pl.touk.nussknacker.ui.process.repository._ +import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRepository import pl.touk.nussknacker.ui.process.version.{ScenarioGraphVersionRepository, ScenarioGraphVersionService} import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} import pl.touk.nussknacker.ui.uiresolving.UIProcessResolver @@ -144,7 +145,7 @@ object TestFactory { def newDummyDBIOActionRunner(): DBIOActionRunner = newDBIOActionRunner(dummyDbRef) - def newCommentRepository(dbRef: DbRef) = new CommentRepository(dbRef) + def newScenarioActivityRepository(dbRef: DbRef, clock: Clock) = new DbScenarioActivityRepository(dbRef, clock) def newScenarioLabelsRepository(dbRef: DbRef) = new ScenarioLabelsRepository(dbRef) @@ -159,16 +160,17 @@ object TestFactory { new DBFetchingProcessRepository[DB](dbRef, newActionProcessRepository(dbRef), newScenarioLabelsRepository(dbRef)) with DbioRepository - def newWriteProcessRepository(dbRef: DbRef, modelVersions: Option[Int] = Some(1)) = + def newWriteProcessRepository(dbRef: DbRef, clock: Clock, modelVersions: Option[Int] = Some(1)) = new DBProcessRepository( dbRef, - newCommentRepository(dbRef), + clock, + newScenarioActivityRepository(dbRef, clock), newScenarioLabelsRepository(dbRef), - mapProcessingTypeDataProvider(modelVersions.map(Streaming.stringify -> _).toList: _*) + mapProcessingTypeDataProvider(modelVersions.map(Streaming.stringify -> _).toList: _*), ) def newDummyWriteProcessRepository(): DBProcessRepository = - newWriteProcessRepository(dummyDbRef) + newWriteProcessRepository(dummyDbRef, Clock.systemUTC()) def newScenarioGraphVersionService(dbRef: DbRef) = new ScenarioGraphVersionService( newScenarioGraphVersionRepository(dbRef), @@ -183,17 +185,14 @@ object TestFactory { new DefaultFragmentRepository(newFutureFetchingScenarioRepository(dbRef)) def newActionProcessRepository(dbRef: DbRef) = - new DbProcessActionRepository( + new DbScenarioActionRepository( dbRef, - newCommentRepository(dbRef), mapProcessingTypeDataProvider(Streaming.stringify -> buildInfo) ) with DbioRepository - def newDummyActionRepository(): DbProcessActionRepository = + def newDummyActionRepository(): DbScenarioActionRepository = newActionProcessRepository(dummyDbRef) - def newProcessActivityRepository(dbRef: DbRef) = new DbProcessActivityRepository(dbRef, newCommentRepository(dbRef)) - def newScenarioMetadataRepository(dbRef: DbRef) = new ScenarioMetadataRepository(dbRef) def newDeploymentRepository(dbRef: DbRef, clock: Clock) = new DeploymentRepository(dbRef, clock) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentCommentSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentCommentSpec.scala index 51e458614a6..6b9f24b5e8f 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentCommentSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DeploymentCommentSpec.scala @@ -3,6 +3,7 @@ package pl.touk.nussknacker.ui.api import cats.data.Validated.{Invalid, Valid} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.ui.listener.Comment import pl.touk.nussknacker.ui.process.repository.{CommentValidationError, DeploymentComment, UserComment} class DeploymentCommentSpec extends AnyFunSuite with Matchers { @@ -36,7 +37,7 @@ class DeploymentCommentSpec extends AnyFunSuite with Matchers { } test("Comment not required, should pass validation for any comment") { - DeploymentComment.createDeploymentComment(Some(validComment), None) shouldEqual Valid(_: DeploymentComment) + DeploymentComment.createDeploymentComment(Some(validComment), None) shouldEqual Valid(_: Comment) } test("Comment required but got empty, should fail validation") { @@ -47,7 +48,7 @@ class DeploymentCommentSpec extends AnyFunSuite with Matchers { test("Comment validation for valid comment") { val deploymentComment = DeploymentComment.createDeploymentComment(Some(validComment), Some(mockDeploymentCommentSettings)) - deploymentComment shouldEqual Valid(_: DeploymentComment) + deploymentComment shouldEqual Valid(_: Comment) } test("Comment validation for invalid comment") { diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ManagementResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ManagementResourcesSpec.scala index 8154ae5117b..988b54345e8 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ManagementResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ManagementResourcesSpec.scala @@ -12,7 +12,7 @@ import org.scalatest.matchers.BeMatcher import org.scalatest.matchers.should.Matchers import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, OptionValues} import pl.touk.nussknacker.engine.api.deployment.simple.SimpleStateStatus -import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ScenarioActionName} +import pl.touk.nussknacker.engine.api.deployment.{ProcessAction, ScenarioActionName, ScenarioActivity} import pl.touk.nussknacker.engine.api.process.{ProcessName, VersionId} import pl.touk.nussknacker.engine.api.{MetaData, StreamMetaData} import pl.touk.nussknacker.engine.build.ScenarioBuilder @@ -26,9 +26,9 @@ import pl.touk.nussknacker.test.base.it.NuResourcesTest import pl.touk.nussknacker.test.mock.MockDeploymentManager import pl.touk.nussknacker.test.utils.domain.TestFactory.{withAllPermissions, withPermissions} import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos import pl.touk.nussknacker.ui.process.ScenarioQuery import pl.touk.nussknacker.ui.process.exception.ProcessIllegalAction -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.ProcessActivity // TODO: all these tests should be migrated to ManagementApiHttpServiceBusinessSpec or ManagementApiHttpServiceSecuritySpec class ManagementResourcesSpec @@ -142,12 +142,16 @@ class ManagementResourcesSpec ) ~> check { status shouldBe StatusCodes.OK // TODO: remove Deployment:, Stop: after adding custom icons - val expectedDeployComment = "Deployment: deployComment" - val expectedStopComment = "Stop: cancelComment" + val expectedDeployComment = "deployComment" + val expectedStopComment = "cancelComment" + val expectedDeployCommentInLegacyService = s"Deployment: $expectedDeployComment" + val expectedStopCommentInLegacyService = s"Stop: $expectedStopComment" getActivity(ProcessTestData.sampleScenario.name) ~> check { - val comments = responseAs[ProcessActivity].comments.sortBy(_.id) - comments.map(_.content) shouldBe List(expectedDeployComment, expectedStopComment) - + val comments = responseAs[Dtos.Legacy.ProcessActivity].comments.sortBy(_.id) + comments.map(_.content) shouldBe List( + expectedDeployCommentInLegacyService, + expectedStopCommentInLegacyService + ) val firstCommentId :: secondCommentId :: Nil = comments.map(_.id) Get(s"/processes/${ProcessTestData.sampleScenario.name}/deployments") ~> withAllPermissions( diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesExportImportResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesExportImportResourcesSpec.scala index 19253b4d432..b7dfe34a120 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesExportImportResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesExportImportResourcesSpec.scala @@ -41,8 +41,9 @@ class ProcessesExportImportResourcesSpec private val processesExportResources = new ProcessesExportResources( futureFetchingScenarioRepository, processService, - processActivityRepository, - processResolverByProcessingType + scenarioActivityRepository, + processResolverByProcessingType, + dbioRunner, ) private val routeWithAllPermissions = withAllPermissions(processesExportResources) ~ diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala index b077c51697a..acf7019a84a 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ProcessesResourcesSpec.scala @@ -35,6 +35,7 @@ import pl.touk.nussknacker.test.config.WithAccessControlCheckingDesignerConfig.{ import pl.touk.nussknacker.test.config.{WithAccessControlCheckingDesignerConfig, WithMockableDeploymentManager} import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} import pl.touk.nussknacker.test.utils.scalas.AkkaHttpExtensions.toRequestEntity +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity import pl.touk.nussknacker.ui.config.scenariotoolbar.CategoriesScenarioToolbarsConfigParser import pl.touk.nussknacker.ui.config.scenariotoolbar.ToolbarButtonConfigType.{CustomLink, ProcessDeploy, ProcessSave} import pl.touk.nussknacker.ui.config.scenariotoolbar.ToolbarPanelTypeConfig.{ @@ -45,7 +46,6 @@ import pl.touk.nussknacker.ui.config.scenariotoolbar.ToolbarPanelTypeConfig.{ } import pl.touk.nussknacker.ui.process.ProcessService.{CreateScenarioCommand, UpdateScenarioCommand} import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.ProcessActivity import pl.touk.nussknacker.ui.process.repository.{FetchingProcessRepository, UpdateProcessComment} import pl.touk.nussknacker.ui.process.{ScenarioQuery, ScenarioToolbarSettings, ToolbarButton, ToolbarPanel} import pl.touk.nussknacker.ui.security.api.SecurityError.ImpersonationMissingPermissionError diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceBusinessSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceBusinessSpec.scala index 4a8dc4a0d3f..98a82c516db 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceBusinessSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/ScenarioActivityApiHttpServiceBusinessSpec.scala @@ -4,11 +4,6 @@ import io.restassured.RestAssured.`given` import io.restassured.module.scala.RestAssuredSupport.AddThenToResponse import org.scalatest.freespec.AnyFreeSpecLike import pl.touk.nussknacker.engine.build.ScenarioBuilder -import pl.touk.nussknacker.test.{ - NuRestAssureExtensions, - NuRestAssureMatchers, - RestAssuredVerboseLoggingIfValidationFails -} import pl.touk.nussknacker.test.base.it.{NuItTest, WithSimplifiedConfigScenarioHelper} import pl.touk.nussknacker.test.config.{ WithBusinessCaseRestAssuredUsersExtensions, @@ -16,6 +11,11 @@ import pl.touk.nussknacker.test.config.{ WithSimplifiedDesignerConfig } import pl.touk.nussknacker.test.processes.WithScenarioActivitySpecAsserts +import pl.touk.nussknacker.test.{ + NuRestAssureExtensions, + NuRestAssureMatchers, + RestAssuredVerboseLoggingIfValidationFails +} import java.util.UUID @@ -47,7 +47,7 @@ class ScenarioActivityApiHttpServiceBusinessSpec .source("sourceId", "barSource") .emptySink("sinkId", "barSink") - "The scenario activity endpoint when" - { + "Deprecated scenario activity endpoint when" - { "return empty comments and attachment for existing process without them" in { given() .applicationState { @@ -78,7 +78,7 @@ class ScenarioActivityApiHttpServiceBusinessSpec } } - "The scenario add comment endpoint when" - { + "Deprecated scenario add comment endpoint when" - { "add comment in existing scenario" in { given() .applicationState { @@ -113,7 +113,7 @@ class ScenarioActivityApiHttpServiceBusinessSpec } } - "The scenario remove comment endpoint when" - { + "Deprecated scenario remove comment endpoint when" - { "remove comment in existing scenario" in { val commentId = given() .applicationState { @@ -296,13 +296,170 @@ class ScenarioActivityApiHttpServiceBusinessSpec } } + "The scenario activity endpoint when" - { + "return empty activities for existing process without them" in { + given() + .applicationState { + createSavedScenario(exampleScenario) + } + .when() + .basicAuthAllPermUser() + .get(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/activities") + .Then() + .statusCode(200) + .equalsJsonBody( + s""" + |{ + | "activities": [] + |} + |""".stripMargin + ) + } + "return 404 for no existing scenario" in { + given() + .when() + .basicAuthAllPermUser() + .get(s"$nuDesignerHttpAddress/api/processes/$wrongScenarioName/activity/activities") + .Then() + .statusCode(404) + .equalsPlainBody(s"No scenario $wrongScenarioName found") + } + } + + "The scenario add comment endpoint when" - { + "add comment in existing scenario" in { + given() + .applicationState { + createSavedScenario(exampleScenario) + } + .when() + .basicAuthAllPermUser() + .plainBody(commentContent) + .post(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/1/activity/comment") + .Then() + .statusCode(200) + .verifyApplicationState { + verifyCommentExists( + scenarioName = exampleScenarioName, + commentContent = commentContent, + commentUser = "allpermuser" + ) + } + } + "return 404 for no existing scenario" in { + given() + .applicationState { + createSavedScenario(exampleScenario) + } + .when() + .basicAuthAllPermUser() + .plainBody(commentContent) + .post(s"$nuDesignerHttpAddress/api/processes/$wrongScenarioName/1/activity/comment") + .Then() + .statusCode(404) + .equalsPlainBody(s"No scenario $wrongScenarioName found") + } + } + + "The scenario edit comment endpoint when" - { + "edit comment in existing scenario" in { + val newContent = "New comment content after modification" + + val commentActivityId = given() + .applicationState { + createSavedScenario(exampleScenario) + createComment(scenarioName = exampleScenarioName, commentContent = commentContent) + } + .when() + .basicAuthAllPermUser() + .get(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/activities") + .Then() + .extractString("activities[0].id") + + given() + .when() + .basicAuthAllPermUser() + .plainBody(newContent) + .put(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/comment/$commentActivityId") + .Then() + .statusCode(200) + .verifyApplicationState { + verifyCommentExists( + scenarioName = exampleScenarioName, + commentContent = newContent, + commentUser = "allpermuser" + ) + } + } + "return 404 for no existing scenario" in { + given() + .applicationState { + createSavedScenario(exampleScenario) + createComment(scenarioName = exampleScenarioName, commentContent = commentContent) + } + .when() + .basicAuthAllPermUser() + .plainBody(commentContent) + .post(s"$nuDesignerHttpAddress/api/processes/$wrongScenarioName/1/activity/comment") + .Then() + .statusCode(404) + .equalsPlainBody(s"No scenario $wrongScenarioName found") + } + } + + "The scenario remove comment endpoint when" - { + "remove comment in existing scenario" in { + val commentActivityId = given() + .applicationState { + createSavedScenario(exampleScenario) + createComment(scenarioName = exampleScenarioName, commentContent = commentContent) + } + .when() + .basicAuthAllPermUser() + .get(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/activities") + .Then() + .extractString("activities[0].id") + + given() + .when() + .basicAuthAllPermUser() + .delete(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/comment/$commentActivityId") + .Then() + .statusCode(200) + .verifyApplicationState { + verifyEmptyCommentsAndAttachments(exampleScenarioName) + } + } + "return 500 for no existing comment" in { + given() + .applicationState { + createSavedScenario(exampleScenario) + } + .when() + .basicAuthAllPermUser() + .delete(s"$nuDesignerHttpAddress/api/processes/$exampleScenarioName/activity/comments/1") + .Then() + .statusCode(500) + .equalsPlainBody("Unable to delete comment with id: 1") + } + "return 404 for no existing scenario" in { + given() + .when() + .basicAuthAllPermUser() + .delete(s"$nuDesignerHttpAddress/api/processes/$wrongScenarioName/activity/comments/1") + .Then() + .statusCode(404) + .equalsPlainBody(s"No scenario $wrongScenarioName found") + } + } + private def createComment(scenarioName: String, commentContent: String): Unit = { given() .when() .plainBody(commentContent) .basicAuthAllPermUser() .when() - .post(s"$nuDesignerHttpAddress/api/processes/$scenarioName/1/activity/comments") + .post(s"$nuDesignerHttpAddress/api/processes/$scenarioName/1/activity/comment") } private def createAttachment( diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala index 561cf82af7c..799acc6eb11 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/definition/component/DefaultComponentServiceSpec.scala @@ -869,7 +869,7 @@ class DefaultComponentServiceSpec processResolverByProcessingType = TestFactory.processResolverByProcessingType, dbioRunner = TestFactory.newDummyDBIOActionRunner(), fetchingProcessRepository = MockFetchingProcessRepository.withProcessesDetails(processes), - processActionRepository = TestFactory.newDummyActionRepository(), + scenarioActionRepository = TestFactory.newDummyActionRepository(), processRepository = TestFactory.newDummyWriteProcessRepository() ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/initialization/InitializationOnDbItSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/initialization/InitializationOnDbItSpec.scala index 9c6db097444..6d1f2b67a7d 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/initialization/InitializationOnDbItSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/initialization/InitializationOnDbItSpec.scala @@ -6,17 +6,18 @@ import org.scalatest.tags.Slow import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.test.PatientScalaFutures -import pl.touk.nussknacker.test.utils.domain.TestFactory.mapProcessingTypeDataProvider import pl.touk.nussknacker.test.base.db.{DbTesting, WithHsqlDbTesting, WithPostgresDbTesting, WithTestDb} +import pl.touk.nussknacker.test.base.it.WithClock +import pl.touk.nussknacker.test.utils.domain.TestFactory.mapProcessingTypeDataProvider import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} import pl.touk.nussknacker.ui.process.ScenarioQuery import pl.touk.nussknacker.ui.process.migrate.TestMigrations import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction -class InitializationOnHsqlItSpec extends InitializationOnDbItSpec with WithHsqlDbTesting +class InitializationOnHsqlItSpec extends InitializationOnDbItSpec with WithHsqlDbTesting with WithClock @Slow -class InitializationOnPostgresItSpec extends InitializationOnDbItSpec with WithPostgresDbTesting +class InitializationOnPostgresItSpec extends InitializationOnDbItSpec with WithPostgresDbTesting with WithClock abstract class InitializationOnDbItSpec extends AnyFlatSpec @@ -24,7 +25,7 @@ abstract class InitializationOnDbItSpec with PatientScalaFutures with BeforeAndAfterEach with BeforeAndAfterAll { - this: DbTesting with WithTestDb => + this: DbTesting with WithTestDb with WithClock => import Initialization.nussknackerUser @@ -34,7 +35,7 @@ abstract class InitializationOnDbItSpec private val migrations = mapProcessingTypeDataProvider("streaming" -> new TestMigrations(1, 2)) - private lazy val commentRepository = TestFactory.newCommentRepository(testDbRef) + private lazy val scenarioActivityRepository = TestFactory.newScenarioActivityRepository(testDbRef, clock) private lazy val scenarioLabelsRepository = TestFactory.newScenarioLabelsRepository(testDbRef) @@ -42,14 +43,14 @@ abstract class InitializationOnDbItSpec private lazy val dbioRunner = TestFactory.newDBIOActionRunner(testDbRef) - private lazy val writeRepository = TestFactory.newWriteProcessRepository(testDbRef) + private lazy val writeRepository = TestFactory.newWriteProcessRepository(testDbRef, clock) private def sampleCanonicalProcess(processName: ProcessName) = ProcessTestData.validProcessWithName(processName) it should "migrate processes" in { saveSampleProcess() - Initialization.init(migrations, testDbRef, scenarioRepository, commentRepository, scenarioLabelsRepository, "env1") + Initialization.init(migrations, testDbRef, clock, scenarioRepository, scenarioActivityRepository, scenarioLabelsRepository, "env1") dbioRunner .runInTransaction( @@ -68,7 +69,7 @@ abstract class InitializationOnDbItSpec saveSampleProcess(ProcessName(s"id$id")) } - Initialization.init(migrations, testDbRef, scenarioRepository, commentRepository, scenarioLabelsRepository, "env1") + Initialization.init(migrations, testDbRef, clock, scenarioRepository, scenarioActivityRepository, scenarioLabelsRepository, "env1") dbioRunner .runInTransaction( @@ -86,8 +87,9 @@ abstract class InitializationOnDbItSpec Initialization.init( mapProcessingTypeDataProvider("streaming" -> new TestMigrations(1, 2, 5)), testDbRef, + clock, scenarioRepository, - commentRepository, + scenarioActivityRepository, scenarioLabelsRepository, "env1" ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/notifications/NotificationServiceTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/notifications/NotificationServiceTest.scala index 6a9e3367dc4..d7238830075 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/notifications/NotificationServiceTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/notifications/NotificationServiceTest.scala @@ -32,7 +32,7 @@ import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeData import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction import pl.touk.nussknacker.ui.process.repository.{ DBIOActionRunner, - DbProcessActionRepository, + DbScenarioActionRepository, ScenarioWithDetailsEntity } import pl.touk.nussknacker.ui.security.api.LoggedUser @@ -61,13 +61,11 @@ class NotificationServiceTest private var currentInstant: Instant = Instant.ofEpochMilli(0) private val clock: Clock = clockForInstant(() => currentInstant) private val processRepository = TestFactory.newFetchingProcessRepository(testDbRef) - private val writeProcessRepository = TestFactory.newWriteProcessRepository(testDbRef) - private val commentRepository = TestFactory.newCommentRepository(testDbRef) + private val writeProcessRepository = TestFactory.newWriteProcessRepository(testDbRef, clock) private val actionRepository = - new DbProcessActionRepository( + new DbScenarioActionRepository( testDbRef, - commentRepository, ProcessingTypeDataProvider.withEmptyCombinedData(Map.empty) ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/DBProcessServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/DBProcessServiceSpec.scala index 1142aaeb91d..f032ca8bb05 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/DBProcessServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/DBProcessServiceSpec.scala @@ -208,7 +208,7 @@ class DBProcessServiceSpec extends AnyFlatSpec with Matchers with PatientScalaFu processResolverByProcessingType = TestFactory.processResolverByProcessingType, dbioRunner = TestFactory.newDummyDBIOActionRunner(), fetchingProcessRepository = MockFetchingProcessRepository.withProcessesDetails(processes), - processActionRepository = TestFactory.newDummyActionRepository(), + scenarioActionRepository = TestFactory.newDummyActionRepository(), processRepository = TestFactory.newDummyWriteProcessRepository() ) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentServiceSpec.scala index ccdeb9899e0..17cdc4f94e0 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/ScenarioAttachmentServiceSpec.scala @@ -1,23 +1,32 @@ package pl.touk.nussknacker.ui.process +import db.util.DBIOActionInstances.DB import org.scalatest.concurrent.ScalaFutures import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.deployment.{ScenarioActivity, ScenarioActivityId} import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} +import pl.touk.nussknacker.test.utils.domain.TestFactory +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.ProcessActivity import pl.touk.nussknacker.ui.config.AttachmentsConfig import pl.touk.nussknacker.ui.db.entity.AttachmentEntityData -import pl.touk.nussknacker.ui.listener.Comment -import pl.touk.nussknacker.ui.process.repository.{DbProcessActivityRepository, ProcessActivityRepository} +import pl.touk.nussknacker.ui.process.repository.activities.ScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} +import slick.dbio.DBIO import java.io.ByteArrayInputStream -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} import scala.util.Random class ScenarioAttachmentServiceSpec extends AnyFunSuite with Matchers with ScalaFutures { private implicit val ec: ExecutionContextExecutor = ExecutionContext.global private implicit val user: LoggedUser = RealLoggedUser("test user", "test user") - private val service = new ScenarioAttachmentService(AttachmentsConfig(10), TestProcessActivityRepository) + + private val service = new ScenarioAttachmentService( + AttachmentsConfig(10), + TestProcessActivityRepository, + TestFactory.newDummyDBIOActionRunner() + ) test("should respect size limit") { val random12bytes = new ByteArrayInputStream(nextBytes(12)) @@ -41,27 +50,43 @@ class ScenarioAttachmentServiceSpec extends AnyFunSuite with Matchers with Scala } -private object TestProcessActivityRepository extends ProcessActivityRepository { +private object TestProcessActivityRepository extends ScenarioActivityRepository { + + override def findActivities(scenarioId: ProcessId): DB[Seq[ScenarioActivity]] = ??? + + override def addActivity(scenarioActivity: ScenarioActivity)(implicit user: LoggedUser): DB[ScenarioActivityId] = ??? + + override def addComment(scenarioId: ProcessId, processVersionId: VersionId, comment: String)( + implicit user: LoggedUser + ): DB[ScenarioActivityId] = ??? + + override def addAttachment(attachmentToAdd: ScenarioAttachmentService.AttachmentToAdd)( + implicit user: LoggedUser + ): DB[ScenarioActivityId] = + DBIO.successful(ScenarioActivityId.random) + + override def findAttachments(scenarioId: ProcessId): DB[Seq[AttachmentEntityData]] = ??? + + override def findAttachment(scenarioId: ProcessId, attachmentId: Long): DB[Option[AttachmentEntityData]] = ??? + + override def findActivity(processId: ProcessId): DB[ProcessActivity] = ??? - override def addComment(processId: ProcessId, processVersionId: VersionId, comment: Comment)( - implicit ec: ExecutionContext, - loggedUser: LoggedUser - ): Future[Unit] = ??? + override def getActivityStats: DB[Map[String, Int]] = ??? - override def deleteComment(commentId: Long)(implicit ec: ExecutionContext): Future[Either[Exception, Unit]] = ??? + override def editComment(scenarioId: ProcessId, scenarioActivityId: ScenarioActivityId, comment: String)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def findActivity(processId: ProcessId)( - implicit ec: ExecutionContext - ): Future[DbProcessActivityRepository.ProcessActivity] = ??? + override def editComment(scenarioId: ProcessId, commentId: Long, comment: String)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def addAttachment( - attachmentToAdd: ScenarioAttachmentService.AttachmentToAdd - )(implicit ec: ExecutionContext, loggedUser: LoggedUser): Future[Unit] = Future.successful(()) + override def deleteComment(scenarioId: ProcessId, commentId: Long)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def findAttachment(attachmentId: Long, scenarioId: ProcessId)( - implicit ec: ExecutionContext - ): Future[Option[AttachmentEntityData]] = - ??? + override def deleteComment(scenarioId: ProcessId, scenarioActivityId: ScenarioActivityId)( + implicit user: LoggedUser + ): DB[Either[ScenarioActivityRepository.ModifyCommentError, Unit]] = ??? - override def getActivityStats(implicit ec: ExecutionContext): Future[Map[String, Int]] = ??? } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentServiceSpec.scala index 94c820069dc..661e3cbc235 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/deployment/DeploymentServiceSpec.scala @@ -19,6 +19,7 @@ import pl.touk.nussknacker.engine.api.process._ import pl.touk.nussknacker.engine.build.ScenarioBuilder import pl.touk.nussknacker.engine.deployment.{CustomActionResult, DeploymentId, ExternalDeploymentId} import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting +import pl.touk.nussknacker.test.base.it.WithClock import pl.touk.nussknacker.test.mock.{MockDeploymentManager, TestProcessChangeListener} import pl.touk.nussknacker.test.utils.domain.TestFactory._ import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} @@ -30,12 +31,7 @@ import pl.touk.nussknacker.ui.process.processingtype.provider.{ProcessingTypeDat import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider.noCombinedDataFun import pl.touk.nussknacker.ui.process.processingtype.ValueWithRestriction import pl.touk.nussknacker.ui.process.repository.ProcessRepository.CreateProcessAction -import pl.touk.nussknacker.ui.process.repository.{ - CommentValidationError, - DBIOActionRunner, - DeploymentComment, - UserComment -} +import pl.touk.nussknacker.ui.process.repository.{CommentValidationError, DBIOActionRunner, UserComment} import pl.touk.nussknacker.ui.process.{ScenarioQuery, ScenarioWithDetailsConversions} import pl.touk.nussknacker.ui.security.api.LoggedUser import slick.dbio.DBIOAction @@ -54,6 +50,7 @@ class DeploymentServiceSpec with BeforeAndAfterEach with BeforeAndAfterAll with WithHsqlDbTesting + with WithClock with EitherValuesDetailedMessage { import VersionId._ @@ -68,9 +65,9 @@ class DeploymentServiceSpec override protected val dbioRunner: DBIOActionRunner = newDBIOActionRunner(testDbRef) private val fetchingProcessRepository = newFetchingProcessRepository(testDbRef) private val futureFetchingProcessRepository = newFutureFetchingScenarioRepository(testDbRef) - private val writeProcessRepository = newWriteProcessRepository(testDbRef) + private val writeProcessRepository = newWriteProcessRepository(testDbRef, clock) private val actionRepository = newActionProcessRepository(testDbRef) - private val activityRepository = newProcessActivityRepository(testDbRef) + private val activityRepository = newScenarioActivityRepository(testDbRef, clock) private val processingTypeDataProvider: ProcessingTypeDataProvider[DeploymentManager, Nothing] = new ProcessingTypeDataProvider[DeploymentManager, Nothing] { @@ -335,7 +332,7 @@ class DeploymentServiceSpec lastStateAction.state shouldBe ProcessActionState.ExecutionFinished // we want to hide finished deploys processDetails.lastDeployedAction shouldBe empty - activityRepository.findActivity(processId.id).futureValue.comments should have length 1 + dbioRunner.run(activityRepository.findActivity(processId.id)).futureValue.comments should have length 1 deploymentManager.withEmptyProcessState(processName) { val stateAfterJobRetention = @@ -1025,7 +1022,7 @@ class DeploymentServiceSpec } private def prepareAction(processId: ProcessId, actionName: ScenarioActionName) = { - val comment = Some(DeploymentComment.unsafe(UserComment(actionName.toString.capitalize)).toComment(actionName)) + val comment = Some(UserComment(actionName.toString.capitalize)) actionRepository.addInstantAction(processId, initialVersionId, actionName, comment, None).map(_.id) } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentRepositorySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentRepositorySpec.scala index 7eec38ccf3b..8b54edda8d1 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentRepositorySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentRepositorySpec.scala @@ -8,6 +8,7 @@ import pl.touk.nussknacker.engine.api.deployment.DeploymentStatus import pl.touk.nussknacker.engine.newdeployment.DeploymentId import pl.touk.nussknacker.test.{EitherValuesDetailedMessage, PatientScalaFutures} import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting +import pl.touk.nussknacker.test.base.it.WithClock import pl.touk.nussknacker.test.config.WithCategoryUsedMoreThanOnceDesignerConfig.TestCategory import pl.touk.nussknacker.test.config.WithSimplifiedDesignerConfig.TestProcessingType.Streaming import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} @@ -24,6 +25,7 @@ class DeploymentRepositorySpec extends AnyFunSuite with Matchers with WithHsqlDbTesting + with WithClock with DBIOActionValues with PatientScalaFutures with OptionValues @@ -35,7 +37,7 @@ class DeploymentRepositorySpec private val deploymentRepository = new DeploymentRepository(testDbRef, Clock.fixed(Instant.ofEpochMilli(0), ZoneOffset.UTC)) - private val scenarioRepository = TestFactory.newWriteProcessRepository(testDbRef) + private val scenarioRepository = TestFactory.newWriteProcessRepository(testDbRef, clock) private lazy val sampleScenarioId = scenarioRepository .saveNewProcess( diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentServiceTest.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentServiceTest.scala index 31a593be083..e8259434b44 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentServiceTest.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/newdeployment/DeploymentServiceTest.scala @@ -10,6 +10,7 @@ import pl.touk.nussknacker.engine.api.process.ProcessName import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.newdeployment.DeploymentId import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting +import pl.touk.nussknacker.test.base.it.WithClock import pl.touk.nussknacker.test.config.WithSimplifiedDesignerConfig.TestProcessingType.Streaming import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} import pl.touk.nussknacker.test.utils.scalas.DBIOActionValues @@ -29,13 +30,14 @@ class DeploymentServiceTest with Matchers with PatientScalaFutures with WithHsqlDbTesting + with WithClock with DBIOActionValues with EitherValuesDetailedMessage with BeforeAndAfterEach { override protected val dbioRunner: DBIOActionRunner = DBIOActionRunner(testDbRef) - private val writeScenarioRepository = TestFactory.newWriteProcessRepository(testDbRef, modelVersions = None) + private val writeScenarioRepository = TestFactory.newWriteProcessRepository(testDbRef, clock, modelVersions = None) private val service = { val clock = Clock.fixed(Instant.ofEpochMilli(0), ZoneOffset.UTC) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepositorySpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepositorySpec.scala index 28dbbd453c3..4f355464161 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepositorySpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/process/repository/DBFetchingProcessRepositorySpec.scala @@ -12,17 +12,19 @@ import pl.touk.nussknacker.restmodel.component.ScenarioComponentsUsages import pl.touk.nussknacker.security.Permission import pl.touk.nussknacker.test.PatientScalaFutures import pl.touk.nussknacker.test.base.db.WithHsqlDbTesting +import pl.touk.nussknacker.test.base.it.WithClock import pl.touk.nussknacker.test.utils.domain.TestFactory.mapProcessingTypeDataProvider import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestFactory} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.Comment import pl.touk.nussknacker.ui.process.ScenarioQuery import pl.touk.nussknacker.ui.process.processingtype.provider.ProcessingTypeDataProvider -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.Comment import pl.touk.nussknacker.ui.process.repository.ProcessDBQueryRepository.ProcessAlreadyExists import pl.touk.nussknacker.ui.process.repository.ProcessRepository.{ CreateProcessAction, ProcessUpdated, UpdateProcessAction } +import pl.touk.nussknacker.ui.process.repository.activities.DbScenarioActivityRepository import pl.touk.nussknacker.ui.security.api.{LoggedUser, RealLoggedUser} import java.time.Instant @@ -36,18 +38,19 @@ class DBFetchingProcessRepositorySpec with BeforeAndAfterEach with BeforeAndAfterAll with WithHsqlDbTesting + with WithClock with PatientScalaFutures { private val dbioRunner = DBIOActionRunner(testDbRef) - private val commentRepository = new CommentRepository(testDbRef) + private val activities = new DbScenarioActivityRepository(testDbRef, clock) private val scenarioLabelsRepository = new ScenarioLabelsRepository(testDbRef) private val writingRepo = new DBProcessRepository( testDbRef, - commentRepository, + clock, activities, scenarioLabelsRepository, mapProcessingTypeDataProvider("Streaming" -> 0) ) { @@ -57,17 +60,14 @@ class DBFetchingProcessRepositorySpec private var currentTime: Instant = Instant.now() private val actions = - new DbProcessActionRepository( + new DbScenarioActionRepository( testDbRef, - commentRepository, ProcessingTypeDataProvider.withEmptyCombinedData(Map.empty) ) private val fetching = DBFetchingProcessRepository.createFutureRepository(testDbRef, actions, scenarioLabelsRepository) - private val activities = DbProcessActivityRepository(testDbRef, commentRepository) - private implicit val user: LoggedUser = TestFactory.adminUser() test("fetch processes for category") { @@ -160,11 +160,11 @@ class DBFetchingProcessRepositorySpec val comments = fetching .fetchProcessId(newName) - .flatMap(v => activities.findActivity(v.get).map(_.comments)) + .flatMap(v => dbioRunner.run(activities.findActivity(v.get).map(_.comments))) .futureValue atLeast(1, comments) should matchPattern { - case Comment(_, VersionId(1L), "Rename: [oldName] -> [newName]", user.username, _) => + case Comment(_, 1L, "Rename: [oldName] -> [newName]", user.username, _) => } } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/util/PdfExporterSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/util/PdfExporterSpec.scala index b8fb2559f75..2f57c408625 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/util/PdfExporterSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/util/PdfExporterSpec.scala @@ -9,8 +9,8 @@ import pl.touk.nussknacker.engine.api.process.{ProcessName, ScenarioVersion, Ver import pl.touk.nussknacker.engine.graph.node.{Filter, UserDefinedAdditionalNodeFields} import pl.touk.nussknacker.engine.util.ResourceLoader import pl.touk.nussknacker.test.utils.domain.{ProcessTestData, TestProcessUtil} +import pl.touk.nussknacker.ui.api.description.scenarioActivity.Dtos.Legacy.{Comment, ProcessActivity} import pl.touk.nussknacker.ui.process.marshall.CanonicalProcessConverter -import pl.touk.nussknacker.ui.process.repository.DbProcessActivityRepository.{Comment, ProcessActivity} import java.io.FileOutputStream import java.time.Instant @@ -36,7 +36,7 @@ class PdfExporterSpec extends AnyFlatSpec with Matchers { .map(commentId => Comment( commentId, - details.processVersionId, + details.processVersionId.value, "Jakiś taki dziwny ten proces??", "Wacław Wójcik", Instant.now() diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 898dd266f91..6ee2b7ee855 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -3105,8 +3105,8 @@ paths: type: string examples: Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + summary: 'Unable to edit comment for activity with id: {commentId}' + value: 'Unable to delete comment for activity with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3195,8 +3195,8 @@ paths: type: string examples: Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + summary: 'Unable to edit comment for activity with id: {commentId}' + value: 'Unable to delete comment for activity with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3374,8 +3374,8 @@ paths: type: string examples: Example: - summary: 'Unable to edit comment with id: {commentId}' - value: 'Unable to delete comment with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' + summary: 'Unable to edit comment for activity with id: {commentId}' + value: 'Unable to delete comment for activity with id: a76d6eba-9b6c-4d97-aaa1-984a23f88019' '501': description: Impersonation is not supported for defined authentication mechanism content: @@ -3743,6 +3743,12 @@ paths: user: some user date: '2024-01-17T14:21:17Z' scenarioVersion: 1 + comment: + status: + comment: Run campaign + type: AVAILABLE + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' dateFinished: '2024-01-17T14:21:17Z' errorMessage: Execution error occurred type: PERFORMED_SINGLE_EXECUTION @@ -3750,6 +3756,11 @@ paths: user: some user date: '2024-01-17T14:21:17Z' scenarioVersion: 1 + comment: + status: + type: DELETED + lastModifiedBy: some user + lastModifiedAt: '2024-01-17T14:21:17Z' dateFinished: '2024-01-17T14:21:17Z' type: PERFORMED_SINGLE_EXECUTION - id: 9b27797e-aa03-42ba-8406-d0ae8005a883 @@ -4416,6 +4427,7 @@ components: - user - date - actionName + - comment - type properties: id: @@ -4433,6 +4445,8 @@ components: format: int64 actionName: type: string + comment: + $ref: '#/components/schemas/ScenarioActivityComment' type: type: string CustomActionRequest: @@ -5705,7 +5719,6 @@ components: - id - user - date - - dateFinished - type properties: id: @@ -5722,7 +5735,9 @@ components: - 'null' format: int64 dateFinished: - type: string + type: + - string + - 'null' format: date-time errorMessage: type: @@ -5737,7 +5752,7 @@ components: - id - user - date - - dateFinished + - comment - type properties: id: @@ -5753,8 +5768,12 @@ components: - integer - 'null' format: int64 + comment: + $ref: '#/components/schemas/ScenarioActivityComment' dateFinished: - type: string + type: + - string + - 'null' format: date-time errorMessage: type: diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala index 1d7ae8184b3..ebb91e628c3 100644 --- a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ProcessAction.scala @@ -9,6 +9,9 @@ import pl.touk.nussknacker.engine.api.process.{ProcessId, VersionId} import java.time.Instant import java.util.UUID +// todo NU-1772 +// - should be eventually replaced with pl.touk.nussknacker.engine.api.deployment.ScenarioActivity +// - this class is currently a compatibility layer for older fragments of code, new code should use ScenarioActivity @JsonCodec case class ProcessAction( id: ProcessActionId, processId: ProcessId, diff --git a/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala new file mode 100644 index 00000000000..97ad3d2255b --- /dev/null +++ b/extensions-api/src/main/scala/pl/touk/nussknacker/engine/api/deployment/ScenarioActivity.scala @@ -0,0 +1,248 @@ +package pl.touk.nussknacker.engine.api.deployment + +import pl.touk.nussknacker.engine.api.component.ProcessingMode + +import java.time.Instant +import java.util.UUID + +final case class ScenarioId(value: Long) extends AnyVal + +final case class ScenarioVersion(value: Long) extends AnyVal + +final case class ScenarioActivityId(value: UUID) extends AnyVal + +object ScenarioActivityId { + def random: ScenarioActivityId = ScenarioActivityId(UUID.randomUUID()) +} + +final case class ScenarioUser( + id: Option[UserId], + name: UserName, + impersonatedByUserId: Option[UserId], + impersonatedByUserName: Option[UserName], +) + +final case class UserId(value: String) +final case class UserName(value: String) + +sealed trait ScenarioComment + +object ScenarioComment { + + final case class Available( + comment: String, + lastModifiedByUserName: UserName, + lastModifiedAt: Instant, + ) extends ScenarioComment + + final case class Deleted( + deletedByUserName: UserName, + deletedAt: Instant, + ) extends ScenarioComment + +} + +sealed trait ScenarioAttachment + +object ScenarioAttachment { + + final case class Available( + attachmentId: AttachmentId, + attachmentFilename: AttachmentFilename, + lastModifiedByUserName: UserName, + lastModifiedAt: Instant, + ) extends ScenarioAttachment + + final case class Deleted( + attachmentFilename: AttachmentFilename, + deletedByUserName: UserName, + deletedAt: Instant, + ) extends ScenarioAttachment + + final case class AttachmentId(value: Long) extends AnyVal + final case class AttachmentFilename(value: String) extends AnyVal +} + +final case class Environment(name: String) extends AnyVal + +sealed trait ScenarioActivity { + def scenarioId: ScenarioId + def scenarioActivityId: ScenarioActivityId + def user: ScenarioUser + def date: Instant + def scenarioVersion: Option[ScenarioVersion] +} + +object ScenarioActivity { + + final case class ScenarioCreated( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + final case class ScenarioArchived( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + final case class ScenarioUnarchived( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + ) extends ScenarioActivity + + // Scenario deployments + + final case class ScenarioDeployed( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioPaused( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioCanceled( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + // Scenario modifications + + final case class ScenarioModified( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class ScenarioNameChanged( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + oldName: String, + newName: String, + ) extends ScenarioActivity + + final case class CommentAdded( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + ) extends ScenarioActivity + + final case class AttachmentAdded( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + attachment: ScenarioAttachment, + ) extends ScenarioActivity + + final case class ChangedProcessingMode( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + from: ProcessingMode, + to: ProcessingMode, + ) extends ScenarioActivity + + // Migration between environments + + final case class IncomingMigration( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + sourceEnvironment: Environment, + sourceScenarioVersion: ScenarioVersion, + ) extends ScenarioActivity + + final case class OutgoingMigration( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + destinationEnvironment: Environment, + ) extends ScenarioActivity + + // Batch + + final case class PerformedSingleExecution( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + comment: ScenarioComment, + dateFinished: Option[Instant], + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class PerformedScheduledExecution( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + dateFinished: Option[Instant], + errorMessage: Option[String], + ) extends ScenarioActivity + + // Other/technical + + final case class AutomaticUpdate( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + dateFinished: Instant, + changes: String, + errorMessage: Option[String], + ) extends ScenarioActivity + + final case class CustomAction( + scenarioId: ScenarioId, + scenarioActivityId: ScenarioActivityId, + user: ScenarioUser, + date: Instant, + scenarioVersion: Option[ScenarioVersion], + actionName: String, + comment: ScenarioComment, + ) extends ScenarioActivity + +}